Skip to content
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: add parameterized dispatch interface #10675

Merged
merged 23 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bc7ae22
ui: add parameterized dispatch interface
nicholascioli Jun 1, 2021
01f9fed
fix failing lint test
nicholascioli Jun 1, 2021
e39b2a4
clean up dispatch and remove meta
nicholascioli Jun 8, 2021
0c8d18d
ui: encode dispatch job payload and start adding tests
lgfa29 Jul 13, 2021
c5dd0e8
ui: remove unused test imports
lgfa29 Jul 13, 2021
c8022a4
ui: redesign job dispatch form
lgfa29 Jul 13, 2021
bfcd72b
ui: initial acceptance tests for dispatch job
lgfa29 Jul 14, 2021
cc8ccce
ui: generate parameterized job children with correct id format
lgfa29 Jul 14, 2021
641316c
ui: fix job dispatch breadcrumb link
lgfa29 Jul 14, 2021
948ab34
ui: refactor job dispatch component into glimmer component and add fo…
lgfa29 Jul 14, 2021
b1a8639
ui: remove unused CSS class
lgfa29 Jul 14, 2021
fc9f9cb
ui: align job dispatch button
lgfa29 Jul 15, 2021
56ae0ff
ui: handle namespace-specific requests on job dispatch
lgfa29 Jul 15, 2021
c548237
ui: rename payloadMissing to payloadHasError
lgfa29 Jul 15, 2021
acd8fcf
ui: don't re-fetch job spec on dispatch job
lgfa29 Jul 15, 2021
b743acc
ui: keep overview tab selected on job dispatch page
lgfa29 Jul 15, 2021
006a1ff
ui: fix task and task-group linting
lgfa29 Jul 15, 2021
f474c3b
ui: URL encode job id on dispatch job tests
lgfa29 Jul 16, 2021
98f2324
ui: fix error when job meta is null
lgfa29 Jul 19, 2021
fc91dce
ui: handle job dispatch from adapter
lgfa29 Jul 19, 2021
2bf00be
ui: add more tests for dispatch job page
lgfa29 Jul 19, 2021
8959f8f
ui: add "job dispatch" capability check
lgfa29 Jul 20, 2021
3782f03
ui: update job dispatch from code review
lgfa29 Jul 20, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ui/app/abilities/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default class Job extends AbstractAbility {
@or('bypassAuthorization', 'selfTokenIsManagement')
canListAll;

@or('bypassAuthorization', 'policiesSupportDispatching')
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
canDispatch;

@computed('rulesForNamespace.@each.capabilities')
get policiesSupportRunning() {
return this.namespaceIncludesCapability('submit-job');
Expand All @@ -29,4 +32,9 @@ export default class Job extends AbstractAbility {
get policiesSupportScaling() {
return this.namespaceIncludesCapability('scale-job');
}

@computed('rulesForNamespace.@each.capabilities')
get policiesSupportDispatching() {
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
return this.namespaceIncludesCapability('dispatch-job');
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
}
}
17 changes: 17 additions & 0 deletions ui/app/adapters/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,21 @@ export default class JobAdapter extends WatchableNamespaceIDs {
},
});
}

dispatch(job, meta, payload) {
const jobId = job.get('id');
const store = this.store;
const url = addToPath(this.urlForFindRecord(jobId, 'job'), '/dispatch');

return this.ajax(url, 'POST', {
data: {
Payload: payload,
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
Meta: meta,
},
}).then(json => {
json.ID = jobId;
store.pushPayload('job-dispatch', { jobDispatches: [json] });
return store.peekRecord('job-dispatch', jobId);
});
}
}
111 changes: 111 additions & 0 deletions ui/app/components/job-dispatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { action, computed } from '@ember/object';
import { task } from 'ember-concurrency';
import classic from 'ember-classic-decorator';

import { noCase } from 'no-case';
import { titleCase } from 'title-case';

import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';

@classic
export default class JobDispatch extends Component {
@service can;
@service store;
@service config;
@service router;

model = null;
dispatchError = null;
paramValues = {};
payload = null;

@computed(
'model.definition.Meta',
'model.definition.ParameterizedJob.{MetaOptional,MetaRequired}'
)
get params() {
// Helper for mapping the params into a useable form
let mapper = (values, isRequired) =>
values.map(x => {
let emptyPlaceholder = '';
let placeholder =
this.model.definition.Meta != null ? this.model.definition.Meta[x] : emptyPlaceholder;

return {
isRequired: isRequired,
name: x,
title: titleCase(noCase(x)),

// Only show the placeholder on fields that aren't mandatory
placeholder: isRequired ? emptyPlaceholder : placeholder,
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
};
});

// Fetch the different types of parameters
let required = mapper(this.model.definition.ParameterizedJob.MetaRequired || [], true);
let optional = mapper(this.model.definition.ParameterizedJob.MetaOptional || [], false);

// Return them, required before optional
return required.concat(optional);
}

@computed('model.definition.ParameterizedJob.Payload')
get hasPayload() {
return this.model.definition.ParameterizedJob.Payload != 'forbidden';
}

@computed('model.definition.ParameterizedJob.Payload')
get isPayloadRequired() {
return this.model.definition.ParameterizedJob.Payload == 'required';
}

@action
updateParamValue(name, input) {
this.paramValues[name] = input.originalTarget.value;
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
}

@task(function*() {
// Make sure that we have all of the fields that we need
let isValid = true;
let required = this.model.definition.ParameterizedJob.MetaRequired || [];
required.forEach(required => {
let input = document.getElementById(required);
isValid &= input.checkValidity();
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
});

// Short out if we are missing fields
if (!isValid) yield;

// Try to create the dispatch
try {
const dispatch = yield this.model.job.dispatch(this.paramValues, this.payload);

// Navigate to the newly created instance
this.router.transitionTo('jobs.job', dispatch.toJSON().dispatchedJobID);
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not dispatch job';
this.set('dispatchError', error);
this.scrollToError();
}
})
submit;

@action
cancel() {
this.router.transitionTo('jobs.job');
}

reset() {
this.set('dispatchError', null);
this.set('paramValues', {});
this.set('payload', null);
}

scrollToError() {
if (!this.get('config.isTest')) {
window.scrollTo(0, 0);
}
}
}
10 changes: 10 additions & 0 deletions ui/app/models/job-dispatch.js
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;
}
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions ui/app/models/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ export default class Job extends Model {
return this.store.adapterFor('job').scale(this, group, count, message);
}

dispatch(meta, payload) {
return this.store.adapterFor('job').dispatch(this, meta, payload);
}

setIdByPayload(payload) {
const namespace = payload.Namespace || 'default';
const id = payload.Name;
Expand Down
1 change: 1 addition & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Router.map(function() {
this.route('definition');
this.route('versions');
this.route('deployments');
this.route('dispatch');
this.route('evaluations');
this.route('allocations');
});
Expand Down
7 changes: 7 additions & 0 deletions ui/app/routes/jobs/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export default class JobRoute extends Route {
this.store.findAll('namespace'),
];

// Only add the definition as needed (for parameterized jobs)
if (job.parameterized) {
relatedModelsQueries.push(
job.fetchRawDefinition().then(definition => (job.definition = definition))
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
);
}

if (this.can.can('accept recommendation')) {
relatedModelsQueries.push(job.get('recommendationSummaries'));
}
Expand Down
29 changes: 29 additions & 0 deletions ui/app/routes/jobs/job/dispatch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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.run'],
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
},
];

beforeModel() {
if (this.can.cannot('dispatch job')) {
this.transitionTo('jobs.job');
}
}

model() {
const job = this.modelFor('jobs.job');
if (!job) return this.transitionTo('jobs.job');

return job.fetchRawDefinition().then(definition => ({
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
job,
definition,
}));
}
}
4 changes: 4 additions & 0 deletions ui/app/styles/utils/bumper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
.bumper-right {
margin-right: 1.5rem;
}

.bumper-top {
margin-top: 1.5em;
}
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
60 changes: 60 additions & 0 deletions ui/app/templates/components/job-dispatch.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{{#if this.dispatchError}}
<div data-test-dispatch-error class="notification is-danger">
<h3 class="title is-4" data-test-parse-error-title>Dispatch Error</h3>
<p data-test-dispatch-error-message>{{this.dispatchError}}</p>
</div>
{{/if}}

<form action="#" onsubmit="return false">
<h1 class="title">Dispatch an instance of '{{ this.model.job.name }}'</h1>
<div class="rows">
{{#each this.params as |meta|}}
<div class="columns">
<div class="column is-4">
<h2 class="title is-4">{{meta.title}} {{#if meta.isRequired}}*{{/if}}</h2>
<input
id="{{meta.name}}"
style="width: 100%;"
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
value={{this.paramValues.[meta.name]}}
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
oninput={{action "updateParamValue" meta.name}}
placeholder={{meta.placeholder}}
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
required={{meta.isRequired}} >
<small>{{#if meta.isRequired}}Required{{else}}Optional{{/if}} Meta Param ({{ meta.name }})</small>
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
{{/each}}

<div class="boxed-section">
<div class="boxed-section-head">
Payload {{#if this.isPayloadRequired}}*{{/if}}
</div>
{{#if this.hasPayload}}
<div class="boxed-section-body is-full-bleed">
<IvyCodemirror
data-test-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-meta-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 class="bumper-top">
<button class="button is-success" type="button" onclick={{perform this.submit}}>Dispatch</button>
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
<button class="button is-white" type="button" onclick={{action "cancel"}}>Cancel</button>
</div>
</div>
</form>
21 changes: 21 additions & 0 deletions ui/app/templates/components/job-page/parameterized-child.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@

<JobPage::Parts::RecentAllocations @job={{this.job}} />

<div class="boxed-section">
<div class="boxed-section-head">
Meta
</div>
{{#if this.job.definition.Meta}}
<div class="boxed-section-body is-full-bleed">
<AttributesTable
data-test-meta
@attributePairs={{this.job.definition.Meta}}
@class="attributes-table" />
</div>
{{else}}
<div class="boxed-section-body">
<div data-test-empty-meta-message class="empty-message">
<h3 class="empty-message-headline">No Meta Attributes</h3>
<p class="empty-message-body">This job is configured with no meta attributes.</p>
</div>
</div>
{{/if}}
</div>

<div class="boxed-section">
<div class="boxed-section-head">Payload</div>
<div class="boxed-section-body is-dark">
Expand Down
3 changes: 3 additions & 0 deletions ui/app/templates/components/job-page/parts/children.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<div class="boxed-section-head">
Job Launches
{{#if (can "dispatch job")}}
<LinkTo @route="jobs.job.dispatch" data-test-run-job class="button is-primary pull-right">Dispatch Job</LinkTo>
lgfa29 marked this conversation as resolved.
Show resolved Hide resolved
{{/if}}
</div>
<div class="boxed-section-body {{if this.sortedChildren.length "is-full-bleed"}}">
{{#if this.sortedChildren}}
Expand Down
7 changes: 7 additions & 0 deletions ui/app/templates/jobs/job/dispatch.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{page-title "Dispatch new " this.model.job.name}}
<JobSubnav @job={{this.model.job}} />
<section class="section">
<JobDispatch
@model={{this.model}}
/>
</section>
4 changes: 3 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@
]
},
"dependencies": {
"lru_map": "^0.3.3"
"lru_map": "^0.3.3",
"no-case": "^3.0.4",
"title-case": "^3.0.3"
},
"resolutions": {
"ivy-codemirror/codemirror": "^5.56.0"
Expand Down
7 changes: 7 additions & 0 deletions ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15812,6 +15812,13 @@ tinycolor2@^1.4.1:
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==

title-case@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/title-case/-/title-case-3.0.3.tgz#bc689b46f02e411f1d1e1d081f7c3deca0489982"
integrity sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==
dependencies:
tslib "^2.0.3"

tmp@0.0.28:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
Expand Down