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: Job Writes #4600

Merged
merged 54 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
751b6e2
Enforce a min-height for the code editor component
DingoEatingFuzz Aug 14, 2018
53f2ca3
New layout helper for associating two elements vertically
DingoEatingFuzz Aug 14, 2018
33956a6
New job run page and navigation to get there.
DingoEatingFuzz Aug 14, 2018
302401d
Add breadcrumb to the run job page
DingoEatingFuzz Aug 14, 2018
da1e179
Parse and Plan API and UI workflows
DingoEatingFuzz Aug 15, 2018
27f4a59
Fix no allocations error message layout for the recent allocations co…
DingoEatingFuzz Aug 15, 2018
21da150
Remove unused solarized theme configuration
DingoEatingFuzz Aug 15, 2018
f212887
Run job UI and API workflows
DingoEatingFuzz Aug 15, 2018
f29f435
Error messages for job submit
DingoEatingFuzz Aug 15, 2018
a970741
Move the Diff property read out of the template
DingoEatingFuzz Aug 15, 2018
09e6432
Support parse, plan, and run endpoints in mirage
DingoEatingFuzz Aug 15, 2018
4b12c06
Use the job name as the job id
DingoEatingFuzz Aug 16, 2018
3b8b894
Acceptance test for the jobs list page
DingoEatingFuzz Aug 16, 2018
875ba99
New test helper for getting the underlying CodeMirror instance from a…
DingoEatingFuzz Aug 16, 2018
3b5d96b
New PageObject helper for getting and setting CodeMirror values
DingoEatingFuzz Aug 16, 2018
c6fa757
New Page Object component for common error formatting
DingoEatingFuzz Aug 16, 2018
635411f
Rework job parse mirage request to get the job ID out of the payload
DingoEatingFuzz Aug 17, 2018
a5d6790
Acceptance tests for job run page
DingoEatingFuzz Aug 17, 2018
da8a6e4
Spiff up the form buttons with type and disabled attributes
DingoEatingFuzz Aug 18, 2018
e329ab7
Merge pull request #4592 from hashicorp/f-ui-new-job
DingoEatingFuzz Aug 18, 2018
1c94fdb
Don't use the verbose diff for job run plan
DingoEatingFuzz Aug 17, 2018
9b7b465
Show the scheduler dry-run output on the plan page
DingoEatingFuzz Aug 17, 2018
223dae2
Fixed bug that prevented non verbose job diffs from printing changed …
DingoEatingFuzz Aug 18, 2018
2d0805c
Test coverage for scheduler dry-run addition to the plan page
DingoEatingFuzz Aug 20, 2018
d64491b
New job edit page
DingoEatingFuzz Aug 21, 2018
a2c12b9
Move the bulk of the new job page into a new job editor component
DingoEatingFuzz Aug 21, 2018
e9190f4
Fix a blocking queries bug
DingoEatingFuzz Aug 21, 2018
8031a0d
Fix multiple highlight bug in the distribution-bar component
DingoEatingFuzz Aug 21, 2018
1e4ca09
Use the same urlForFindRecord logic for urlForUpdateRecord
DingoEatingFuzz Aug 21, 2018
522f6d7
Support job update in the adapter
DingoEatingFuzz Aug 21, 2018
cdc37ec
Support different contexts for the job editor
DingoEatingFuzz Aug 21, 2018
91f8c15
fixup-adapter
DingoEatingFuzz Aug 21, 2018
d36270b
fixup-job-editor
DingoEatingFuzz Aug 21, 2018
69d28de
Handle update job in the model
DingoEatingFuzz Aug 21, 2018
b68ba61
Fix bug where scrolling wasn't using the document
DingoEatingFuzz Aug 21, 2018
b478d71
Support cancellation of editing in the job-editor
DingoEatingFuzz Aug 21, 2018
684f45c
Introduce job editing to the job definition page
DingoEatingFuzz Aug 21, 2018
4ae15be
Since registerHelper doesn't work in integration tests a new way is n…
DingoEatingFuzz Aug 23, 2018
6463efe
Test coverage for the job-editor component
DingoEatingFuzz Aug 23, 2018
c1d44fb
Rewrite the job run acceptance tests to be about routing
DingoEatingFuzz Aug 23, 2018
e8db71a
Acceptance tests for the edit behaviors on the job definition page
DingoEatingFuzz Aug 23, 2018
a0cdc96
Merge pull request #4603 from hashicorp/f-ui-new-job-improved-plan
DingoEatingFuzz Aug 23, 2018
85aaa53
Simplify the data control flow around job.plan()
DingoEatingFuzz Aug 23, 2018
78b9d32
Support the promote deployment api action
DingoEatingFuzz Aug 24, 2018
c510060
Change the latest deployment component to include a Promote Canary bu…
DingoEatingFuzz Aug 24, 2018
916cc52
Merge pull request #4612 from hashicorp/f-ui-job-edit
DingoEatingFuzz Aug 28, 2018
619376c
Add Start Job action on the job overview page for when a job is dead
DingoEatingFuzz Aug 24, 2018
1b481a3
Test coverage for the Start Job behavior
DingoEatingFuzz Aug 24, 2018
f09e9a4
Switch stop/run job actions to EC tasks
DingoEatingFuzz Aug 24, 2018
f79f037
Add a confirmation loading state to the two-step-button component
DingoEatingFuzz Aug 24, 2018
f4ceb22
Fix the flickering issue with start/stop job
DingoEatingFuzz Aug 24, 2018
c96c99a
Test coverage for the promote canary feature
DingoEatingFuzz Aug 24, 2018
d824b70
Merge pull request #4616 from hashicorp/f-ui-promote-canary
DingoEatingFuzz Aug 30, 2018
334358a
Merge pull request #4615 from hashicorp/f-ui-restart-stopped-job
DingoEatingFuzz Aug 30, 2018
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
51 changes: 27 additions & 24 deletions ui/app/adapters/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,32 +73,35 @@ export default RESTAdapter.extend({
//
// This is the original implementation of _buildURL
// without the pluralization of modelName
urlForFindRecord(id, modelName) {
let path;
let url = [];
let host = get(this, 'host');
let prefix = this.urlPrefix();

if (modelName) {
path = modelName.camelize();
if (path) {
url.push(path);
}
}
urlForFindRecord: urlForRecord,
urlForUpdateRecord: urlForRecord,
});

if (id) {
url.push(encodeURIComponent(id));
}
function urlForRecord(id, modelName) {
let path;
let url = [];
let host = get(this, 'host');
let prefix = this.urlPrefix();

if (prefix) {
url.unshift(prefix);
if (modelName) {
path = modelName.camelize();
if (path) {
url.push(path);
}
}

url = url.join('/');
if (!host && url && url.charAt(0) !== '/') {
url = '/' + url;
}
if (id) {
url.push(encodeURIComponent(id));
}

return url;
},
});
if (prefix) {
url.unshift(prefix);
}

url = url.join('/');
if (!host && url && url.charAt(0) !== '/') {
url = '/' + url;
}

return url;
}
29 changes: 29 additions & 0 deletions ui/app/adapters/deployment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Watchable from './watchable';

export default Watchable.extend({
promote(deployment) {
const id = deployment.get('id');
const url = urlForAction(this.urlForFindRecord(id, 'deployment'), '/promote');
return this.ajax(url, 'POST', {
data: {
DeploymentId: id,
All: true,
},
});
},
});

// The deployment action API endpoints all end with the ID
// /deployment/:action/:deployment_id instead of /deployment/:deployment_id/:action
function urlForAction(url, extension = '') {
const [path, params] = url.split('?');
const pathParts = path.split('/');
const idPart = pathParts.pop();
let newUrl = `${pathParts.join('/')}${extension}/${idPart}`;

if (params) {
newUrl += `?${params}`;
}

return newUrl;
}
51 changes: 51 additions & 0 deletions ui/app/adapters/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export default Watchable.extend({
return associateNamespace(url, namespace);
},

urlForUpdateRecord(id, type, hash) {
const [name, namespace] = JSON.parse(id);
let url = this._super(name, type, hash);
return associateNamespace(url, namespace);
},

xhrKey(url, method, options = {}) {
const plainKey = this._super(...arguments);
const namespace = options.data && options.data.namespace;
Expand All @@ -59,6 +65,51 @@ 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 jobId = job.get('id');
const store = this.get('store');
const url = addToPath(this.urlForFindRecord(jobId, 'job'), '/plan');

return this.ajax(url, 'POST', {
data: {
Job: job.get('_newDefinitionJSON'),
Diff: true,
},
}).then(json => {
json.ID = jobId;
store.pushPayload('job-plan', { jobPlans: [json] });
return store.peekRecord('job-plan', jobId);
});
},

// Running a job doesn't follow REST create semantics so it's easier to
// treat it as an action.
run(job) {
return this.ajax(this.urlForCreateRecord('job'), 'POST', {
data: {
Job: job.get('_newDefinitionJSON'),
},
});
},

update(job) {
return this.ajax(this.urlForUpdateRecord(job.get('id'), 'job'), 'POST', {
data: {
Job: job.get('_newDefinitionJSON'),
},
});
},
});

function associateNamespace(url, namespace) {
Expand Down
102 changes: 102 additions & 0 deletions ui/app/components/job-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Component from '@ember/component';
import { assert } from '@ember/debug';
import { inject as service } from '@ember/service';
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 Component.extend({
store: service(),
config: service(),

'data-test-job-editor': true,

job: null,
onSubmit() {},
context: computed({
get() {
return this.get('_context');
},
set(key, value) {
const allowedValues = ['new', 'edit'];

assert(`context must be one of: ${allowedValues.join(', ')}`, allowedValues.includes(value));

this.set('_context', value);
return value;
},
}),

_context: null,
parseError: null,
planError: null,
runError: null,

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('job').parse();
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not parse input';
this.set('parseError', error);
this.scrollToError();
return;
}

try {
const plan = yield this.get('job').plan();
this.set('planOutput', plan);
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not plan job';
this.set('planError', error);
this.scrollToError();
}
}).drop(),

submit: task(function*() {
try {
if (this.get('context') === 'new') {
yield this.get('job').run();
} else {
yield this.get('job').update();
}

const id = this.get('job.plainId');
const namespace = this.get('job.namespace.name') || 'default';

this.reset();

// Treat the job as ephemeral and only provide ID parts.
this.get('onSubmit')(id, namespace);
} catch (err) {
const error = messageFromAdapterError(err) || 'Could not submit job';
this.set('runError', error);
this.set('planOutput', null);
this.scrollToError();
}
}),

reset() {
this.set('planOutput', null);
this.set('planError', null);
this.set('parseError', null);
this.set('runError', null);
},

scrollToError() {
if (!this.get('config.isTest')) {
window.scrollTo(0, 0);
}
},
});
19 changes: 19 additions & 0 deletions ui/app/components/job-page/parts/latest-deployment.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';

export default Component.extend({
job: null,
tagName: '',

handleError() {},

isShowingDeploymentDetails: false,

promote: task(function*() {
try {
yield this.get('job.latestDeployment.content').promote();
} catch (err) {
let message = messageFromAdapterError(err);
if (!message || message === 'Forbidden') {
message = 'Your ACL token does not grant permission to promote deployments.';
}
this.get('handleError')({
title: 'Could Not Promote Deployment',
description: message,
});
}
}),
});
52 changes: 40 additions & 12 deletions ui/app/components/job-page/parts/title.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';

export default Component.extend({
tagName: '',
Expand All @@ -8,16 +10,42 @@ export default Component.extend({

handleError() {},

actions: {
stopJob() {
this.get('job')
.stop()
.catch(() => {
this.get('handleError')({
title: 'Could Not Stop Job',
description: 'Your ACL token does not grant permission to stop jobs.',
});
});
},
},
stopJob: task(function*() {
try {
const job = this.get('job');
yield job.stop();
// Eagerly update the job status to avoid flickering
this.job.set('status', 'dead');
} catch (err) {
this.get('handleError')({
title: 'Could Not Stop Job',
description: 'Your ACL token does not grant permission to stop jobs.',
});
}
}),

startJob: task(function*() {
const job = this.get('job');
const definition = yield job.fetchRawDefinition();

delete definition.Stop;
job.set('_newDefinition', JSON.stringify(definition));

try {
yield job.parse();
yield job.update();
// Eagerly update the job status to avoid flickering
job.set('status', 'running');
} catch (err) {
let message = messageFromAdapterError(err);
if (!message || message === 'Forbidden') {
message = 'Your ACL token does not grant permission to stop jobs.';
}

this.get('handleError')({
title: 'Could Not Start Job',
description: message,
});
}
}),
});
10 changes: 10 additions & 0 deletions ui/app/components/placement-failure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Component from '@ember/component';
import { or } from '@ember/object/computed';

export default Component.extend({
// Either provide a taskGroup or a failedTGAlloc
taskGroup: null,
failedTGAlloc: null,

placementFailures: or('taskGroup.placementFailures', 'failedTGAlloc'),
});
7 changes: 7 additions & 0 deletions ui/app/components/two-step-button.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Component from '@ember/component';
import { equal } from '@ember/object/computed';
import RSVP from 'rsvp';

export default Component.extend({
classNames: ['two-step-button'],
Expand All @@ -8,6 +9,7 @@ export default Component.extend({
cancelText: '',
confirmText: '',
confirmationMessage: '',
awaitingConfirmation: false,
onConfirm() {},
onCancel() {},

Expand All @@ -22,5 +24,10 @@ export default Component.extend({
promptForConfirmation() {
this.set('state', 'prompt');
},
confirm() {
RSVP.resolve(this.get('onConfirm')()).then(() => {
this.send('setToIdle');
});
},
},
});
Loading