-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI: Run a job from the Web UI #4592
Changes from 18 commits
751b6e2
53f2ca3
33956a6
302401d
da1e179
27f4a59
21da150
f212887
f29f435
a970741
09e6432
4b12c06
3b8b894
875ba99
3b5d96b
c6fa757
635411f
a5d6790
da8a6e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import Controller from '@ember/controller'; | ||
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({ | ||
parseError: null, | ||
planError: null, | ||
runError: null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for separating these out? Would you ever get a parse error that could then proceed to a plan? |
||
|
||
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 { | ||
const planOutput = yield this.get('model').plan(); | ||
this.set('planOutput', planOutput.Diff); | ||
} 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); | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,54 @@ 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); | ||
}, | ||
|
||
run() { | ||
assert('A job must be parsed before ran', this.get('_newDefinitionJSON')); | ||
return this.store.adapterFor('job').run(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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha, we do this in Vault too. |
||
// 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) { | ||
const namespace = payload.Namespace || 'default'; | ||
const id = payload.Name; | ||
|
||
this.set('plainId', id); | ||
this.set('id', JSON.stringify([id, namespace])); | ||
|
||
const namespaceRecord = this.store.peekRecord('namespace', namespace); | ||
if (namespaceRecord) { | ||
this.set('namespace', namespaceRecord); | ||
} | ||
}, | ||
|
||
statusClass: computed('status', function() { | ||
const classMap = { | ||
pending: 'is-pending', | ||
|
@@ -206,4 +256,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'), | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ const Router = EmberRouter.extend({ | |
|
||
Router.map(function() { | ||
this.route('jobs', function() { | ||
this.route('run'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🏃♂️ |
||
this.route('job', { path: '/:job_name' }, function() { | ||
this.route('task-group', { path: '/:name' }); | ||
this.route('definition'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import Route from '@ember/routing/route'; | ||
import { inject as service } from '@ember/service'; | ||
|
||
export default Route.extend({ | ||
store: service(), | ||
system: service(), | ||
|
||
breadcrumbs: [ | ||
{ | ||
label: 'Run', | ||
args: ['jobs.run'], | ||
}, | ||
], | ||
|
||
model() { | ||
return this.get('store').createRecord('job', { | ||
namespace: this.get('system.activeNamespace'), | ||
}); | ||
}, | ||
|
||
resetController(controller, isExiting) { | ||
if (isExiting) { | ||
controller.get('model').deleteRecord(); | ||
} | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,35 @@ | ||
// Utils | ||
@import "./utils/reset.scss"; | ||
@import "./utils/z-indices"; | ||
@import "./utils/product-colors"; | ||
@import "./utils/bumper"; | ||
@import './utils/reset.scss'; | ||
@import './utils/z-indices'; | ||
@import './utils/product-colors'; | ||
@import './utils/bumper'; | ||
@import './utils/layout'; | ||
|
||
// Start with Bulma variables as a foundation | ||
@import "bulma/sass/utilities/initial-variables"; | ||
@import 'bulma/sass/utilities/initial-variables'; | ||
|
||
// Override variables where appropriate | ||
@import "./core/variables.scss"; | ||
@import './core/variables.scss'; | ||
|
||
// Bring in the rest of Bulma | ||
@import "bulma/bulma"; | ||
@import 'bulma/bulma'; | ||
|
||
// Override Bulma details where appropriate | ||
@import "./core/buttons"; | ||
@import "./core/breadcrumb"; | ||
@import "./core/columns"; | ||
@import "./core/forms"; | ||
@import "./core/icon"; | ||
@import "./core/level"; | ||
@import "./core/menu"; | ||
@import "./core/message"; | ||
@import "./core/navbar"; | ||
@import "./core/notification"; | ||
@import "./core/pagination"; | ||
@import "./core/progress"; | ||
@import "./core/section"; | ||
@import "./core/table"; | ||
@import "./core/tabs"; | ||
@import "./core/tag"; | ||
@import "./core/title"; | ||
@import "./core/typography"; | ||
@import './core/buttons'; | ||
@import './core/breadcrumb'; | ||
@import './core/columns'; | ||
@import './core/forms'; | ||
@import './core/icon'; | ||
@import './core/level'; | ||
@import './core/menu'; | ||
@import './core/message'; | ||
@import './core/navbar'; | ||
@import './core/notification'; | ||
@import './core/pagination'; | ||
@import './core/progress'; | ||
@import './core/section'; | ||
@import './core/table'; | ||
@import './core/tabs'; | ||
@import './core/tag'; | ||
@import './core/title'; | ||
@import './core/typography'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💥Quotepocalypse - did prettier just not do this file before? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apparently not. It doesn't get touched very often. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.is-associative { | ||
margin-top: -0.75em; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So nice the API does this 💅