Merge pull request #4189 from hashicorp/f-ui-stop-job-button
UI: Stop job button
This commit is contained in:
commit
b8c91c90fe
|
@ -74,14 +74,24 @@ export default Watchable.extend({
|
|||
|
||||
forcePeriodic(job) {
|
||||
if (job.get('periodic')) {
|
||||
const [path, params] = this.buildURL('job', job.get('id'), job, 'findRecord').split('?');
|
||||
let url = `${path}/periodic/force`;
|
||||
|
||||
if (params) {
|
||||
url += `?${params}`;
|
||||
}
|
||||
|
||||
const url = addToPath(this.urlForFindRecord(job.get('id'), 'job'), '/periodic/force');
|
||||
return this.ajax(url, 'POST');
|
||||
}
|
||||
},
|
||||
|
||||
stop(job) {
|
||||
const url = this.urlForFindRecord(job.get('id'), 'job');
|
||||
return this.ajax(url, 'DELETE');
|
||||
},
|
||||
});
|
||||
|
||||
function addToPath(url, extension = '') {
|
||||
const [path, params] = url.split('?');
|
||||
let newUrl = `${path}${extension}`;
|
||||
|
||||
if (params) {
|
||||
newUrl += `?${params}`;
|
||||
}
|
||||
|
||||
return newUrl;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ export default Component.extend({
|
|||
gotoTaskGroup() {},
|
||||
gotoJob() {},
|
||||
|
||||
// Set to a { title, description } to surface an error
|
||||
errorMessage: null,
|
||||
|
||||
breadcrumbs: computed('job.{name,id}', function() {
|
||||
const job = this.get('job');
|
||||
return [
|
||||
|
@ -33,4 +36,13 @@ export default Component.extend({
|
|||
},
|
||||
];
|
||||
}),
|
||||
|
||||
actions: {
|
||||
clearErrorMessage() {
|
||||
this.set('errorMessage', null);
|
||||
},
|
||||
handleError(errorObject) {
|
||||
this.set('errorMessage', errorObject);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
errorMessage: null,
|
||||
onDismiss() {},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
job: null,
|
||||
title: null,
|
||||
|
||||
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.',
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -4,18 +4,21 @@ import { inject as service } from '@ember/service';
|
|||
export default AbstractJobPage.extend({
|
||||
store: service(),
|
||||
|
||||
errorMessage: '',
|
||||
errorMessage: null,
|
||||
|
||||
actions: {
|
||||
forceLaunch() {
|
||||
this.get('job')
|
||||
.forcePeriodic()
|
||||
.catch(error => {
|
||||
this.set('errorMessage', `Could not force launch: ${error}`);
|
||||
.catch(() => {
|
||||
this.set('errorMessage', {
|
||||
title: 'Could Not Force Launch',
|
||||
description: 'Your ACL token does not grant permission to submit jobs.',
|
||||
});
|
||||
});
|
||||
},
|
||||
clearErrorMessage() {
|
||||
this.set('errorMessage', '');
|
||||
this.set('errorMessage', null);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import Component from '@ember/component';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['two-step-button'],
|
||||
|
||||
idleText: '',
|
||||
cancelText: '',
|
||||
confirmText: '',
|
||||
confirmationMessage: '',
|
||||
onConfirm() {},
|
||||
onCancel() {},
|
||||
|
||||
state: 'idle',
|
||||
isIdle: equal('state', 'idle'),
|
||||
isPendingConfirmation: equal('state', 'prompt'),
|
||||
|
||||
actions: {
|
||||
setToIdle() {
|
||||
this.set('state', 'idle');
|
||||
},
|
||||
promptForConfirmation() {
|
||||
this.set('state', 'prompt');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -162,6 +162,10 @@ export default Model.extend({
|
|||
return this.store.adapterFor('job').forcePeriodic(this);
|
||||
},
|
||||
|
||||
stop() {
|
||||
return this.store.adapterFor('job').stop(this);
|
||||
},
|
||||
|
||||
statusClass: computed('status', function() {
|
||||
const classMap = {
|
||||
pending: 'is-pending',
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
@import "./components/badge";
|
||||
@import "./components/boxed-section";
|
||||
@import "./components/cli-window";
|
||||
@import "./components/ember-power-select";
|
||||
@import "./components/empty-message";
|
||||
@import "./components/error-container";
|
||||
@import "./components/gutter";
|
||||
@import "./components/inline-definitions";
|
||||
@import "./components/job-diff";
|
||||
@import "./components/json-viewer";
|
||||
@import "./components/loading-spinner";
|
||||
@import "./components/metrics";
|
||||
@import "./components/node-status-light";
|
||||
@import "./components/page-layout";
|
||||
@import "./components/simple-list";
|
||||
@import "./components/status-text";
|
||||
@import "./components/timeline";
|
||||
@import "./components/tooltip";
|
||||
@import './components/badge';
|
||||
@import './components/boxed-section';
|
||||
@import './components/cli-window';
|
||||
@import './components/ember-power-select';
|
||||
@import './components/empty-message';
|
||||
@import './components/error-container';
|
||||
@import './components/gutter';
|
||||
@import './components/inline-definitions';
|
||||
@import './components/job-diff';
|
||||
@import './components/json-viewer';
|
||||
@import './components/loading-spinner';
|
||||
@import './components/metrics';
|
||||
@import './components/node-status-light';
|
||||
@import './components/page-layout';
|
||||
@import './components/simple-list';
|
||||
@import './components/status-text';
|
||||
@import './components/timeline';
|
||||
@import './components/tooltip';
|
||||
@import './components/two-step-button';
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
.two-step-button {
|
||||
display: inline;
|
||||
position: relative;
|
||||
|
||||
.confirmation-text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -1.2em;
|
||||
font-size: $body-size;
|
||||
font-weight: $weight-normal;
|
||||
color: darken($grey-blue, 20%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
|
@ -6,10 +6,9 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{job-page/parts/title job=job handleError=(action "handleError")}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.trimmedName}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{job-page/parts/title job=job title=job.trimmedName handleError=(action "handleError")}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{#job-page/parts/title job=job handleError=(action "handleError")}}
|
||||
<span class="tag is-hollow">Parameterized</span>
|
||||
</h1>
|
||||
{{/job-page/parts/title}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{{#if errorMessage}}
|
||||
<div class="notification is-danger">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 data-test-job-error-title class="title is-4">{{errorMessage.title}}</h3>
|
||||
<p data-test-job-error-body>{{errorMessage.description}}</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button data-test-job-error-close class="button is-danger" onclick={{action onDismiss}}>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,14 @@
|
|||
<h1 class="title">
|
||||
{{or title job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
{{yield}}
|
||||
{{#if (not (eq job.status "dead"))}}
|
||||
{{two-step-button
|
||||
data-test-stop
|
||||
idleText="Stop"
|
||||
cancelText="Cancel"
|
||||
confirmText="Yes, Stop"
|
||||
confirmationMessage="Are you sure you want to stop this job?"
|
||||
onConfirm=(action "stopJob")}}
|
||||
{{/if}}
|
||||
</h1>
|
|
@ -6,10 +6,9 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.trimmedName}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{job-page/parts/title job=job title=job.trimmedName handleError=(action "handleError")}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -6,25 +6,12 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
{{#if errorMessage}}
|
||||
<div class="notification is-danger">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 data-test-force-error-title class="title is-4">Could Not Force Launch</h3>
|
||||
<p data-test-force-error-body>Your ACL token does not grant permission to submit jobs.</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button data-test-force-error-close class="button is-danger" {{action "clearErrorMessage"}}>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{#job-page/parts/title job=job title=job.trimmedName handleError=(action "handleError")}}
|
||||
<span class="tag is-hollow">periodic</span>
|
||||
<button data-test-force-launch class="button is-warning is-small is-inline" onclick={{action "forceLaunch"}}>Force Launch</button>
|
||||
</h1>
|
||||
{{/job-page/parts/title}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{job-page/parts/title job=job handleError=(action "handleError")}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
{{/each}}
|
||||
{{/global-header}}
|
||||
{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}
|
||||
<h1 class="title">
|
||||
{{job.name}}
|
||||
<span class="bumper-left tag {{job.statusClass}}" data-test-job-status>{{job.status}}</span>
|
||||
</h1>
|
||||
{{job-page/parts/error errorMessage=errorMessage onDismiss=(action "clearErrorMessage")}}
|
||||
|
||||
{{job-page/parts/title job=job handleError=(action "handleError")}}
|
||||
|
||||
<div class="boxed-section job-stats">
|
||||
<div class="boxed-section-body">
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{{#if isIdle}}
|
||||
<button data-test-idle-button type="button" class="button is-warning is-small is-inline" onclick={{action "promptForConfirmation"}}>
|
||||
{{idleText}}
|
||||
</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 is-inline" onclick={{action (queue
|
||||
(action "setToIdle")
|
||||
(action onCancel)
|
||||
)}}>
|
||||
{{cancelText}}
|
||||
</button>
|
||||
<button data-test-confirm-button class="button is-danger is-small is-inline" onclick={{action (queue
|
||||
(action "setToIdle")
|
||||
(action onConfirm)
|
||||
)}}>
|
||||
{{confirmText}}
|
||||
</button>
|
||||
{{/if}}
|
|
@ -106,6 +106,12 @@ export default function() {
|
|||
return new Response(200, {}, '{}');
|
||||
});
|
||||
|
||||
this.delete('/job/:id', function(schema, { params }) {
|
||||
const job = schema.jobs.find(params.id);
|
||||
job.update({ status: 'dead' });
|
||||
return new Response(204, {}, '');
|
||||
});
|
||||
|
||||
this.get('/deployment/:id');
|
||||
|
||||
this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { click, find } from 'ember-native-dom-helpers';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
|
||||
export function jobURL(job, path = '') {
|
||||
const id = job.get('plainId');
|
||||
const namespace = job.get('namespace.name') || 'default';
|
||||
let expectedURL = `/v1/job/${id}${path}`;
|
||||
if (namespace !== 'default') {
|
||||
expectedURL += `?namespace=${namespace}`;
|
||||
}
|
||||
return expectedURL;
|
||||
}
|
||||
|
||||
export function stopJob() {
|
||||
click('[data-test-stop] [data-test-idle-button]');
|
||||
return wait().then(() => {
|
||||
click('[data-test-stop] [data-test-confirm-button]');
|
||||
return wait();
|
||||
});
|
||||
}
|
||||
|
||||
export function expectStopError(assert) {
|
||||
return () => {
|
||||
assert.equal(
|
||||
find('[data-test-job-error-title]').textContent,
|
||||
'Could Not Stop Job',
|
||||
'Appropriate error is shown'
|
||||
);
|
||||
assert.ok(
|
||||
find('[data-test-job-error-body]').textContent.includes('ACL'),
|
||||
'The error message mentions ACLs'
|
||||
);
|
||||
|
||||
click('[data-test-job-error-close]');
|
||||
assert.notOk(find('[data-test-job-error-title]'), 'Error message is dismissable');
|
||||
return wait();
|
||||
};
|
||||
}
|
||||
|
||||
export function expectDeleteRequest(assert, server, job) {
|
||||
const expectedURL = jobURL(job);
|
||||
|
||||
assert.ok(
|
||||
server.pretender.handledRequests
|
||||
.filterBy('method', 'DELETE')
|
||||
.find(req => req.url === expectedURL),
|
||||
'DELETE URL was made correctly'
|
||||
);
|
||||
|
||||
return wait();
|
||||
}
|
|
@ -4,6 +4,7 @@ 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';
|
||||
|
||||
moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', {
|
||||
integration: true,
|
||||
|
@ -19,6 +20,23 @@ moduleForComponent('job-page/periodic', 'Integration | Component | job-page/peri
|
|||
},
|
||||
});
|
||||
|
||||
const commonTemplate = hbs`
|
||||
{{job-page/periodic
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`;
|
||||
|
||||
const commonProperties = job => ({
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob: () => {},
|
||||
});
|
||||
|
||||
test('Clicking Force Launch launches a new periodic child job', function(assert) {
|
||||
const childrenCount = 3;
|
||||
|
||||
|
@ -32,22 +50,9 @@ test('Clicking Force Launch launches a new periodic child job', function(assert)
|
|||
|
||||
return wait().then(() => {
|
||||
const job = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
this.setProperties({
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob: () => {},
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/periodic
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
this.setProperties(commonProperties(job));
|
||||
this.render(commonTemplate);
|
||||
|
||||
return wait().then(() => {
|
||||
const currentJobCount = server.db.jobs.length;
|
||||
|
@ -61,15 +66,10 @@ test('Clicking Force Launch launches a new periodic child job', function(assert)
|
|||
click('[data-test-force-launch]');
|
||||
|
||||
return wait().then(() => {
|
||||
const id = job.get('plainId');
|
||||
const namespace = job.get('namespace.name') || 'default';
|
||||
let expectedURL = `/v1/job/${id}/periodic/force`;
|
||||
if (namespace !== 'default') {
|
||||
expectedURL += `?namespace=${namespace}`;
|
||||
}
|
||||
const expectedURL = jobURL(job, '/periodic/force');
|
||||
|
||||
assert.ok(
|
||||
server.pretender.handledRequests
|
||||
this.server.pretender.handledRequests
|
||||
.filterBy('method', 'POST')
|
||||
.find(req => req.url === expectedURL),
|
||||
'POST URL was correct'
|
||||
|
@ -82,55 +82,90 @@ test('Clicking Force Launch launches a new periodic child job', function(assert)
|
|||
});
|
||||
|
||||
test('Clicking force launch without proper permissions shows an error message', function(assert) {
|
||||
server.pretender.post('/v1/job/:id/periodic/force', () => [403, {}, null]);
|
||||
this.server.pretender.post('/v1/job/:id/periodic/force', () => [403, {}, null]);
|
||||
|
||||
this.server.create('job', 'periodic', {
|
||||
id: 'parent',
|
||||
childrenCount: 1,
|
||||
createAllocations: false,
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
this.store.findAll('job');
|
||||
|
||||
return wait().then(() => {
|
||||
const job = this.store.peekAll('job').findBy('plainId', 'parent');
|
||||
this.setProperties({
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob: () => {},
|
||||
});
|
||||
|
||||
this.render(hbs`
|
||||
{{job-page/periodic
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`);
|
||||
this.setProperties(commonProperties(job));
|
||||
this.render(commonTemplate);
|
||||
|
||||
return wait().then(() => {
|
||||
assert.notOk(find('[data-test-force-error-title]'), 'No error message yet');
|
||||
assert.notOk(find('[data-test-job-error-title]'), 'No error message yet');
|
||||
|
||||
click('[data-test-force-launch]');
|
||||
|
||||
return wait().then(() => {
|
||||
assert.equal(
|
||||
find('[data-test-force-error-title]').textContent,
|
||||
find('[data-test-job-error-title]').textContent,
|
||||
'Could Not Force Launch',
|
||||
'Appropriate error is shown'
|
||||
);
|
||||
assert.ok(
|
||||
find('[data-test-force-error-body]').textContent.includes('ACL'),
|
||||
find('[data-test-job-error-body]').textContent.includes('ACL'),
|
||||
'The error message mentions ACLs'
|
||||
);
|
||||
|
||||
click('[data-test-force-error-close]');
|
||||
click('[data-test-job-error-close]');
|
||||
|
||||
assert.notOk(find('[data-test-force-error-title]'), 'Error message is dismissable');
|
||||
assert.notOk(find('[data-test-job-error-title]'), 'Error message is dismissable');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('Stopping a job sends a delete request for the job', function(assert) {
|
||||
const mirageJob = this.server.create('job', 'periodic', {
|
||||
childrenCount: 0,
|
||||
createAllocations: false,
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
let job;
|
||||
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(stopJob)
|
||||
.then(() => expectDeleteRequest(assert, this.server, job));
|
||||
});
|
||||
|
||||
test('Stopping a job without proper permissions shows an error message', function(assert) {
|
||||
this.server.pretender.delete('/v1/job/:id', () => [403, {}, null]);
|
||||
|
||||
const mirageJob = this.server.create('job', 'periodic', {
|
||||
childrenCount: 0,
|
||||
createAllocations: false,
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
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(stopJob)
|
||||
.then(expectStopError(assert));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import { getOwner } from '@ember/application';
|
||||
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';
|
||||
|
||||
moduleForComponent('job-page/service', 'Integration | Component | job-page/service', {
|
||||
integration: true,
|
||||
beforeEach() {
|
||||
window.localStorage.clear();
|
||||
this.store = getOwner(this).lookup('service:store');
|
||||
this.server = startMirage();
|
||||
this.server.create('namespace');
|
||||
},
|
||||
afterEach() {
|
||||
this.server.shutdown();
|
||||
window.localStorage.clear();
|
||||
},
|
||||
});
|
||||
|
||||
const commonTemplate = hbs`
|
||||
{{job-page/service
|
||||
job=job
|
||||
sortProperty=sortProperty
|
||||
sortDescending=sortDescending
|
||||
currentPage=currentPage
|
||||
gotoJob=gotoJob}}
|
||||
`;
|
||||
|
||||
const commonProperties = job => ({
|
||||
job,
|
||||
sortProperty: 'name',
|
||||
sortDescending: true,
|
||||
currentPage: 1,
|
||||
gotoJob() {},
|
||||
});
|
||||
|
||||
const makeMirageJob = server =>
|
||||
server.create('job', {
|
||||
type: 'service',
|
||||
createAllocations: false,
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
test('Stopping a job sends a delete request for the job', function(assert) {
|
||||
let job;
|
||||
|
||||
const mirageJob = makeMirageJob(this.server);
|
||||
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(stopJob)
|
||||
.then(() => expectDeleteRequest(assert, this.server, job));
|
||||
});
|
||||
|
||||
test('Stopping a job without proper permissions shows an error message', function(assert) {
|
||||
this.server.pretender.delete('/v1/job/:id', () => [403, {}, null]);
|
||||
|
||||
const mirageJob = makeMirageJob(this.server);
|
||||
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(stopJob)
|
||||
.then(expectStopError(assert));
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
import { find, click } from 'ember-native-dom-helpers';
|
||||
import { test, moduleForComponent } from 'ember-qunit';
|
||||
import wait from 'ember-test-helpers/wait';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
|
||||
moduleForComponent('two-step-button', 'Integration | Component | two step button', {
|
||||
integration: true,
|
||||
});
|
||||
|
||||
const commonProperties = () => ({
|
||||
idleText: 'Idle State Button',
|
||||
cancelText: 'Cancel Action',
|
||||
confirmText: 'Confirm Action',
|
||||
confirmationMessage: 'Are you certain',
|
||||
onConfirm: sinon.spy(),
|
||||
onCancel: sinon.spy(),
|
||||
});
|
||||
|
||||
const commonTemplate = hbs`
|
||||
{{two-step-button
|
||||
idleText=idleText
|
||||
cancelText=cancelText
|
||||
confirmText=confirmText
|
||||
confirmationMessage=confirmationMessage
|
||||
onConfirm=onConfirm
|
||||
onCancel=onCancel}}
|
||||
`;
|
||||
|
||||
test('presents as a button in the idle state', function(assert) {
|
||||
const props = commonProperties();
|
||||
this.setProperties(props);
|
||||
this.render(commonTemplate);
|
||||
|
||||
assert.ok(find('[data-test-idle-button]'), 'Idle button is rendered');
|
||||
assert.equal(
|
||||
find('[data-test-idle-button]').textContent.trim(),
|
||||
props.idleText,
|
||||
'Button is labeled correctly'
|
||||
);
|
||||
|
||||
assert.notOk(find('[data-test-cancel-button]'), 'No cancel button yet');
|
||||
assert.notOk(find('[data-test-confirm-button]'), 'No confirm button yet');
|
||||
assert.notOk(find('[data-test-confirmation-message]'), 'No confirmation message yet');
|
||||
});
|
||||
|
||||
test('clicking the idle state button transitions into the promptForConfirmation state', function(assert) {
|
||||
const props = commonProperties();
|
||||
this.setProperties(props);
|
||||
this.render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(find('[data-test-cancel-button]'), 'Cancel button is rendered');
|
||||
assert.equal(
|
||||
find('[data-test-cancel-button]').textContent.trim(),
|
||||
props.cancelText,
|
||||
'Button is labeled correctly'
|
||||
);
|
||||
|
||||
assert.ok(find('[data-test-confirm-button]'), 'Confirm button is rendered');
|
||||
assert.equal(
|
||||
find('[data-test-confirm-button]').textContent.trim(),
|
||||
props.confirmText,
|
||||
'Button is labeled correctly'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-confirmation-message]').textContent.trim(),
|
||||
props.confirmationMessage,
|
||||
'Confirmation message is shown'
|
||||
);
|
||||
|
||||
assert.notOk(find('[data-test-idle-button]'), 'No more idle button');
|
||||
});
|
||||
});
|
||||
|
||||
test('canceling in the promptForConfirmation state calls the onCancel hook and resets to the idle state', function(assert) {
|
||||
const props = commonProperties();
|
||||
this.setProperties(props);
|
||||
this.render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
|
||||
return wait().then(() => {
|
||||
click('[data-test-cancel-button]');
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(props.onCancel.calledOnce, 'The onCancel hook fired');
|
||||
assert.ok(find('[data-test-idle-button]'), 'Idle button is back');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('confirming the promptForConfirmation state calls the onConfirm hook and resets to the idle state', function(assert) {
|
||||
const props = commonProperties();
|
||||
this.setProperties(props);
|
||||
this.render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
|
||||
return wait().then(() => {
|
||||
click('[data-test-confirm-button]');
|
||||
|
||||
return wait().then(() => {
|
||||
assert.ok(props.onConfirm.calledOnce, 'The onConfirm hook fired');
|
||||
assert.ok(find('[data-test-idle-button]'), 'Idle button is back');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue