-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ui: add parameterized dispatch interface (#10675)
* ui: add parameterized dispatch interface This commit adds a new interface for dispatching parameteried jobs, if the user has the right permissions. The UI can be accessed by viewing a parameterized job and clicking on the "Dispatch Job" button located in the "Job Launches" section. * fix failing lint test * clean up dispatch and remove meta This commit cleans up a few things that had typos and inconsistent naming. In line with this, the custom `meta` view was removed in favor of using the included `AttributesTable`. * ui: encode dispatch job payload and start adding tests * ui: remove unused test imports * ui: redesign job dispatch form * ui: initial acceptance tests for dispatch job * ui: generate parameterized job children with correct id format * ui: fix job dispatch breadcrumb link * ui: refactor job dispatch component into glimmer component and add form validation * ui: remove unused CSS class * ui: align job dispatch button * ui: handle namespace-specific requests on job dispatch * ui: rename payloadMissing to payloadHasError * ui: don't re-fetch job spec on dispatch job * ui: keep overview tab selected on job dispatch page * ui: fix task and task-group linting * ui: URL encode job id on dispatch job tests * ui: fix error when job meta is null * ui: handle job dispatch from adapter * ui: add more tests for dispatch job page * ui: add "job dispatch" capability check * ui: update job dispatch from code review Co-authored-by: Luiz Aoqui <[email protected]>
- Loading branch information
1 parent
3165ae8
commit 801a732
Showing
29 changed files
with
981 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,9 @@ export default class Job extends AbstractAbility { | |
@or('bypassAuthorization', 'selfTokenIsManagement') | ||
canListAll; | ||
|
||
@or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportDispatching') | ||
canDispatch; | ||
|
||
@computed('[email protected]') | ||
get policiesSupportRunning() { | ||
return this.namespaceIncludesCapability('submit-job'); | ||
|
@@ -29,4 +32,9 @@ export default class Job extends AbstractAbility { | |
get policiesSupportScaling() { | ||
return this.namespaceIncludesCapability('scale-job'); | ||
} | ||
|
||
@computed('[email protected]') | ||
get policiesSupportDispatching() { | ||
return this.namespaceIncludesCapability('dispatch-job'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { inject as service } from '@ember/service'; | ||
import { action } from '@ember/object'; | ||
import { A } from '@ember/array'; | ||
import { task } from 'ember-concurrency'; | ||
import { noCase } from 'no-case'; | ||
import { titleCase } from 'title-case'; | ||
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; | ||
|
||
class MetaField { | ||
@tracked value; | ||
@tracked error; | ||
|
||
name; | ||
required; | ||
title; | ||
|
||
constructor(meta) { | ||
this.name = meta.name; | ||
this.required = meta.required; | ||
this.title = meta.title; | ||
this.value = meta.value; | ||
this.error = meta.error; | ||
} | ||
|
||
validate() { | ||
this.error = ''; | ||
|
||
if (this.required && !this.value) { | ||
this.error = `Missing required meta parameter "${this.name}".`; | ||
} | ||
} | ||
} | ||
|
||
export default class JobDispatch extends Component { | ||
@service router; | ||
@service config; | ||
|
||
@tracked metaFields = []; | ||
@tracked payload = ''; | ||
@tracked payloadHasError = false; | ||
|
||
errors = A([]); | ||
|
||
constructor() { | ||
super(...arguments); | ||
|
||
// Helper for mapping the params into a useable form. | ||
const mapper = (values = [], required) => | ||
values.map( | ||
x => | ||
new MetaField({ | ||
name: x, | ||
required, | ||
title: titleCase(noCase(x)), | ||
value: this.args.job.meta ? this.args.job.meta[x] : '', | ||
}) | ||
); | ||
|
||
// Fetch the different types of parameters. | ||
const required = mapper(this.args.job.parameterizedDetails.MetaRequired, true); | ||
const optional = mapper(this.args.job.parameterizedDetails.MetaOptional, false); | ||
|
||
// Merge them, required before optional. | ||
this.metaFields = required.concat(optional); | ||
} | ||
|
||
get hasPayload() { | ||
return this.args.job.parameterizedDetails.Payload !== 'forbidden'; | ||
} | ||
|
||
get payloadRequired() { | ||
return this.args.job.parameterizedDetails.Payload === 'required'; | ||
} | ||
|
||
@action | ||
dispatch() { | ||
this.validateForm(); | ||
if (this.errors.length > 0) { | ||
this.scrollToError(); | ||
return; | ||
} | ||
|
||
this.onDispatched.perform(); | ||
} | ||
|
||
@action | ||
cancel() { | ||
this.router.transitionTo('jobs.job'); | ||
} | ||
|
||
@task({ drop: true }) *onDispatched() { | ||
// Try to create the dispatch. | ||
try { | ||
let paramValues = {}; | ||
this.metaFields.forEach(m => (paramValues[m.name] = m.value)); | ||
const dispatch = yield this.args.job.dispatch(paramValues, this.payload); | ||
|
||
// Navigate to the newly created instance. | ||
this.router.transitionTo('jobs.job', dispatch.DispatchedJobID); | ||
} catch (err) { | ||
const error = messageFromAdapterError(err) || 'Could not dispatch job'; | ||
this.errors.pushObject(error); | ||
this.scrollToError(); | ||
} | ||
} | ||
|
||
scrollToError() { | ||
if (!this.config.isTest) { | ||
window.scrollTo(0, 0); | ||
} | ||
} | ||
|
||
resetErrors() { | ||
this.payloadHasError = false; | ||
this.errors.clear(); | ||
} | ||
|
||
validateForm() { | ||
this.resetErrors(); | ||
|
||
// Make sure that we have all of the meta fields that we need. | ||
this.metaFields.forEach(f => { | ||
f.validate(); | ||
if (f.error) { | ||
this.errors.pushObject(f.error); | ||
} | ||
}); | ||
|
||
// Validate payload. | ||
if (this.payloadRequired && !this.payload) { | ||
this.errors.pushObject('Missing required payload.'); | ||
this.payloadHasError = true; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import Model from '@ember-data/model'; | ||
import { attr } from '@ember-data/model'; | ||
|
||
export default class JobDispatch extends Model { | ||
@attr() index; | ||
@attr() jobCreateIndex; | ||
@attr() evalCreateIndex; | ||
@attr() evalID; | ||
@attr() dispatchedJobID; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,16 @@ export default class TaskGroup extends Fragment { | |
|
||
@fragment('group-scaling') scaling; | ||
|
||
@attr() meta; | ||
|
||
@computed('job.meta', 'meta') | ||
get mergedMeta() { | ||
return { | ||
...this.job.meta, | ||
...this.meta, | ||
}; | ||
} | ||
|
||
@computed('[email protected]') | ||
get drivers() { | ||
return this.tasks.mapBy('driver').uniq(); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Route from '@ember/routing/route'; | ||
import { inject as service } from '@ember/service'; | ||
|
||
export default class DispatchRoute extends Route { | ||
@service can; | ||
|
||
breadcrumbs = [ | ||
{ | ||
label: 'Dispatch', | ||
args: ['jobs.job.dispatch'], | ||
}, | ||
]; | ||
|
||
beforeModel() { | ||
const job = this.modelFor('jobs.job'); | ||
const namespace = job.namespace.get('name'); | ||
if (this.can.cannot('dispatch job', null, { namespace })) { | ||
this.transitionTo('jobs.job'); | ||
} | ||
} | ||
|
||
model() { | ||
const job = this.modelFor('jobs.job'); | ||
if (!job) return this.transitionTo('jobs.job'); | ||
return job; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
{{#if this.errors}} | ||
<div data-test-dispatch-error class="notification is-danger"> | ||
<h3 class="title is-4" data-test-parse-error-title>Dispatch Error</h3> | ||
<ul> | ||
{{#each this.errors as |error|}} | ||
<li>{{error}}</li> | ||
{{/each}} | ||
</ul> | ||
</div> | ||
{{/if}} | ||
|
||
<form action="#" onsubmit="return false"> | ||
<h1 class="title">Dispatch an instance of '{{@job.name}}'</h1> | ||
|
||
{{#each this.metaFields as |meta|}} | ||
<div class="columns"> | ||
<div class="column is-6"> | ||
<div data-test-meta-field="{{ if meta.required "required" "optional" }}" class="field"> | ||
<label data-test-meta-field-label class="label {{if meta.error "has-text-danger"}}" for="{{meta.name}}"> | ||
{{meta.title}} {{#if meta.required}}*{{/if}} | ||
</label> | ||
<div class="control"> | ||
<input | ||
data-test-meta-field-input | ||
id="{{meta.name}}" | ||
class="input {{if meta.error "is-danger"}}" | ||
type="text" | ||
value={{meta.value}} | ||
oninput={{action (mut meta.value) value="target.value"}} | ||
required={{meta.required}} > | ||
|
||
<p class="help {{if meta.error "has-text-danger"}}"> | ||
{{#if meta.required}}Required{{else}}Optional{{/if}} | ||
Meta Param | ||
<span class="badge is-light is-faded"> | ||
<code>{{ meta.name }}</code> | ||
</span> | ||
</p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
{{/each}} | ||
|
||
<div class="boxed-section {{if this.payloadHasError "is-danger"}}"> | ||
<div data-test-payload-head class="boxed-section-head"> | ||
Payload {{#if this.payloadRequired}}*{{/if}} | ||
</div> | ||
{{#if this.hasPayload}} | ||
<div class="boxed-section-body is-full-bleed"> | ||
<IvyCodemirror | ||
data-test-payload-editor | ||
aria-label="Payload definition" | ||
@valueUpdated={{action (mut this.payload)}} | ||
@options={{hash | ||
mode="javascript" | ||
theme="hashi" | ||
screenReaderLabel="Payload definition editor" | ||
tabSize=2 | ||
lineNumbers=true | ||
}} /> | ||
</div> | ||
{{else}} | ||
<div class="boxed-section-body"> | ||
<div data-test-empty-payload-message class="empty-message"> | ||
<h3 class="empty-message-headline">Payload Disabled</h3> | ||
<p class="empty-message-body">Payload is disabled for this job.</p> | ||
</div> | ||
</div> | ||
{{/if}} | ||
</div> | ||
|
||
<div> | ||
<button data-test-dispatch-button class="button is-primary" type="button" onclick={{action "dispatch"}}>Dispatch</button> | ||
<button data-test-cancel-button class="button is-white" type="button" onclick={{action "cancel"}}>Cancel</button> | ||
</div> | ||
</form> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.