diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index e13047815..af8b05aaf 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -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) { diff --git a/ui/app/controllers/jobs/run.js b/ui/app/controllers/jobs/run.js new file mode 100644 index 000000000..916a0c8e2 --- /dev/null +++ b/ui/app/controllers/jobs/run.js @@ -0,0 +1,36 @@ +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); + }, +}); diff --git a/ui/app/models/job.js b/ui/app/models/job.js index b69040eaa..5de6977ae 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -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'), }); diff --git a/ui/app/routes/jobs/run.js b/ui/app/routes/jobs/run.js index ac6c654b1..121e9b4ce 100644 --- a/ui/app/routes/jobs/run.js +++ b/ui/app/routes/jobs/run.js @@ -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(); + } + }, }); diff --git a/ui/app/styles/components/codemirror.scss b/ui/app/styles/components/codemirror.scss index 16b54f23b..81c08356e 100644 --- a/ui/app/styles/components/codemirror.scss +++ b/ui/app/styles/components/codemirror.scss @@ -43,7 +43,7 @@ $dark-bright: lighten($dark, 15%); } span.cm-comment { - color: $grey-light; + color: $grey; } span.cm-string, diff --git a/ui/app/templates/jobs/run.hbs b/ui/app/templates/jobs/run.hbs index d142248aa..953ea855f 100644 --- a/ui/app/templates/jobs/run.hbs +++ b/ui/app/templates/jobs/run.hbs @@ -1,31 +1,58 @@
-
-
-
-

Run a Job

-

Paste or author HCL or JSON to submit to your cluster. A plan will be requested before the job is submitted.

-
-
- + {{#if (eq stage "editor")}} +
+
+
+

Run a Job

+

Paste or author HCL or JSON to submit to your cluster. A plan will be requested before the job is submitted.

+
+
+ +
-
-
-
- Job Definition +
+
+ Job Definition +
+
+ {{ivy-codemirror + value=(or model._newDefinition jobSpec) + valueUpdated=(action (mut model._newDefinition)) + options=(hash + mode="javascript" + theme="hashi" + tabSize=2 + lineNumbers=true + )}} +
-
- {{ivy-codemirror - value=jobSpec - options=(hash - mode="javascript" - theme="hashi" - tabSize=2 - lineNumbers=true - )}} +
+
-
-
- -
+ {{/if}} + + {{#if (eq stage "plan")}} +
+
+
+

Job Plan

+

This is the impact running this job will have on your cluster.

+
+
+ +
+
+
+
+
Job Plan
+
+ {{job-diff diff=planOutput.Diff}} +
+
+
+ + +
+ {{/if}}