Commit da1e1797 authored by Michael Lange's avatar Michael Lange
Browse files

Parse and Plan API and UI workflows

parent 302401d4
Showing with 167 additions and 25 deletions
+167 -25
......@@ -59,6 +59,26 @@ export default Watchable.extend({
const url = this.urlForFindRecord(job.get('id'), 'job');
return this.ajax(url, 'DELETE');
},
parse(spec) {
const url = addToPath(this.urlForFindAll('job'), '/parse');
return this.ajax(url, 'POST', {
data: {
JobHCL: spec,
Canonicalize: true,
},
});
},
plan(job) {
const url = addToPath(this.urlForFindRecord(job.get('id'), 'job'), '/plan');
return this.ajax(url, 'POST', {
data: {
Job: job.get('_newDefinitionJSON'),
Diff: true,
},
});
},
});
function associateNamespace(url, namespace) {
......
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { task } from 'ember-concurrency';
export default Controller.extend({
stage: computed('planOutput', function() {
return this.get('planOutput') ? 'plan' : 'editor';
}),
plan: task(function*() {
this.cancel();
try {
yield this.get('model').parse();
} catch (err) {
this.set('parseError', err);
}
try {
const planOutput = yield this.get('model').plan();
console.log('Heyo!', planOutput);
this.set('planOutput', planOutput);
} catch (err) {
this.set('planError', err);
console.log('Uhoh', err);
}
}).drop(),
submit: task(function*() {}),
cancel() {
this.set('planOutput', null);
this.set('planError', null);
this.set('parseError', null);
},
});
......@@ -4,6 +4,8 @@ import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo, hasMany } from 'ember-data/relationships';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import RSVP from 'rsvp';
import { assert } from '@ember/debug';
const JOB_TYPES = ['service', 'batch', 'system'];
......@@ -191,6 +193,41 @@ export default Model.extend({
return this.store.adapterFor('job').stop(this);
},
plan() {
assert('A job must be parsed before planned', this.get('_newDefinitionJSON'));
return this.store.adapterFor('job').plan(this);
},
parse() {
const definition = this.get('_newDefinition');
let promise;
try {
// If the definition is already JSON then it doesn't need to be parsed.
const json = JSON.parse(definition);
this.set('_newDefinitionJSON', definition);
this.setIDByPayload(json);
promise = RSVP.Resolve(definition);
} catch (err) {
// If the definition is invalid JSON, assume it is HCL. If it is invalid
// in anyway, the parse endpoint will throw an error.
promise = this.store
.adapterFor('job')
.parse(this.get('_newDefinition'))
.then(response => {
this.set('_newDefinitionJSON', response);
this.setIDByPayload(response);
});
}
return promise;
},
setIDByPayload(payload) {
this.set('plainId', payload.Name);
this.set('id', JSON.stringify([payload.Name, payload.Namespace || 'default']));
},
statusClass: computed('status', function() {
const classMap = {
pending: 'is-pending',
......@@ -206,4 +243,13 @@ export default Model.extend({
// Lazily decode the base64 encoded payload
return window.atob(this.get('payload') || '');
}),
// An arbitrary HCL or JSON string that is used by the serializer to plan
// and run this job. Used for both new job models and saved job models.
_newDefinition: attr('string'),
// The new definition may be HCL, in which case the API will need to parse the
// spec first. In order to preserve both the original HCL and the parsed response
// that will be submitted to the create job endpoint, another prop is necessary.
_newDefinitionJSON: attr('string'),
});
......@@ -3,6 +3,7 @@ import { inject as service } from '@ember/service';
export default Route.extend({
store: service(),
system: service(),
breadcrumbs: [
{
......@@ -10,4 +11,16 @@ export default Route.extend({
args: ['jobs.run'],
},
],
model() {
return this.get('store').createRecord('job', {
namespace: this.get('system.activeNamespace'),
});
},
resetController(controller, isExiting) {
if (isExiting) {
controller.get('model').deleteRecord();
}
},
});
......@@ -43,7 +43,7 @@ $dark-bright: lighten($dark, 15%);
}
span.cm-comment {
color: $grey-light;
color: $grey;
}
span.cm-string,
......
<section class="section">
<div class="notification is-info">
<div class="columns">
<div class="column">
<h3 class="title is-4">Run a Job</h3>
<p>Paste or author HCL or JSON to submit to your cluster. A plan will be requested before the job is submitted.</p>
{{#if (eq stage "editor")}}
<div class="notification is-info">
<div class="columns">
<div class="column">
<h3 class="title is-4">Run a Job</h3>
<p>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">Okay</button>
</div>
</div>
<div class="column is-centered is-minimum">
<button class="button is-info">Okay</button>
</div>
<div class="boxed-section">
<div class="boxed-section-head">
Job Definition
</div>
<div class="boxed-section-body is-full-bleed">
{{ivy-codemirror
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"}}" onclick={{perform plan}}>Plan</button>
</div>
{{/if}}
{{#if (eq stage "plan")}}
<div class="notification is-info">
<div class="columns">
<div class="column">
<h3 class="title is-4">Job Plan</h3>
<p>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">Okay</button>
</div>
</div>
</div>
</div>
<div class="boxed-section">
<div class="boxed-section-head">
Job Definition
<div class="boxed-section">
<div class="boxed-section-head">Job Plan</div>
<div class="boxed-section-body is-dark">
{{job-diff diff=planOutput.Diff}}
</div>
</div>
<div class="boxed-section-body is-full-bleed">
{{ivy-codemirror
value=jobSpec
options=(hash
mode="javascript"
theme="hashi"
tabSize=2
lineNumbers=true
)}}
<div class="content is-associative">
<button class="button is-primary {{if submit.isRunning "is-loading"}}" onclick={{perform submit}}>Submit</button>
<button class="button is-light" onclick={{action cancel}}>Cancel</button>
</div>
</div>
<div class="content is-associative">
<button class="button is-primary">Plan</button>
</div>
{{/if}}
</section>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment