commit
e9454e1b05
|
@ -73,32 +73,35 @@ export default RESTAdapter.extend({
|
|||
//
|
||||
// This is the original implementation of _buildURL
|
||||
// without the pluralization of modelName
|
||||
urlForFindRecord(id, modelName) {
|
||||
let path;
|
||||
let url = [];
|
||||
let host = get(this, 'host');
|
||||
let prefix = this.urlPrefix();
|
||||
|
||||
if (modelName) {
|
||||
path = modelName.camelize();
|
||||
if (path) {
|
||||
url.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (id) {
|
||||
url.push(encodeURIComponent(id));
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
url.unshift(prefix);
|
||||
}
|
||||
|
||||
url = url.join('/');
|
||||
if (!host && url && url.charAt(0) !== '/') {
|
||||
url = '/' + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
urlForFindRecord: urlForRecord,
|
||||
urlForUpdateRecord: urlForRecord,
|
||||
});
|
||||
|
||||
function urlForRecord(id, modelName) {
|
||||
let path;
|
||||
let url = [];
|
||||
let host = get(this, 'host');
|
||||
let prefix = this.urlPrefix();
|
||||
|
||||
if (modelName) {
|
||||
path = modelName.camelize();
|
||||
if (path) {
|
||||
url.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (id) {
|
||||
url.push(encodeURIComponent(id));
|
||||
}
|
||||
|
||||
if (prefix) {
|
||||
url.unshift(prefix);
|
||||
}
|
||||
|
||||
url = url.join('/');
|
||||
if (!host && url && url.charAt(0) !== '/') {
|
||||
url = '/' + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,12 @@ export default Watchable.extend({
|
|||
return associateNamespace(url, namespace);
|
||||
},
|
||||
|
||||
urlForUpdateRecord(id, type, hash) {
|
||||
const [name, namespace] = JSON.parse(id);
|
||||
let url = this._super(name, type, hash);
|
||||
return associateNamespace(url, namespace);
|
||||
},
|
||||
|
||||
xhrKey(url, method, options = {}) {
|
||||
const plainKey = this._super(...arguments);
|
||||
const namespace = options.data && options.data.namespace;
|
||||
|
@ -71,15 +77,19 @@ export default Watchable.extend({
|
|||
},
|
||||
|
||||
plan(job) {
|
||||
const url = addToPath(this.urlForFindRecord(job.get('id'), 'job'), '/plan');
|
||||
const jobId = job.get('id');
|
||||
const store = this.get('store');
|
||||
const url = addToPath(this.urlForFindRecord(jobId, 'job'), '/plan');
|
||||
|
||||
return this.ajax(url, 'POST', {
|
||||
data: {
|
||||
Job: job.get('_newDefinitionJSON'),
|
||||
Diff: true,
|
||||
},
|
||||
}).then(json => {
|
||||
json.ID = job.get('id');
|
||||
this.get('store').pushPayload('job-plan', { jobPlans: [json] });
|
||||
json.ID = jobId;
|
||||
store.pushPayload('job-plan', { jobPlans: [json] });
|
||||
return store.peekRecord('job-plan', jobId);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -92,6 +102,14 @@ export default Watchable.extend({
|
|||
},
|
||||
});
|
||||
},
|
||||
|
||||
update(job) {
|
||||
return this.ajax(this.urlForUpdateRecord(job.get('id'), 'job'), 'POST', {
|
||||
data: {
|
||||
Job: job.get('_newDefinitionJSON'),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function associateNamespace(url, namespace) {
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import Component from '@ember/component';
|
||||
import { assert } from '@ember/debug';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
|
||||
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
|
||||
|
||||
export default Component.extend({
|
||||
store: service(),
|
||||
config: service(),
|
||||
|
||||
'data-test-job-editor': true,
|
||||
|
||||
job: null,
|
||||
onSubmit() {},
|
||||
context: computed({
|
||||
get() {
|
||||
return this.get('_context');
|
||||
},
|
||||
set(key, value) {
|
||||
const allowedValues = ['new', 'edit'];
|
||||
|
||||
assert(`context must be one of: ${allowedValues.join(', ')}`, allowedValues.includes(value));
|
||||
|
||||
this.set('_context', value);
|
||||
return value;
|
||||
},
|
||||
}),
|
||||
|
||||
_context: null,
|
||||
parseError: null,
|
||||
planError: null,
|
||||
runError: null,
|
||||
|
||||
planOutput: null,
|
||||
|
||||
showPlanMessage: localStorageProperty('nomadMessageJobPlan', true),
|
||||
showEditorMessage: localStorageProperty('nomadMessageJobEditor', true),
|
||||
|
||||
stage: computed('planOutput', function() {
|
||||
return this.get('planOutput') ? 'plan' : 'editor';
|
||||
}),
|
||||
|
||||
plan: task(function*() {
|
||||
this.reset();
|
||||
|
||||
try {
|
||||
yield this.get('job').parse();
|
||||
} catch (err) {
|
||||
const error = messageFromAdapterError(err) || 'Could not parse input';
|
||||
this.set('parseError', error);
|
||||
this.scrollToError();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const plan = yield this.get('job').plan();
|
||||
this.set('planOutput', plan);
|
||||
} catch (err) {
|
||||
const error = messageFromAdapterError(err) || 'Could not plan job';
|
||||
this.set('planError', error);
|
||||
this.scrollToError();
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
submit: task(function*() {
|
||||
try {
|
||||
if (this.get('context') === 'new') {
|
||||
yield this.get('job').run();
|
||||
} else {
|
||||
yield this.get('job').update();
|
||||
}
|
||||
|
||||
const id = this.get('job.plainId');
|
||||
const namespace = this.get('job.namespace.name') || 'default';
|
||||
|
||||
this.reset();
|
||||
|
||||
// Treat the job as ephemeral and only provide ID parts.
|
||||
this.get('onSubmit')(id, namespace);
|
||||
} catch (err) {
|
||||
const error = messageFromAdapterError(err) || 'Could not submit job';
|
||||
this.set('runError', error);
|
||||
this.set('planOutput', null);
|
||||
this.scrollToError();
|
||||
}
|
||||
}),
|
||||
|
||||
reset() {
|
||||
this.set('planOutput', null);
|
||||
this.set('planError', null);
|
||||
this.set('parseError', null);
|
||||
this.set('runError', null);
|
||||
},
|
||||
|
||||
scrollToError() {
|
||||
if (!this.get('config.isTest')) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -4,4 +4,22 @@ import { alias } from '@ember/object/computed';
|
|||
|
||||
export default Controller.extend(WithNamespaceResetting, {
|
||||
job: alias('model.job'),
|
||||
definition: alias('model.definition'),
|
||||
|
||||
isEditing: false,
|
||||
|
||||
edit() {
|
||||
this.get('job').set('_newDefinition', JSON.stringify(this.get('definition'), null, 2));
|
||||
this.set('isEditing', true);
|
||||
},
|
||||
|
||||
onCancel() {
|
||||
this.set('isEditing', false);
|
||||
},
|
||||
|
||||
onSubmit(id, namespace) {
|
||||
this.transitionToRoute('jobs.job', id, {
|
||||
queryParams: { jobNamespace: namespace },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,69 +1,9 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import { task } from 'ember-concurrency';
|
||||
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
|
||||
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
|
||||
|
||||
export default Controller.extend({
|
||||
store: service(),
|
||||
|
||||
parseError: null,
|
||||
planError: null,
|
||||
runError: null,
|
||||
|
||||
planOutput: null,
|
||||
|
||||
showPlanMessage: localStorageProperty('nomadMessageJobPlan', true),
|
||||
showEditorMessage: localStorageProperty('nomadMessageJobEditor', true),
|
||||
|
||||
stage: computed('planOutput', function() {
|
||||
return this.get('planOutput') ? 'plan' : 'editor';
|
||||
}),
|
||||
|
||||
plan: task(function*() {
|
||||
this.reset();
|
||||
|
||||
try {
|
||||
yield this.get('model').parse();
|
||||
} catch (err) {
|
||||
const error = messageFromAdapterError(err) || 'Could not parse input';
|
||||
this.set('parseError', error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield this.get('model').plan();
|
||||
const plan = this.get('store').peekRecord('job-plan', this.get('model.id'));
|
||||
this.set('planOutput', plan);
|
||||
} catch (err) {
|
||||
const error = messageFromAdapterError(err) || 'Could not plan job';
|
||||
this.set('planError', error);
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
submit: task(function*() {
|
||||
try {
|
||||
yield this.get('model').run();
|
||||
|
||||
const id = this.get('model.plainId');
|
||||
const namespace = this.get('model.namespace.name') || 'default';
|
||||
|
||||
this.reset();
|
||||
|
||||
// navigate to the new job page
|
||||
this.transitionToRoute('jobs.job', id, {
|
||||
queryParams: { jobNamespace: namespace },
|
||||
});
|
||||
} catch (err) {
|
||||
const error = messageFromAdapterError(err) || 'Could not submit job';
|
||||
this.set('runError', error);
|
||||
}
|
||||
}),
|
||||
|
||||
reset() {
|
||||
this.set('planOutput', null);
|
||||
this.set('planError', null);
|
||||
this.set('parseError', null);
|
||||
onSubmit(id, namespace) {
|
||||
this.transitionToRoute('jobs.job', id, {
|
||||
queryParams: { jobNamespace: namespace },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -203,6 +203,11 @@ export default Model.extend({
|
|||
return this.store.adapterFor('job').run(this);
|
||||
},
|
||||
|
||||
update() {
|
||||
assert('A job must be parsed before updated', this.get('_newDefinitionJSON'));
|
||||
return this.store.adapterFor('job').update(this);
|
||||
},
|
||||
|
||||
parse() {
|
||||
const definition = this.get('_newDefinition');
|
||||
let promise;
|
||||
|
@ -211,7 +216,12 @@ export default Model.extend({
|
|||
// If the definition is already JSON then it doesn't need to be parsed.
|
||||
const json = JSON.parse(definition);
|
||||
this.set('_newDefinitionJSON', json);
|
||||
this.setIDByPayload(json);
|
||||
|
||||
// You can't set the ID of a record that already exists
|
||||
if (this.get('isNew')) {
|
||||
this.setIdByPayload(json);
|
||||
}
|
||||
|
||||
promise = RSVP.resolve(definition);
|
||||
} catch (err) {
|
||||
// If the definition is invalid JSON, assume it is HCL. If it is invalid
|
||||
|
@ -221,14 +231,14 @@ export default Model.extend({
|
|||
.parse(this.get('_newDefinition'))
|
||||
.then(response => {
|
||||
this.set('_newDefinitionJSON', response);
|
||||
this.setIDByPayload(response);
|
||||
this.setIdByPayload(response);
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
},
|
||||
|
||||
setIDByPayload(payload) {
|
||||
setIdByPayload(payload) {
|
||||
const namespace = payload.Namespace || 'default';
|
||||
const id = payload.Name;
|
||||
|
||||
|
@ -241,6 +251,10 @@ export default Model.extend({
|
|||
}
|
||||
},
|
||||
|
||||
resetId() {
|
||||
this.set('id', JSON.stringify([this.get('plainId'), this.get('namespace.name') || 'default']));
|
||||
},
|
||||
|
||||
statusClass: computed('status', function() {
|
||||
const classMap = {
|
||||
pending: 'is-pending',
|
||||
|
|
|
@ -16,6 +16,7 @@ Router.map(function() {
|
|||
this.route('deployments');
|
||||
this.route('evaluations');
|
||||
this.route('allocations');
|
||||
this.route('edit');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,4 +8,13 @@ export default Route.extend({
|
|||
definition,
|
||||
}));
|
||||
},
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
if (isExiting) {
|
||||
const job = controller.get('job');
|
||||
job.rollbackAttributes();
|
||||
job.resetId();
|
||||
controller.set('isEditing', false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,6 +18,6 @@ export default Service.extend({
|
|||
},
|
||||
|
||||
setIndexFor(url, value) {
|
||||
list[url] = value;
|
||||
list[url] = +value;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.page-layout {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<div class="chart-tooltip {{if isActive "active" "inactive"}}" style={{tooltipStyle}}>
|
||||
<ol>
|
||||
{{#each _data as |datum index|}}
|
||||
<li class="{{if (eq datum.index activeDatum.index) "active"}}">
|
||||
<li class="{{if (eq datum.label activeDatum.label) "active"}}">
|
||||
<span class="label {{if (eq datum.value 0) "is-empty"}}">
|
||||
<span class="color-swatch {{if datum.className datum.className (concat "swatch-" index)}}" />
|
||||
{{datum.label}}
|
||||
|
|
Before Width: | Height: | Size: 801 B After Width: | Height: | Size: 801 B |
|
@ -0,0 +1,95 @@
|
|||
{{#if parseError}}
|
||||
<div data-test-parse-error class="notification is-danger">
|
||||
<h3 class="title is-4" data-test-parse-error-title>Parse Error</h3>
|
||||
<p data-test-parse-error-message>{{parseError}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if planError}}
|
||||
<div data-test-plan-error class="notification is-danger">
|
||||
<h3 class="title is-4" data-test-plan-error-title>Plan Error</h3>
|
||||
<p data-test-plan-error-message>{{planError}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if runError}}
|
||||
<div data-test-run-error class="notification is-danger">
|
||||
<h3 class="title is-4" data-test-run-error-title>Run Error</h3>
|
||||
<p data-test-run-error-message>{{runError}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq stage "editor")}}
|
||||
{{#if (and showEditorMessage (eq context "new"))}}
|
||||
<div class="notification is-info">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 class="title is-4" data-test-editor-help-title>Run a Job</h3>
|
||||
<p data-test-editor-help-message>Paste or author HCL or JSON to submit to your cluster. A plan will be requested before the job is submitted.</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button class="button is-info" onclick={{toggle-action "showEditorMessage" this}} data-test-editor-help-dismiss>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Job Definition
|
||||
{{#if cancelable}}
|
||||
<button class="button is-light is-compact pull-right" onclick={{action onCancel}} data-test-cancel-editing>Cancel</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{ivy-codemirror
|
||||
data-test-editor
|
||||
value=(or job._newDefinition jobSpec)
|
||||
valueUpdated=(action (mut job._newDefinition))
|
||||
options=(hash
|
||||
mode="javascript"
|
||||
theme="hashi"
|
||||
tabSize=2
|
||||
lineNumbers=true
|
||||
)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content is-associative">
|
||||
<button class="button is-primary {{if plan.isRunning "is-loading"}}" type="button" onclick={{perform plan}} disabled={{or plan.isRunning (not job._newDefinition)}} data-test-plan>Plan</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq stage "plan")}}
|
||||
{{#if showPlanMessage}}
|
||||
<div class="notification is-info">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 class="title is-4" data-test-plan-help-title>Job Plan</h3>
|
||||
<p data-test-plan-help-message>This is the impact running this job will have on your cluster.</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button class="button is-info" onclick={{toggle-action "showPlanMessage" this}} data-test-plan-help-dismiss>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">Job Plan</div>
|
||||
<div class="boxed-section-body is-dark">
|
||||
{{job-diff data-test-plan-output diff=planOutput.diff verbose=false}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section {{if planOutput.failedTGAllocs "is-warning" "is-primary"}}" data-test-dry-run-message>
|
||||
<div class="boxed-section-head" data-test-dry-run-title>Scheduler dry-run</div>
|
||||
<div class="boxed-section-body" data-test-dry-run-body>
|
||||
{{#if planOutput.failedTGAllocs}}
|
||||
{{#each planOutput.failedTGAllocs as |placementFailure|}}
|
||||
{{placement-failure failedTGAlloc=placementFailure}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
All tasks successfully allocated.
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content is-associative">
|
||||
<button class="button is-primary {{if submit.isRunning "is-loading"}}" type="button" onclick={{perform submit}} disabled={{submit.isRunning}} data-test-run>Run</button>
|
||||
<button class="button is-light" type="button" onclick={{action reset}} data-test-cancel>Cancel</button>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -39,7 +39,7 @@
|
|||
class="split-view" as |chart|}}
|
||||
<ol data-test-legend class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li class="{{datum.className}} {{if (eq datum.index chart.activeDatum.index) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<li class="{{datum.className}} {{if (eq datum.label chart.activeDatum.label) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<span class="color-swatch {{if datum.className datum.className (concat "swatch-" index)}}" />
|
||||
<span class="value" data-test-legend-value="{{datum.className}}">{{datum.value}}</span>
|
||||
<span class="label">
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
{{partial "jobs/job/subnav"}}
|
||||
<section class="section">
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{json-viewer data-test-definition-view json=model.definition}}
|
||||
{{#unless isEditing}}
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Job Definition
|
||||
<button class="button is-light is-compact pull-right" type="button" onclick={{action edit}} data-test-edit-job>Edit</button>
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{json-viewer data-test-definition-view json=definition}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{job-editor
|
||||
job=job
|
||||
cancelable=true
|
||||
context="edit"
|
||||
onCancel=(action onCancel)
|
||||
onSubmit=(action onSubmit)}}
|
||||
{{/unless}}
|
||||
</section>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
{{#allocation-status-bar allocationContainer=model.summary class="split-view" as |chart|}}
|
||||
<ol class="legend">
|
||||
{{#each chart.data as |datum index|}}
|
||||
<li class="{{datum.className}} {{if (eq datum.index chart.activeDatum.index) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<li class="{{datum.className}} {{if (eq datum.label chart.activeDatum.label) "is-active"}} {{if (eq datum.value 0) "is-empty"}}">
|
||||
<span class="color-swatch {{if datum.className datum.className (concat "swatch-" index)}}" />
|
||||
<span class="value">{{datum.value}}</span>
|
||||
<span class="label">
|
||||
|
|
|
@ -1,94 +1,6 @@
|
|||
<section class="section">
|
||||
{{#if parseError}}
|
||||
<div data-test-parse-error class="notification is-danger">
|
||||
<h3 class="title is-4" data-test-parse-error-title>Parse Error</h3>
|
||||
<p data-test-parse-error-message>{{parseError}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if planError}}
|
||||
<div data-test-plan-error class="notification is-danger">
|
||||
<h3 class="title is-4" data-test-plan-error-title>Plan Error</h3>
|
||||
<p data-test-plan-error-message>{{planError}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if runError}}
|
||||
<div data-test-run-error class="notification is-danger">
|
||||
<h3 class="title is-4" data-test-run-error-title>Run Error</h3>
|
||||
<p data-test-run-error-message>{{runError}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq stage "editor")}}
|
||||
{{#if showEditorMessage}}
|
||||
<div class="notification is-info">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 class="title is-4" data-test-editor-help-title>Run a Job</h3>
|
||||
<p data-test-editor-help-message>Paste or author HCL or JSON to submit to your cluster. A plan will be requested before the job is submitted.</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button class="button is-info" onclick={{toggle-action "showEditorMessage" this}} data-test-editor-help-dismiss>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">
|
||||
Job Definition
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{ivy-codemirror
|
||||
data-test-editor
|
||||
value=(or model._newDefinition jobSpec)
|
||||
valueUpdated=(action (mut model._newDefinition))
|
||||
options=(hash
|
||||
mode="javascript"
|
||||
theme="hashi"
|
||||
tabSize=2
|
||||
lineNumbers=true
|
||||
)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content is-associative">
|
||||
<button class="button is-primary {{if plan.isRunning "is-loading"}}" type="button" onclick={{perform plan}} disabled={{or plan.isRunning (not model._newDefinition)}} data-test-plan>Plan</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq stage "plan")}}
|
||||
{{#if showPlanMessage}}
|
||||
<div class="notification is-info">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 class="title is-4" data-test-plan-help-title>Job Plan</h3>
|
||||
<p data-test-plan-help-message>This is the impact running this job will have on your cluster.</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button class="button is-info" onclick={{toggle-action "showPlanMessage" this}} data-test-plan-help-dismiss>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="boxed-section">
|
||||
<div class="boxed-section-head">Job Plan</div>
|
||||
<div class="boxed-section-body is-dark">
|
||||
{{job-diff data-test-plan-output diff=planOutput.diff verbose=false}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section {{if planOutput.failedTGAllocs "is-warning" "is-primary"}}" data-test-dry-run-message>
|
||||
<div class="boxed-section-head" data-test-dry-run-title>Scheduler dry-run</div>
|
||||
<div class="boxed-section-body" data-test-dry-run-body>
|
||||
{{#if planOutput.failedTGAllocs}}
|
||||
{{#each planOutput.failedTGAllocs as |placementFailure|}}
|
||||
{{placement-failure failedTGAlloc=placementFailure}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
All tasks successfully allocated.
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content is-associative">
|
||||
<button class="button is-primary {{if submit.isRunning "is-loading"}}" type="button" onclick={{perform submit}} disabled={{submit.isRunning}} data-test-run>Run</button>
|
||||
<button class="button is-light" type="button" onclick={{action reset}} data-test-cancel>Cancel</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{job-editor
|
||||
job=model
|
||||
context="new"
|
||||
onSubmit=(action onSubmit)}}
|
||||
</section>
|
||||
|
|
|
@ -116,6 +116,14 @@ export default function() {
|
|||
})
|
||||
);
|
||||
|
||||
this.post('/job/:id', function(schema, req) {
|
||||
const body = JSON.parse(req.requestBody);
|
||||
|
||||
if (!body.Job) return new Response(400, {}, 'Job is a required field on the request payload');
|
||||
|
||||
return okEmpty();
|
||||
});
|
||||
|
||||
this.get(
|
||||
'/job/:id/summary',
|
||||
withBlockingSupport(function({ jobSummaries }, { params }) {
|
||||
|
|
|
@ -29,3 +29,56 @@ test('the job definition page requests the job to display in an unmutated form',
|
|||
.filter(url => url === jobURL);
|
||||
assert.ok(jobRequests.length === 2, 'Two requests for the job were made');
|
||||
});
|
||||
|
||||
test('the job definition can be edited', function(assert) {
|
||||
assert.notOk(Definition.editor.isPresent, 'Editor is not shown on load');
|
||||
|
||||
Definition.edit();
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(Definition.editor.isPresent, 'Editor is shown after clicking edit');
|
||||
assert.notOk(Definition.jsonViewer, 'Editor replaces the JSON viewer');
|
||||
});
|
||||
});
|
||||
|
||||
test('when in editing mode, the action can be canceled, showing the read-only definition again', function(assert) {
|
||||
Definition.edit();
|
||||
|
||||
andThen(() => {
|
||||
Definition.editor.cancelEditing();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(Definition.jsonViewer, 'The JSON Viewer is back');
|
||||
assert.notOk(Definition.editor.isPresent, 'The editor is gone');
|
||||
});
|
||||
});
|
||||
|
||||
test('when in editing mode, the editor is prepopulated with the job definition', function(assert) {
|
||||
const requests = server.pretender.handledRequests;
|
||||
const jobDefinition = requests.findBy('url', `/v1/job/${job.id}`).responseText;
|
||||
const formattedJobDefinition = JSON.stringify(JSON.parse(jobDefinition), null, 2);
|
||||
|
||||
Definition.edit();
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
Definition.editor.editor.contents,
|
||||
formattedJobDefinition,
|
||||
'The editor already has the job definition in it'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when changes are submitted, the site redirects to the job overview page', function(assert) {
|
||||
Definition.edit();
|
||||
|
||||
andThen(() => {
|
||||
Definition.editor.plan();
|
||||
Definition.editor.run();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(currentURL(), `/jobs/${job.id}`, 'Now on the job overview page');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,17 +35,6 @@ const jsonJob = overrides => {
|
|||
);
|
||||
};
|
||||
|
||||
const hclJob = () => `
|
||||
job "${newJobName}" {
|
||||
namespace = "default"
|
||||
datacenters = ["dc1"]
|
||||
|
||||
task "${newJobTaskGroupName}" {
|
||||
driver = "docker"
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
moduleForAcceptance('Acceptance | job run', {
|
||||
beforeEach() {
|
||||
// Required for placing allocations (a result of creating jobs)
|
||||
|
@ -61,234 +50,25 @@ test('visiting /jobs/run', function(assert) {
|
|||
});
|
||||
});
|
||||
|
||||
test('the page has an editor and an explanation popup', function(assert) {
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(JobRun.editorHelp.isPresent, 'Editor explanation popup is present');
|
||||
assert.ok(JobRun.editor.isPresent, 'Editor is present');
|
||||
});
|
||||
});
|
||||
|
||||
test('the explanation popup can be dismissed', function(assert) {
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editorHelp.dismiss();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(JobRun.editorHelp.isPresent, 'Editor explanation popup is gone');
|
||||
assert.equal(
|
||||
window.localStorage.nomadMessageJobEditor,
|
||||
'false',
|
||||
'Dismissal is persisted in localStorage'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('the explanation popup is not shown once the dismissal state is set in localStorage', function(assert) {
|
||||
window.localStorage.nomadMessageJobEditor = 'false';
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(JobRun.editorHelp.isPresent, 'Editor explanation popup is gone');
|
||||
});
|
||||
});
|
||||
|
||||
test('submitting a json job skips the parse endpoint', function(assert) {
|
||||
test('when submitting a job, the site redirects to the new job overview page', function(assert) {
|
||||
const spec = jsonJob();
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
JobRun.editor.editor.fillIn(spec);
|
||||
JobRun.editor.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
const requests = server.pretender.handledRequests.mapBy('url');
|
||||
assert.notOk(requests.includes('/v1/jobs/parse'), 'JSON job spec is not parsed');
|
||||
assert.ok(requests.includes(`/v1/job/${newJobName}/plan`), 'JSON job spec is still planned');
|
||||
JobRun.editor.run();
|
||||
});
|
||||
});
|
||||
|
||||
test('submitting an hcl job requires the parse endpoint', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
const requests = server.pretender.handledRequests.mapBy('url');
|
||||
assert.ok(requests.includes('/v1/jobs/parse'), 'HCL job spec is parsed first');
|
||||
assert.ok(requests.includes(`/v1/job/${newJobName}/plan`), 'HCL job spec is planned');
|
||||
assert.ok(
|
||||
requests.indexOf('/v1/jobs/parse') < requests.indexOf(`/v1/job/${newJobName}/plan`),
|
||||
'Parse comes before Plan'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when a job is successfully parsed and planned, the plan is shown to the user', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(JobRun.planOutput, 'The plan is outputted');
|
||||
assert.notOk(JobRun.editor.isPresent, 'The editor is replaced with the plan output');
|
||||
assert.ok(JobRun.planHelp.isPresent, 'The plan explanation popup is shown');
|
||||
});
|
||||
});
|
||||
|
||||
test('from the plan screen, the cancel button goes back to the editor with the job still in tact', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
JobRun.cancel();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(JobRun.editor.isPresent, 'The editor is shown again');
|
||||
assert.notOk(JobRun.planOutpu, 'The plan is gone');
|
||||
assert.equal(JobRun.editor.contents, spec, 'The spec that was planned is still in the editor');
|
||||
});
|
||||
});
|
||||
|
||||
test('from the plan screen, the submit button submits the job and redirects to the job overview page', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
JobRun.run();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
`/jobs/${newJobName}`,
|
||||
`Redirected to the job overview page for ${newJobName}`
|
||||
);
|
||||
|
||||
const runRequest = server.pretender.handledRequests.find(
|
||||
req => req.method === 'POST' && req.url === '/v1/jobs'
|
||||
);
|
||||
const planRequest = server.pretender.handledRequests.find(
|
||||
req => req.method === 'POST' && req.url === '/v1/jobs/parse'
|
||||
);
|
||||
|
||||
assert.ok(runRequest, 'A POST request was made to run the new job');
|
||||
assert.deepEqual(
|
||||
JSON.parse(runRequest.requestBody).Job,
|
||||
JSON.parse(planRequest.responseText),
|
||||
'The Job payload parameter is equivalent to the result of the parse request'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when parse fails, the parse error message is shown', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
const errorMessage = 'Parse Failed!! :o';
|
||||
server.pretender.post('/v1/jobs/parse', () => [400, {}, errorMessage]);
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(JobRun.planError.isPresent, 'Plan error is not shown');
|
||||
assert.notOk(JobRun.runError.isPresent, 'Run error is not shown');
|
||||
|
||||
assert.ok(JobRun.parseError.isPresent, 'Parse error is shown');
|
||||
assert.equal(
|
||||
JobRun.parseError.message,
|
||||
errorMessage,
|
||||
'The error message from the server is shown in the error in the UI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when plan fails, the plan error message is shown', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
const errorMessage = 'Parse Failed!! :o';
|
||||
server.pretender.post(`/v1/job/${newJobName}/plan`, () => [400, {}, errorMessage]);
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(JobRun.parseError.isPresent, 'Parse error is not shown');
|
||||
assert.notOk(JobRun.runError.isPresent, 'Run error is not shown');
|
||||
|
||||
assert.ok(JobRun.planError.isPresent, 'Plan error is shown');
|
||||
assert.equal(
|
||||
JobRun.planError.message,
|
||||
errorMessage,
|
||||
'The error message from the server is shown in the error in the UI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when run fails, the run error message is shown', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
const errorMessage = 'Parse Failed!! :o';
|
||||
server.pretender.post('/v1/jobs', () => [400, {}, errorMessage]);
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
JobRun.run();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.notOk(JobRun.planError.isPresent, 'Plan error is not shown');
|
||||
assert.notOk(JobRun.parseError.isPresent, 'Parse error is not shown');
|
||||
|
||||
assert.ok(JobRun.runError.isPresent, 'Run error is shown');
|
||||
assert.equal(
|
||||
JobRun.runError.message,
|
||||
errorMessage,
|
||||
'The error message from the server is shown in the error in the UI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -301,12 +81,12 @@ test('when submitting a job to a different namespace, the redirect to the job ov
|
|||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
JobRun.editor.editor.fillIn(spec);
|
||||
JobRun.editor.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
JobRun.run();
|
||||
JobRun.editor.run();
|
||||
});
|
||||
andThen(() => {
|
||||
assert.equal(
|
||||
|
@ -316,52 +96,3 @@ test('when submitting a job to a different namespace, the redirect to the job ov
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the scheduler dry-run has warnings, the warnings are shown to the user', function(assert) {
|
||||
// Unschedulable is a hint to Mirage to respond with warnings from the plan endpoint
|
||||
const spec = jsonJob({ Unschedulable: true });
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
JobRun.dryRunMessage.errored,
|
||||
'The scheduler dry-run message is in the warning state'
|
||||
);
|
||||
assert.notOk(
|
||||
JobRun.dryRunMessage.succeeded,
|
||||
'The success message is not shown in addition to the warning message'
|
||||
);
|
||||
assert.ok(
|
||||
JobRun.dryRunMessage.body.includes(newJobTaskGroupName),
|
||||
'The scheduler dry-run message includes the warning from send back by the API'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the scheduler dry-run has no warnings, a success message is shown to the user', function(assert) {
|
||||
const spec = hclJob();
|
||||
|
||||
JobRun.visit();
|
||||
|
||||
andThen(() => {
|
||||
JobRun.editor.fillIn(spec);
|
||||
JobRun.plan();
|
||||
});
|
||||
|
||||
andThen(() => {
|
||||
assert.ok(
|
||||
JobRun.dryRunMessage.succeeded,
|
||||
'The scheduler dry-run message is in the success state'
|
||||
);
|
||||
assert.notOk(
|
||||
JobRun.dryRunMessage.errored,
|
||||
'The warning message is not shown in addition to the success message'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,9 +4,9 @@ const invariant = (truthy, error) => {
|
|||
if (!truthy) throw new Error(error);
|
||||
};
|
||||
|
||||
export default function registerCodeMirrorHelpers() {
|
||||
registerHelper('getCodeMirrorInstance', function(app, selector) {
|
||||
const cmService = app.__container__.lookup('service:code-mirror');
|
||||
export function getCodeMirrorInstance(container) {
|
||||
return function(selector) {
|
||||
const cmService = container.lookup('service:code-mirror');
|
||||
|
||||
const element = document.querySelector(selector);
|
||||
invariant(element, `Selector ${selector} matched no elements`);
|
||||
|
@ -15,5 +15,12 @@ export default function registerCodeMirrorHelpers() {
|
|||
invariant(cm, `No registered CodeMirror instance for ${selector}`);
|
||||
|
||||
return cm;
|
||||
};
|
||||
}
|
||||
|
||||
export default function registerCodeMirrorHelpers() {
|
||||
registerHelper('getCodeMirrorInstance', function(app, selector) {
|
||||
const helper = getCodeMirrorInstance(app.__container__);
|
||||
return helper(selector);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,492 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { run } from '@ember/runloop';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import sinon from 'sinon';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
import { getCodeMirrorInstance } from 'nomad-ui/tests/helpers/codemirror';
|
||||
import jobEditor from 'nomad-ui/tests/pages/components/job-editor';
|
||||
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';
|
||||
|
||||
const Editor = create(jobEditor());
|
||||
|
||||
moduleForComponent('job-editor', 'Integration | Component | job-editor', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
window.localStorage.clear();
|
||||
|
||||
fragmentSerializerInitializer(getOwner(this));
|
||||
|
||||
// Normally getCodeMirrorInstance is a registered test helper,
|
||||
// but those registered test helpers only work in acceptance tests.
|
||||
window._getCodeMirrorInstance = window.getCodeMirrorInstance;
|
||||
window.getCodeMirrorInstance = getCodeMirrorInstance(getOwner(this));
|
||||
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
|
||||
// Required for placing allocations (a result of creating jobs)
|
||||
this.server.create('node');
|
||||
|
||||
Editor.setContext(this);
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
Editor.removeContext();
|
||||
window.getCodeMirrorInstance = window._getCodeMirrorInstance;
|
||||
delete window._getCodeMirrorInstance;
|
||||
},
|
||||
});
|
||||
|
||||
const newJobName = 'new-job';
|
||||
const newJobTaskGroupName = 'redis';
|
||||
const jsonJob = overrides => {
|
||||
return JSON.stringify(
|
||||
assign(
|
||||
{},
|
||||
{
|
||||
Name: newJobName,
|
||||
Namespace: 'default',
|
||||
Datacenters: ['dc1'],
|
||||
Priority: 50,
|
||||
TaskGroups: [
|
||||
{
|
||||
Name: newJobTaskGroupName,
|
||||
Tasks: [
|
||||
{
|
||||
Name: 'redis',
|
||||
Driver: 'docker',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides
|
||||
),
|
||||
null,
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
const hclJob = () => `
|
||||
job "${newJobName}" {
|
||||
namespace = "default"
|
||||
datacenters = ["dc1"]
|
||||
|
||||
task "${newJobTaskGroupName}" {
|
||||
driver = "docker"
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const commonTemplate = hbs`
|
||||
{{job-editor
|
||||
job=job
|
||||
context=context
|
||||
onSubmit=onSubmit}}
|
||||
`;
|
||||
|
||||
const cancelableTemplate = hbs`
|
||||
{{job-editor
|
||||
job=job
|
||||
context=context
|
||||
cancelable=true
|
||||
onSubmit=onSubmit
|
||||
onCancel=onCancel}}
|
||||
`;
|
||||
|
||||
const renderNewJob = (component, job) => () => {
|
||||
component.setProperties({ job, onSubmit: sinon.spy(), context: 'new' });
|
||||
component.render(commonTemplate);
|
||||
return wait();
|
||||
};
|
||||
|
||||
const renderEditJob = (component, job) => () => {
|
||||
component.setProperties({ job, onSubmit: sinon.spy(), onCancel: sinon.spy(), context: 'edit' });
|
||||
component.render(cancelableTemplate);
|
||||
};
|
||||
|
||||
const planJob = spec => () => {
|
||||
Editor.editor.fillIn(spec);
|
||||
return wait().then(() => {
|
||||
Editor.plan();
|
||||
return wait();
|
||||
});
|
||||
};
|
||||
|
||||
test('the default state is an editor with an explanation popup', function(assert) {
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(() => {
|
||||
assert.ok(Editor.editorHelp.isPresent, 'Editor explanation popup is present');
|
||||
assert.ok(Editor.editor.isPresent, 'Editor is present');
|
||||
});
|
||||
});
|
||||
|
||||
test('the explanation popup can be dismissed', function(assert) {
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(() => {
|
||||
Editor.editorHelp.dismiss();
|
||||
return wait();
|
||||
})
|
||||
.then(() => {
|
||||
assert.notOk(Editor.editorHelp.isPresent, 'Editor explanation popup is gone');
|
||||
assert.equal(
|
||||
window.localStorage.nomadMessageJobEditor,
|
||||
'false',
|
||||
'Dismissal is persisted in localStorage'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('the explanation popup is not shown once the dismissal state is set in localStorage', function(assert) {
|
||||
window.localStorage.nomadMessageJobEditor = 'false';
|
||||
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(() => {
|
||||
assert.notOk(Editor.editorHelp.isPresent, 'Editor explanation popup is gone');
|
||||
});
|
||||
});
|
||||
|
||||
test('submitting a json job skips the parse endpoint', function(assert) {
|
||||
const spec = jsonJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
const requests = this.server.pretender.handledRequests.mapBy('url');
|
||||
assert.notOk(requests.includes('/v1/jobs/parse'), 'JSON job spec is not parsed');
|
||||
assert.ok(requests.includes(`/v1/job/${newJobName}/plan`), 'JSON job spec is still planned');
|
||||
});
|
||||
});
|
||||
|
||||
test('submitting an hcl job requires the parse endpoint', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
const requests = this.server.pretender.handledRequests.mapBy('url');
|
||||
assert.ok(requests.includes('/v1/jobs/parse'), 'HCL job spec is parsed first');
|
||||
assert.ok(requests.includes(`/v1/job/${newJobName}/plan`), 'HCL job spec is planned');
|
||||
assert.ok(
|
||||
requests.indexOf('/v1/jobs/parse') < requests.indexOf(`/v1/job/${newJobName}/plan`),
|
||||
'Parse comes before Plan'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when a job is successfully parsed and planned, the plan is shown to the user', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
assert.ok(Editor.planOutput, 'The plan is outputted');
|
||||
assert.notOk(Editor.editor.isPresent, 'The editor is replaced with the plan output');
|
||||
assert.ok(Editor.planHelp.isPresent, 'The plan explanation popup is shown');
|
||||
});
|
||||
});
|
||||
|
||||
test('from the plan screen, the cancel button goes back to the editor with the job still in tact', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
Editor.cancel();
|
||||
return wait();
|
||||
})
|
||||
.then(() => {
|
||||
assert.ok(Editor.editor.isPresent, 'The editor is shown again');
|
||||
assert.equal(
|
||||
Editor.editor.contents,
|
||||
spec,
|
||||
'The spec that was planned is still in the editor'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when parse fails, the parse error message is shown', function(assert) {
|
||||
const spec = hclJob();
|
||||
const errorMessage = 'Parse Failed!! :o';
|
||||
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
this.server.pretender.post('/v1/jobs/parse', () => [400, {}, errorMessage]);
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
assert.notOk(Editor.planError.isPresent, 'Plan error is not shown');
|
||||
assert.notOk(Editor.runError.isPresent, 'Run error is not shown');
|
||||
|
||||
assert.ok(Editor.parseError.isPresent, 'Parse error is shown');
|
||||
assert.equal(
|
||||
Editor.parseError.message,
|
||||
errorMessage,
|
||||
'The error message from the server is shown in the error in the UI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when plan fails, the plan error message is shown', function(assert) {
|
||||
const spec = hclJob();
|
||||
const errorMessage = 'Plan Failed!! :o';
|
||||
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
this.server.pretender.post(`/v1/job/${newJobName}/plan`, () => [400, {}, errorMessage]);
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
assert.notOk(Editor.parseError.isPresent, 'Parse error is not shown');
|
||||
assert.notOk(Editor.runError.isPresent, 'Run error is not shown');
|
||||
|
||||
assert.ok(Editor.planError.isPresent, 'Plan error is shown');
|
||||
assert.equal(
|
||||
Editor.planError.message,
|
||||
errorMessage,
|
||||
'The error message from the server is shown in the error in the UI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when run fails, the run error message is shown', function(assert) {
|
||||
const spec = hclJob();
|
||||
const errorMessage = 'Run Failed!! :o';
|
||||
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
this.server.pretender.post('/v1/jobs', () => [400, {}, errorMessage]);
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
Editor.run();
|
||||
return wait();
|
||||
})
|
||||
.then(() => {
|
||||
assert.notOk(Editor.planError.isPresent, 'Plan error is not shown');
|
||||
assert.notOk(Editor.parseError.isPresent, 'Parse error is not shown');
|
||||
|
||||
assert.ok(Editor.runError.isPresent, 'Run error is shown');
|
||||
assert.equal(
|
||||
Editor.runError.message,
|
||||
errorMessage,
|
||||
'The error message from the server is shown in the error in the UI'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the scheduler dry-run has warnings, the warnings are shown to the user', function(assert) {
|
||||
const spec = jsonJob({ Unschedulable: true });
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
assert.ok(
|
||||
Editor.dryRunMessage.errored,
|
||||
'The scheduler dry-run message is in the warning state'
|
||||
);
|
||||
assert.notOk(
|
||||
Editor.dryRunMessage.succeeded,
|
||||
'The success message is not shown in addition to the warning message'
|
||||
);
|
||||
assert.ok(
|
||||
Editor.dryRunMessage.body.includes(newJobTaskGroupName),
|
||||
'The scheduler dry-run message includes the warning from send back by the API'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the scheduler dry-run has no warnings, a success message is shown to the user', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
assert.ok(
|
||||
Editor.dryRunMessage.succeeded,
|
||||
'The scheduler dry-run message is in the success state'
|
||||
);
|
||||
assert.notOk(
|
||||
Editor.dryRunMessage.errored,
|
||||
'The warning message is not shown in addition to the success message'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when a job is submitted in the edit context, a POST request is made to the update job endpoint', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderEditJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
Editor.run();
|
||||
})
|
||||
.then(() => {
|
||||
const requests = this.server.pretender.handledRequests
|
||||
.filterBy('method', 'POST')
|
||||
.mapBy('url');
|
||||
assert.ok(requests.includes(`/v1/job/${newJobName}`), 'A request was made to job update');
|
||||
assert.notOk(requests.includes('/v1/jobs'), 'A request was not made to job create');
|
||||
});
|
||||
});
|
||||
|
||||
test('when a job is submitted in the new context, a POST request is made to the create job endpoint', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
Editor.run();
|
||||
})
|
||||
.then(() => {
|
||||
const requests = this.server.pretender.handledRequests
|
||||
.filterBy('method', 'POST')
|
||||
.mapBy('url');
|
||||
assert.ok(requests.includes('/v1/jobs'), 'A request was made to job create');
|
||||
assert.notOk(
|
||||
requests.includes(`/v1/job/${newJobName}`),
|
||||
'A request was not made to job update'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when a job is successfully submitted, the onSubmit hook is called', function(assert) {
|
||||
const spec = hclJob();
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(planJob(spec))
|
||||
.then(() => {
|
||||
Editor.run();
|
||||
return wait();
|
||||
})
|
||||
.then(() => {
|
||||
assert.ok(
|
||||
this.get('onSubmit').calledWith(newJobName, 'default'),
|
||||
'The onSubmit hook was called with the correct arguments'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job-editor cancelable flag is false, there is no cancel button in the header', function(assert) {
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderNewJob(this, job))
|
||||
.then(() => {
|
||||
assert.notOk(Editor.cancelEditingIsAvailable, 'No way to cancel editing');
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job-editor cancelable flag is true, there is a cancel button in the header', function(assert) {
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderEditJob(this, job))
|
||||
.then(() => {
|
||||
assert.ok(Editor.cancelEditingIsAvailable, 'Cancel editing button exists');
|
||||
});
|
||||
});
|
||||
|
||||
test('when the job-editor cancel button is clicked, the onCancel hook is called', function(assert) {
|
||||
let job;
|
||||
run(() => {
|
||||
job = this.store.createRecord('job');
|
||||
});
|
||||
|
||||
return wait()
|
||||
.then(renderEditJob(this, job))
|
||||
.then(() => {
|
||||
Editor.cancelEditing();
|
||||
})
|
||||
.then(() => {
|
||||
assert.ok(this.get('onCancel').calledOnce, 'The onCancel hook was called');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
import { clickable, hasClass, isPresent, text } from 'ember-cli-page-object';
|
||||
import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror';
|
||||
|
||||
import error from 'nomad-ui/tests/pages/components/error';
|
||||
|
||||
export default () => ({
|
||||
scope: '[data-test-job-editor]',
|
||||
|
||||
isPresent: isPresent(),
|
||||
|
||||
planError: error('data-test-plan-error'),
|
||||
parseError: error('data-test-parse-error'),
|
||||
runError: error('data-test-run-error'),
|
||||
|
||||
plan: clickable('[data-test-plan]'),
|
||||
cancel: clickable('[data-test-cancel]'),
|
||||
run: clickable('[data-test-run]'),
|
||||
|
||||
cancelEditing: clickable('[data-test-cancel-editing]'),
|
||||
cancelEditingIsAvailable: isPresent('[data-test-cancel-editing]'),
|
||||
|
||||
planOutput: text('[data-test-plan-output]'),
|
||||
|
||||
planHelp: {
|
||||
isPresent: isPresent('[data-test-plan-help-title]'),
|
||||
title: text('[data-test-plan-help-title]'),
|
||||
message: text('[data-test-plan-help-message]'),
|
||||
dismiss: clickable('[data-test-plan-help-dismiss]'),
|
||||
},
|
||||
|
||||
editorHelp: {
|
||||
isPresent: isPresent('[data-test-editor-help-title]'),
|
||||
title: text('[data-test-editor-help-title]'),
|
||||
message: text('[data-test-editor-help-message]'),
|
||||
dismiss: clickable('[data-test-editor-help-dismiss]'),
|
||||
},
|
||||
|
||||
editor: {
|
||||
isPresent: isPresent('[data-test-editor]'),
|
||||
contents: code('[data-test-editor]'),
|
||||
fillIn: codeFillable('[data-test-editor]'),
|
||||
},
|
||||
|
||||
dryRunMessage: {
|
||||
scope: '[data-test-dry-run-message]',
|
||||
title: text('[data-test-dry-run-title]'),
|
||||
body: text('[data-test-dry-run-body]'),
|
||||
errored: hasClass('is-warning'),
|
||||
succeeded: hasClass('is-primary'),
|
||||
},
|
||||
});
|
|
@ -1,7 +1,12 @@
|
|||
import { create, isPresent, visitable } from 'ember-cli-page-object';
|
||||
import { create, isPresent, visitable, clickable } from 'ember-cli-page-object';
|
||||
|
||||
import jobEditor from 'nomad-ui/tests/pages/components/job-editor';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/jobs/:id/definition'),
|
||||
|
||||
jsonViewer: isPresent('[data-test-definition-view]'),
|
||||
editor: jobEditor(),
|
||||
|
||||
edit: clickable('[data-test-edit-job]'),
|
||||
});
|
||||
|
|
|
@ -1,46 +1,8 @@
|
|||
import { clickable, create, hasClass, isPresent, text, visitable } from 'ember-cli-page-object';
|
||||
import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror';
|
||||
import { create, visitable } from 'ember-cli-page-object';
|
||||
|
||||
import error from 'nomad-ui/tests/pages/components/error';
|
||||
import jobEditor from 'nomad-ui/tests/pages/components/job-editor';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/jobs/run'),
|
||||
|
||||
planError: error('data-test-plan-error'),
|
||||
parseError: error('data-test-parse-error'),
|
||||
runError: error('data-test-run-error'),
|
||||
|
||||
plan: clickable('[data-test-plan]'),
|
||||
cancel: clickable('[data-test-cancel]'),
|
||||
run: clickable('[data-test-run]'),
|
||||
|
||||
planOutput: text('[data-test-plan-output]'),
|
||||
|
||||
planHelp: {
|
||||
isPresent: isPresent('[data-test-plan-help-title]'),
|
||||
title: text('[data-test-plan-help-title]'),
|
||||
message: text('[data-test-plan-help-message]'),
|
||||
dismiss: clickable('[data-test-plan-help-dismiss]'),
|
||||
},
|
||||
|
||||
editorHelp: {
|
||||
isPresent: isPresent('[data-test-editor-help-title]'),
|
||||
title: text('[data-test-editor-help-title]'),
|
||||
message: text('[data-test-editor-help-message]'),
|
||||
dismiss: clickable('[data-test-editor-help-dismiss]'),
|
||||
},
|
||||
|
||||
editor: {
|
||||
isPresent: isPresent('[data-test-editor]'),
|
||||
contents: code('[data-test-editor]'),
|
||||
fillIn: codeFillable('[data-test-editor]'),
|
||||
},
|
||||
|
||||
dryRunMessage: {
|
||||
scope: '[data-test-dry-run-message]',
|
||||
title: text('[data-test-dry-run-title]'),
|
||||
body: text('[data-test-dry-run-body]'),
|
||||
errored: hasClass('is-warning'),
|
||||
succeeded: hasClass('is-primary'),
|
||||
},
|
||||
editor: jobEditor(),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue