Skip to content

Commit

Permalink
Merge pull request #4615 from hashicorp/f-ui-restart-stopped-job
Browse files Browse the repository at this point in the history
UI: Restart stopped job
  • Loading branch information
DingoEatingFuzz committed Aug 30, 2018
2 parents d824b70 + f4ceb22 commit 334358a
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 27 deletions.
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');
});
},
},
});
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 @@ -5,7 +5,7 @@ import { click, find } 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 { stopJob, expectStopError, expectDeleteRequest } from './helpers';
import { startJob, stopJob, expectError, expectDeleteRequest, expectStartRequest } from './helpers';
import Job from 'nomad-ui/tests/pages/jobs/detail';
import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer';

Expand Down Expand Up @@ -91,7 +91,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'
);
});
});

0 comments on commit 334358a

Please sign in to comment.