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: Restart stopped job #4615

Merged
merged 5 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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,
});
}
}),
});
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');
});
},
DingoEatingFuzz marked this conversation as resolved.
Show resolved Hide resolved
},
});
18 changes: 18 additions & 0 deletions ui/app/templates/components/freestyle/sg-two-step-button.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,21 @@
</h1>
</div>
{{/freestyle-usage}}

{{#freestyle-usage "two-step-button-loading" title="Two Step Button Loading State"}}
<div class="mock-spacing">
<h1 class="title">
This is a page title
{{two-step-button
idleText="Scary Action"
cancelText="Nvm"
confirmText="Yep"
confirmationMessage="Wait, really? Like...seriously?"
awaitingConfirmation=true
state="prompt"}}
</h1>
</div>
{{/freestyle-usage}}
{{#freestyle-annotation}}
<strong>Note:</strong> the <code>state</code> property is internal state and only used here to bypass the idle state for demonstration purposes.
{{/freestyle-annotation}}
12 changes: 11 additions & 1 deletion ui/app/templates/components/job-page/parts/title.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
cancelText="Cancel"
confirmText="Yes, Stop"
confirmationMessage="Are you sure you want to stop this job?"
onConfirm=(action "stopJob")}}
awaitingConfirmation=stopJob.isRunning
onConfirm=(perform stopJob)}}
{{else}}
{{two-step-button
data-test-start
idleText="Start"
cancelText="Cancel"
confirmText="Yes, Start"
confirmationMessage="Are you sure you want to start this job?"
awaitingConfirmation=startJob.isRunning
onConfirm=(perform startJob)}}
{{/if}}
</h1>
22 changes: 14 additions & 8 deletions ui/app/templates/components/two-step-button.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@
</button>
{{else if isPendingConfirmation}}
<span data-test-confirmation-message class="confirmation-text">{{confirmationMessage}}</span>
<button data-test-cancel-button type="button" class="button is-dark is-outlined is-small" onclick={{action (queue
(action "setToIdle")
(action onCancel)
)}}>
<button
data-test-cancel-button
type="button"
class="button is-dark is-outlined is-small"
disabled={{awaitingConfirmation}}
onclick={{action (queue
(action "setToIdle")
(action onCancel)
)}}>
{{cancelText}}
</button>
<button data-test-confirm-button class="button is-danger is-small" onclick={{action (queue
(action "setToIdle")
(action onConfirm)
)}}>
<button
data-test-confirm-button
class="button is-danger is-small {{if awaitingConfirmation "is-loading"}}"
disabled={{awaitingConfirmation}}
onclick={{action "confirm"}}>
{{confirmText}}
</button>
{{/if}}
24 changes: 22 additions & 2 deletions ui/tests/integration/job-page/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,31 @@ export function stopJob() {
});
}

export function expectStopError(assert) {
export function startJob() {
click('[data-test-start] [data-test-idle-button]');
return wait().then(() => {
click('[data-test-start] [data-test-confirm-button]');
return wait();
});
}

export function expectStartRequest(assert, server, job) {
const expectedURL = jobURL(job);
const request = server.pretender.handledRequests
.filterBy('method', 'POST')
.find(req => req.url === expectedURL);

const requestPayload = JSON.parse(request.requestBody).Job;

assert.ok(request, 'POST URL was made correctly');
assert.ok(requestPayload.Stop == null, 'The Stop signal is not sent in the POST request');
}

export function expectError(assert, title) {
return () => {
assert.equal(
find('[data-test-job-error-title]').textContent,
'Could Not Stop Job',
title,
'Appropriate error is shown'
);
assert.ok(
Expand Down
57 changes: 55 additions & 2 deletions ui/tests/integration/job-page/periodic-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import { click, find, findAll } from 'ember-native-dom-helpers';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import { jobURL, stopJob, expectStopError, expectDeleteRequest } from './helpers';
import {
jobURL,
stopJob,
startJob,
expectError,
expectDeleteRequest,
expectStartRequest,
} from './helpers';

moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', {
integration: true,
Expand Down Expand Up @@ -167,5 +174,51 @@ test('Stopping a job without proper permissions shows an error message', functio
return wait();
})
.then(stopJob)
.then(expectStopError(assert));
.then(expectError(assert, 'Could Not Stop Job'));
});

test('Starting a job sends a post request for the job using the current definition', function(assert) {
let job;

const mirageJob = this.server.create('job', 'periodic', {
childrenCount: 0,
createAllocations: false,
status: 'dead',
});
this.store.findAll('job');

return wait()
.then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);

this.setProperties(commonProperties(job));
this.render(commonTemplate);

return wait();
})
.then(startJob)
.then(() => expectStartRequest(assert, this.server, job));
});

test('Starting a job without proper permissions shows an error message', function(assert) {
this.server.pretender.post('/v1/job/:id', () => [403, {}, null]);

const mirageJob = this.server.create('job', 'periodic', {
childrenCount: 0,
createAllocations: false,
status: 'dead',
});
this.store.findAll('job');

return wait()
.then(() => {
const job = this.store.peekAll('job').findBy('plainId', mirageJob.id);

this.setProperties(commonProperties(job));
this.render(commonTemplate);

return wait();
})
.then(startJob)
.then(expectError(assert, 'Could Not Start Job'));
});
42 changes: 40 additions & 2 deletions ui/tests/integration/job-page/service-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { test, moduleForComponent } from 'ember-qunit';
import wait from 'ember-test-helpers/wait';
import hbs from 'htmlbars-inline-precompile';
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
import { stopJob, expectStopError, expectDeleteRequest } from './helpers';
import { startJob, stopJob, expectError, expectDeleteRequest, expectStartRequest } from './helpers';
import Job from 'nomad-ui/tests/pages/jobs/detail';

moduleForComponent('job-page/service', 'Integration | Component | job-page/service', {
Expand Down Expand Up @@ -88,7 +88,45 @@ test('Stopping a job without proper permissions shows an error message', functio
return wait();
})
.then(stopJob)
.then(expectStopError(assert));
.then(expectError(assert, 'Could Not Stop Job'));
});

test('Starting a job sends a post request for the job using the current definition', function(assert) {
let job;

const mirageJob = makeMirageJob(this.server, { status: 'dead' });
this.store.findAll('job');

return wait()
.then(() => {
job = this.store.peekAll('job').findBy('plainId', mirageJob.id);

this.setProperties(commonProperties(job));
this.render(commonTemplate);

return wait();
})
.then(startJob)
.then(() => expectStartRequest(assert, this.server, job));
});

test('Starting a job without proper permissions shows an error message', function(assert) {
this.server.pretender.post('/v1/job/:id', () => [403, {}, null]);

const mirageJob = makeMirageJob(this.server, { status: 'dead' });
this.store.findAll('job');

return wait()
.then(() => {
const job = this.store.peekAll('job').findBy('plainId', mirageJob.id);

this.setProperties(commonProperties(job));
this.render(commonTemplate);

return wait();
})
.then(startJob)
.then(expectError(assert, 'Could Not Start Job'));
});

test('Recent allocations shows allocations in the job context', function(assert) {
Expand Down
26 changes: 26 additions & 0 deletions ui/tests/integration/two-step-button-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const commonProperties = () => ({
cancelText: 'Cancel Action',
confirmText: 'Confirm Action',
confirmationMessage: 'Are you certain',
awaitingConfirmation: false,
onConfirm: sinon.spy(),
onCancel: sinon.spy(),
});
Expand All @@ -23,6 +24,7 @@ const commonTemplate = hbs`
cancelText=cancelText
confirmText=confirmText
confirmationMessage=confirmationMessage
awaitingConfirmation=awaitingConfirmation
onConfirm=onConfirm
onCancel=onCancel}}
`;
Expand Down Expand Up @@ -109,3 +111,27 @@ test('confirming the promptForConfirmation state calls the onConfirm hook and re
});
});
});

test('when awaitingConfirmation is true, the cancel and submit buttons are disabled and the submit button is loading', function(assert) {
const props = commonProperties();
props.awaitingConfirmation = true;
this.setProperties(props);
this.render(commonTemplate);

click('[data-test-idle-button]');

return wait().then(() => {
assert.ok(
find('[data-test-cancel-button]').hasAttribute('disabled'),
'The cancel button is disabled'
);
assert.ok(
find('[data-test-confirm-button]').hasAttribute('disabled'),
'The confirm button is disabled'
);
assert.ok(
find('[data-test-confirm-button]').classList.contains('is-loading'),
'The confirm button is in a loading state'
);
});
});