Merge pull request #5734 from hashicorp/f-ui/allocation-lifecycle
UI: Allocation lifecycle
This commit is contained in:
commit
dff3abb630
|
@ -1,3 +1,21 @@
|
|||
import Watchable from './watchable';
|
||||
import addToPath from 'nomad-ui/utils/add-to-path';
|
||||
|
||||
export default Watchable.extend();
|
||||
export default Watchable.extend({
|
||||
stop: adapterAction('/stop'),
|
||||
|
||||
restart(allocation, taskName) {
|
||||
const prefix = `${this.host || '/'}${this.urlPrefix()}`;
|
||||
const url = `${prefix}/client/allocation/${allocation.id}/restart`;
|
||||
return this.ajax(url, 'PUT', {
|
||||
data: taskName && { TaskName: taskName },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function adapterAction(path, verb = 'POST') {
|
||||
return function(allocation) {
|
||||
const url = addToPath(this.urlForFindRecord(allocation.id, 'allocation'), path);
|
||||
return this.ajax(url, verb);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Watchable from './watchable';
|
||||
import addToPath from 'nomad-ui/utils/add-to-path';
|
||||
|
||||
export default Watchable.extend({
|
||||
system: service(),
|
||||
|
@ -118,14 +119,3 @@ function associateNamespace(url, namespace) {
|
|||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function addToPath(url, extension = '') {
|
||||
const [path, params] = url.split('?');
|
||||
let newUrl = `${path}${extension}`;
|
||||
|
||||
if (params) {
|
||||
newUrl += `?${params}`;
|
||||
}
|
||||
|
||||
return newUrl;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import Component from '@ember/component';
|
||||
import { next } from '@ember/runloop';
|
||||
import { equal } from '@ember/object/computed';
|
||||
import { task, waitForEvent } from 'ember-concurrency';
|
||||
import RSVP from 'rsvp';
|
||||
|
||||
export default Component.extend({
|
||||
|
@ -10,6 +12,7 @@ export default Component.extend({
|
|||
confirmText: '',
|
||||
confirmationMessage: '',
|
||||
awaitingConfirmation: false,
|
||||
disabled: false,
|
||||
onConfirm() {},
|
||||
onCancel() {},
|
||||
|
||||
|
@ -17,12 +20,25 @@ export default Component.extend({
|
|||
isIdle: equal('state', 'idle'),
|
||||
isPendingConfirmation: equal('state', 'prompt'),
|
||||
|
||||
cancelOnClickOutside: task(function*() {
|
||||
while (true) {
|
||||
let ev = yield waitForEvent(document.body, 'click');
|
||||
if (!this.element.contains(ev.target) && !this.awaitingConfirmation) {
|
||||
this.send('setToIdle');
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setToIdle() {
|
||||
this.set('state', 'idle');
|
||||
this.cancelOnClickOutside.cancelAll();
|
||||
},
|
||||
promptForConfirmation() {
|
||||
this.set('state', 'prompt');
|
||||
next(() => {
|
||||
this.cancelOnClickOutside.perform();
|
||||
});
|
||||
},
|
||||
confirm() {
|
||||
RSVP.resolve(this.onConfirm()).then(() => {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed, observer } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { task } from 'ember-concurrency';
|
||||
import Sortable from 'nomad-ui/mixins/sortable';
|
||||
import { lazyClick } from 'nomad-ui/helpers/lazy-click';
|
||||
import { watchRecord } from 'nomad-ui/utils/properties/watch';
|
||||
|
||||
export default Controller.extend(Sortable, {
|
||||
token: service(),
|
||||
|
@ -21,6 +24,50 @@ export default Controller.extend(Sortable, {
|
|||
// Set in the route
|
||||
preempter: null,
|
||||
|
||||
error: computed(() => {
|
||||
// { title, description }
|
||||
return null;
|
||||
}),
|
||||
|
||||
onDismiss() {
|
||||
this.set('error', null);
|
||||
},
|
||||
|
||||
watchNext: watchRecord('allocation'),
|
||||
|
||||
observeWatchNext: observer('model.nextAllocation.clientStatus', function() {
|
||||
const nextAllocation = this.model.nextAllocation;
|
||||
if (nextAllocation && nextAllocation.content) {
|
||||
this.watchNext.perform(nextAllocation);
|
||||
} else {
|
||||
this.watchNext.cancelAll();
|
||||
}
|
||||
}),
|
||||
|
||||
stopAllocation: task(function*() {
|
||||
try {
|
||||
yield this.model.stop();
|
||||
// Eagerly update the allocation clientStatus to avoid flickering
|
||||
this.model.set('clientStatus', 'complete');
|
||||
} catch (err) {
|
||||
this.set('error', {
|
||||
title: 'Could Not Stop Allocation',
|
||||
description: 'Your ACL token does not grant allocation lifecycle permissions.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
restartAllocation: task(function*() {
|
||||
try {
|
||||
yield this.model.restart();
|
||||
} catch (err) {
|
||||
this.set('error', {
|
||||
title: 'Could Not Restart Allocation',
|
||||
description: 'Your ACL token does not grant allocation lifecycle permissions.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
gotoTask(allocation, task) {
|
||||
this.transitionToRoute('allocations.allocation.task', task);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import { alias } from '@ember/object/computed';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend({
|
||||
network: alias('model.resources.networks.firstObject'),
|
||||
|
@ -20,4 +21,24 @@ export default Controller.extend({
|
|||
)
|
||||
.sortBy('name');
|
||||
}),
|
||||
|
||||
error: computed(() => {
|
||||
// { title, description }
|
||||
return null;
|
||||
}),
|
||||
|
||||
onDismiss() {
|
||||
this.set('error', null);
|
||||
},
|
||||
|
||||
restartTask: task(function*() {
|
||||
try {
|
||||
yield this.model.restart();
|
||||
} catch (err) {
|
||||
this.set('error', {
|
||||
title: 'Could Not Restart Task',
|
||||
description: 'Your ACL token does not grant allocation lifecycle permissions.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -31,8 +31,11 @@ export default Mixin.create(WithVisibilityDetection, {
|
|||
},
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
willTransition(transition) {
|
||||
// Don't cancel watchers if transitioning into a sub-route
|
||||
if (!transition.intent.name || !transition.intent.name.startsWith(this.routeName)) {
|
||||
this.cancelAllWatchers();
|
||||
}
|
||||
|
||||
// Bubble the action up to the application route
|
||||
return true;
|
||||
|
|
|
@ -99,4 +99,12 @@ export default Model.extend({
|
|||
);
|
||||
}
|
||||
),
|
||||
|
||||
stop() {
|
||||
return this.store.adapterFor('allocation').stop(this);
|
||||
},
|
||||
|
||||
restart(taskName) {
|
||||
return this.store.adapterFor('allocation').restart(this, taskName);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -43,4 +43,8 @@ export default Fragment.extend({
|
|||
|
||||
return classMap[this.state] || 'is-dark';
|
||||
}),
|
||||
|
||||
restart() {
|
||||
return this.allocation.restart(this.name);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,4 +10,10 @@ export default Route.extend({
|
|||
|
||||
return this._super(...arguments);
|
||||
},
|
||||
|
||||
resetController(controller, isExiting) {
|
||||
if (isExiting) {
|
||||
controller.watchNext.cancelAll();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,4 +4,8 @@
|
|||
&.is-6 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&.with-headroom {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,42 @@
|
|||
<section class="section">
|
||||
<h1 data-test-title class="title">
|
||||
{{#if error}}
|
||||
<div data-test-inline-error class="notification is-danger">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 data-test-inline-error-title class="title is-4">{{error.title}}</h3>
|
||||
<p data-test-inline-error-body>{{error.description}}</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button data-test-inline-error-close class="button is-danger" onclick={{action onDismiss}}>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<h1 data-test-title class="title with-headroom">
|
||||
Allocation {{model.name}}
|
||||
<span class="bumper-left tag {{model.statusClass}}">{{model.clientStatus}}</span>
|
||||
<span class="tag is-hollow is-small no-text-transform">{{model.id}}</span>
|
||||
{{#if model.isRunning}}
|
||||
{{two-step-button
|
||||
data-test-stop
|
||||
idleText="Stop"
|
||||
cancelText="Cancel"
|
||||
confirmText="Yes, Stop"
|
||||
confirmationMessage="Are you sure? This will reschedule the allocation on a different client."
|
||||
awaitingConfirmation=stopAllocation.isRunning
|
||||
disabled=(or stopAllocation.isRunning restartAllocation.isRunning)
|
||||
onConfirm=(perform stopAllocation)}}
|
||||
{{two-step-button
|
||||
data-test-restart
|
||||
idleText="Restart"
|
||||
cancelText="Cancel"
|
||||
confirmText="Yes, Restart"
|
||||
confirmationMessage="Are you sure? This will restart the allocation in-place."
|
||||
awaitingConfirmation=restartAllocation.isRunning
|
||||
disabled=(or stopAllocation.isRunning restartAllocation.isRunning)
|
||||
onConfirm=(perform restartAllocation)}}
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section is-small">
|
||||
|
|
|
@ -1,8 +1,33 @@
|
|||
{{partial "allocations/allocation/task/subnav"}}
|
||||
<section class="section">
|
||||
{{#if error}}
|
||||
<div data-test-inline-error class="notification is-danger">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 data-test-inline-error-title class="title is-4">{{error.title}}</h3>
|
||||
<p data-test-inline-error-body>{{error.description}}</p>
|
||||
</div>
|
||||
<div class="column is-centered is-minimum">
|
||||
<button data-test-inline-error-close class="button is-danger" onclick={{action onDismiss}}>Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<h1 class="title" data-test-title>
|
||||
{{model.name}}
|
||||
<span class="bumper-left tag {{model.stateClass}}" data-test-state>{{model.state}}</span>
|
||||
{{#if model.isRunning}}
|
||||
{{two-step-button
|
||||
data-test-restart
|
||||
idleText="Restart"
|
||||
cancelText="Cancel"
|
||||
confirmText="Yes, Restart"
|
||||
confirmationMessage="Are you sure? This will restart the task in-place."
|
||||
awaitingConfirmation=restartTask.isRunning
|
||||
disabled=restartTask.isRunning
|
||||
onConfirm=(perform restartTask)}}
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
<div class="boxed-section is-small">
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
{{#if isIdle}}
|
||||
<button data-test-idle-button type="button" class="button is-danger is-outlined is-important is-small" onclick={{action "promptForConfirmation"}}>
|
||||
<button
|
||||
data-test-idle-button
|
||||
type="button"
|
||||
class="button is-danger is-outlined is-important is-small"
|
||||
disabled={{disabled}}
|
||||
onclick={{action "promptForConfirmation"}}>
|
||||
{{idleText}}
|
||||
</button>
|
||||
{{else if isPendingConfirmation}}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
// Adds a string to the end of a URL path while being mindful of query params
|
||||
export default function addToPath(url, extension = '') {
|
||||
const [path, params] = url.split('?');
|
||||
let newUrl = `${path}${extension}`;
|
||||
|
||||
if (params) {
|
||||
newUrl += `?${params}`;
|
||||
}
|
||||
|
||||
return newUrl;
|
||||
}
|
|
@ -200,6 +200,10 @@ export default function() {
|
|||
|
||||
this.get('/allocation/:id');
|
||||
|
||||
this.post('/allocation/:id/stop', function() {
|
||||
return new Response(204, {}, '');
|
||||
});
|
||||
|
||||
this.get('/namespaces', function({ namespaces }) {
|
||||
const records = namespaces.all();
|
||||
|
||||
|
@ -301,6 +305,10 @@ export default function() {
|
|||
};
|
||||
|
||||
// Client requests are available on the server and the client
|
||||
this.put('/client/allocation/:id/restart', function() {
|
||||
return new Response(204, {}, '');
|
||||
});
|
||||
|
||||
this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
|
||||
this.get('/client/fs/logs/:allocation_id', clientAllocationLog);
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { run } from '@ember/runloop';
|
||||
import { currentURL } from '@ember/test-helpers';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { module, test } from 'qunit';
|
||||
|
@ -155,6 +156,63 @@ module('Acceptance | allocation detail', function(hooks) {
|
|||
assert.ok(Allocation.error.isShown, 'Error message is shown');
|
||||
assert.equal(Allocation.error.title, 'Not Found', 'Error message is for 404');
|
||||
});
|
||||
|
||||
test('allocation can be stopped', async function(assert) {
|
||||
await Allocation.stop.idle();
|
||||
await Allocation.stop.confirm();
|
||||
|
||||
assert.equal(
|
||||
server.pretender.handledRequests.findBy('method', 'POST').url,
|
||||
`/v1/allocation/${allocation.id}/stop`,
|
||||
'Stop request is made for the allocation'
|
||||
);
|
||||
});
|
||||
|
||||
test('allocation can be restarted', async function(assert) {
|
||||
await Allocation.restart.idle();
|
||||
await Allocation.restart.confirm();
|
||||
|
||||
assert.equal(
|
||||
server.pretender.handledRequests.findBy('method', 'PUT').url,
|
||||
`/v1/client/allocation/${allocation.id}/restart`,
|
||||
'Restart request is made for the allocation'
|
||||
);
|
||||
});
|
||||
|
||||
test('while an allocation is being restarted, the stop button is disabled', async function(assert) {
|
||||
server.pretender.post('/v1/allocation/:id/stop', () => [204, {}, ''], true);
|
||||
|
||||
await Allocation.stop.idle();
|
||||
|
||||
run.later(() => {
|
||||
assert.ok(Allocation.stop.isRunning, 'Stop is loading');
|
||||
assert.ok(Allocation.restart.isDisabled, 'Restart is disabled');
|
||||
server.pretender.resolve(server.pretender.requestReferences[0].request);
|
||||
}, 500);
|
||||
|
||||
await Allocation.stop.confirm();
|
||||
});
|
||||
|
||||
test('if stopping or restarting fails, an error message is shown', async function(assert) {
|
||||
server.pretender.post('/v1/allocation/:id/stop', () => [403, {}, '']);
|
||||
|
||||
await Allocation.stop.idle();
|
||||
await Allocation.stop.confirm();
|
||||
|
||||
assert.ok(Allocation.inlineError.isShown, 'Inline error is shown');
|
||||
assert.ok(
|
||||
Allocation.inlineError.title.includes('Could Not Stop Allocation'),
|
||||
'Title is descriptive'
|
||||
);
|
||||
assert.ok(
|
||||
/ACL token.+?allocation lifecycle/.test(Allocation.inlineError.message),
|
||||
'Message mentions ACLs and the appropriate permission'
|
||||
);
|
||||
|
||||
await Allocation.inlineError.dismiss();
|
||||
|
||||
assert.notOk(Allocation.inlineError.isShown, 'Inline error is no longer shown');
|
||||
});
|
||||
});
|
||||
|
||||
module('Acceptance | allocation detail (rescheduled)', function(hooks) {
|
||||
|
|
|
@ -174,6 +174,42 @@ module('Acceptance | task detail', function(hooks) {
|
|||
assert.ok(Task.error.isPresent, 'Error message is shown');
|
||||
assert.equal(Task.error.title, 'Not Found', 'Error message is for 404');
|
||||
});
|
||||
|
||||
test('task can be restarted', async function(assert) {
|
||||
await Task.restart.idle();
|
||||
await Task.restart.confirm();
|
||||
|
||||
const request = server.pretender.handledRequests.findBy('method', 'PUT');
|
||||
assert.equal(
|
||||
request.url,
|
||||
`/v1/client/allocation/${allocation.id}/restart`,
|
||||
'Restart request is made for the allocation'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(request.requestBody),
|
||||
{ TaskName: task.name },
|
||||
'Restart request is made for the correct task'
|
||||
);
|
||||
});
|
||||
|
||||
test('when task restart fails, an error message is shown', async function(assert) {
|
||||
server.pretender.put('/v1/client/allocation/:id/restart', () => [403, {}, '']);
|
||||
|
||||
await Task.restart.idle();
|
||||
await Task.restart.confirm();
|
||||
|
||||
assert.ok(Task.inlineError.isShown, 'Inline error is shown');
|
||||
assert.ok(Task.inlineError.title.includes('Could Not Restart Task'), 'Title is descriptive');
|
||||
assert.ok(
|
||||
/ACL token.+?allocation lifecycle/.test(Task.inlineError.message),
|
||||
'Message mentions ACLs and the appropriate permission'
|
||||
);
|
||||
|
||||
await Task.inlineError.dismiss();
|
||||
|
||||
assert.notOk(Task.inlineError.isShown, 'Inline error is no longer shown');
|
||||
});
|
||||
});
|
||||
|
||||
module('Acceptance | task detail (no addresses)', function(hooks) {
|
||||
|
|
|
@ -4,6 +4,10 @@ import { setupRenderingTest } from 'ember-qunit';
|
|||
import { render, settled } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button';
|
||||
|
||||
const TwoStepButton = create(twoStepButton());
|
||||
|
||||
module('Integration | Component | two step button', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
@ -14,6 +18,7 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
confirmText: 'Confirm Action',
|
||||
confirmationMessage: 'Are you certain',
|
||||
awaitingConfirmation: false,
|
||||
disabled: false,
|
||||
onConfirm: sinon.spy(),
|
||||
onCancel: sinon.spy(),
|
||||
});
|
||||
|
@ -25,6 +30,7 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
confirmText=confirmText
|
||||
confirmationMessage=confirmationMessage
|
||||
awaitingConfirmation=awaitingConfirmation
|
||||
disabled=disabled
|
||||
onConfirm=onConfirm
|
||||
onCancel=onCancel}}
|
||||
`;
|
||||
|
@ -35,11 +41,7 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
await 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.equal(TwoStepButton.idleText, 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');
|
||||
|
@ -51,25 +53,17 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
TwoStepButton.idle();
|
||||
|
||||
return settled().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.equal(TwoStepButton.cancelText, 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(TwoStepButton.confirmText, props.confirmText, 'Button is labeled correctly');
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-confirmation-message]').textContent.trim(),
|
||||
TwoStepButton.confirmationMessage,
|
||||
props.confirmationMessage,
|
||||
'Confirmation message is shown'
|
||||
);
|
||||
|
@ -83,10 +77,10 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
TwoStepButton.idle();
|
||||
|
||||
return settled().then(() => {
|
||||
click('[data-test-cancel-button]');
|
||||
TwoStepButton.cancel();
|
||||
|
||||
return settled().then(() => {
|
||||
assert.ok(props.onCancel.calledOnce, 'The onCancel hook fired');
|
||||
|
@ -100,10 +94,10 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
TwoStepButton.idle();
|
||||
|
||||
return settled().then(() => {
|
||||
click('[data-test-confirm-button]');
|
||||
TwoStepButton.confirm();
|
||||
|
||||
return settled().then(() => {
|
||||
assert.ok(props.onConfirm.calledOnce, 'The onConfirm hook fired');
|
||||
|
@ -118,21 +112,73 @@ module('Integration | Component | two step button', function(hooks) {
|
|||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
click('[data-test-idle-button]');
|
||||
TwoStepButton.idle();
|
||||
|
||||
return settled().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'
|
||||
);
|
||||
assert.ok(TwoStepButton.cancelIsDisabled, 'The cancel button is disabled');
|
||||
assert.ok(TwoStepButton.confirmIsDisabled, 'The confirm button is disabled');
|
||||
assert.ok(TwoStepButton.isRunning, 'The confirm button is in a loading state');
|
||||
});
|
||||
});
|
||||
|
||||
test('when in the prompt state, clicking outside will reset state back to idle', async function(assert) {
|
||||
const props = commonProperties();
|
||||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
TwoStepButton.idle();
|
||||
await settled();
|
||||
|
||||
assert.ok(find('[data-test-cancel-button]'), 'In the prompt state');
|
||||
|
||||
click(document.body);
|
||||
await settled();
|
||||
|
||||
assert.ok(find('[data-test-idle-button]'), 'Back in the idle state');
|
||||
});
|
||||
|
||||
test('when in the prompt state, clicking inside will not reset state back to idle', async function(assert) {
|
||||
const props = commonProperties();
|
||||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
TwoStepButton.idle();
|
||||
await settled();
|
||||
|
||||
assert.ok(find('[data-test-cancel-button]'), 'In the prompt state');
|
||||
|
||||
click('[data-test-confirmation-message]');
|
||||
await settled();
|
||||
|
||||
assert.notOk(find('[data-test-idle-button]'), 'Still in the prompt state');
|
||||
});
|
||||
|
||||
test('when awaitingConfirmation is true, clicking outside does nothing', async function(assert) {
|
||||
const props = commonProperties();
|
||||
props.awaitingConfirmation = true;
|
||||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
TwoStepButton.idle();
|
||||
await settled();
|
||||
|
||||
assert.ok(find('[data-test-cancel-button]'), 'In the prompt state');
|
||||
|
||||
click(document.body);
|
||||
await settled();
|
||||
|
||||
assert.notOk(find('[data-test-idle-button]'), 'Still in the prompt state');
|
||||
});
|
||||
|
||||
test('when disabled is true, the idle button is disabled', async function(assert) {
|
||||
const props = commonProperties();
|
||||
props.disabled = true;
|
||||
this.setProperties(props);
|
||||
await render(commonTemplate);
|
||||
|
||||
assert.ok(TwoStepButton.isDisabled, 'The idle button is disabled');
|
||||
|
||||
TwoStepButton.idle();
|
||||
assert.ok(find('[data-test-idle-button]'), 'Still in the idle state after clicking');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,12 +9,16 @@ import {
|
|||
} from 'ember-cli-page-object';
|
||||
|
||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||
import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/allocations/:id'),
|
||||
|
||||
title: text('[data-test-title]'),
|
||||
|
||||
stop: twoStepButton('[data-test-stop]'),
|
||||
restart: twoStepButton('[data-test-restart]'),
|
||||
|
||||
details: {
|
||||
scope: '[data-test-allocation-details]',
|
||||
|
||||
|
@ -77,4 +81,11 @@ export default create({
|
|||
message: text('[data-test-error-message]'),
|
||||
seekHelp: clickable('[data-test-error-message] a'),
|
||||
},
|
||||
|
||||
inlineError: {
|
||||
isShown: isPresent('[data-test-inline-error]'),
|
||||
title: text('[data-test-inline-error-title]'),
|
||||
message: text('[data-test-inline-error-body]'),
|
||||
dismiss: clickable('[data-test-inline-error-close]'),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
visitable,
|
||||
} from 'ember-cli-page-object';
|
||||
|
||||
import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button';
|
||||
|
||||
export default create({
|
||||
visit: visitable('/allocations/:id/:name'),
|
||||
|
||||
|
@ -15,6 +17,8 @@ export default create({
|
|||
state: text('[data-test-state]'),
|
||||
startedAt: text('[data-test-started-at]'),
|
||||
|
||||
restart: twoStepButton('[data-test-restart]'),
|
||||
|
||||
breadcrumbs: collection('[data-test-breadcrumb]', {
|
||||
id: attribute('data-test-breadcrumb'),
|
||||
text: text(),
|
||||
|
@ -51,4 +55,11 @@ export default create({
|
|||
message: text('[data-test-error-message]'),
|
||||
seekHelp: clickable('[data-test-error-message] a'),
|
||||
},
|
||||
|
||||
inlineError: {
|
||||
isShown: isPresent('[data-test-inline-error]'),
|
||||
title: text('[data-test-inline-error-title]'),
|
||||
message: text('[data-test-inline-error-body]'),
|
||||
dismiss: clickable('[data-test-inline-error-close]'),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { attribute, clickable, hasClass, isPresent, text } from 'ember-cli-page-object';
|
||||
|
||||
export default scope => ({
|
||||
scope,
|
||||
|
||||
isPresent: isPresent(),
|
||||
|
||||
idle: clickable('[data-test-idle-button]'),
|
||||
confirm: clickable('[data-test-confirm-button]'),
|
||||
cancel: clickable('[data-test-cancel-button]'),
|
||||
|
||||
isRunning: hasClass('is-loading', '[data-test-confirm-button]'),
|
||||
isDisabled: attribute('disabled', '[data-test-idle-button]'),
|
||||
|
||||
cancelIsDisabled: attribute('disabled', '[data-test-cancel-button]'),
|
||||
confirmIsDisabled: attribute('disabled', '[data-test-confirm-button]'),
|
||||
|
||||
idleText: text('[data-test-idle-button]'),
|
||||
cancelText: text('[data-test-cancel-button]'),
|
||||
confirmText: text('[data-test-confirm-button]'),
|
||||
confirmationMessage: text('[data-test-confirmation-message]'),
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
|
||||
|
||||
module('Unit | Adapter | Allocation', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
hooks.beforeEach(async function() {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.subject = () => this.store.adapterFor('allocation');
|
||||
|
||||
this.server = startMirage();
|
||||
|
||||
this.server.create('namespace');
|
||||
this.server.create('node');
|
||||
this.server.create('job', { createAllocations: false });
|
||||
this.server.create('allocation', { id: 'alloc-1' });
|
||||
});
|
||||
|
||||
hooks.afterEach(function() {
|
||||
this.server.shutdown();
|
||||
});
|
||||
|
||||
test('`stop` makes the correct API call', async function(assert) {
|
||||
const { pretender } = this.server;
|
||||
const allocationId = 'alloc-1';
|
||||
|
||||
const allocation = await this.store.findRecord('allocation', allocationId);
|
||||
pretender.handledRequests.length = 0;
|
||||
|
||||
await this.subject().stop(allocation);
|
||||
const req = pretender.handledRequests[0];
|
||||
assert.equal(
|
||||
`${req.method} ${req.url}`,
|
||||
`POST /v1/allocation/${allocationId}/stop`,
|
||||
`POST /v1/allocation/${allocationId}/stop`
|
||||
);
|
||||
});
|
||||
|
||||
test('`restart` makes the correct API call', async function(assert) {
|
||||
const { pretender } = this.server;
|
||||
const allocationId = 'alloc-1';
|
||||
|
||||
const allocation = await this.store.findRecord('allocation', allocationId);
|
||||
pretender.handledRequests.length = 0;
|
||||
|
||||
await this.subject().restart(allocation);
|
||||
const req = pretender.handledRequests[0];
|
||||
assert.equal(
|
||||
`${req.method} ${req.url}`,
|
||||
`PUT /v1/client/allocation/${allocationId}/restart`,
|
||||
`PUT /v1/client/allocation/${allocationId}/restart`
|
||||
);
|
||||
});
|
||||
|
||||
test('`restart` takes an optional task name and makes the correct API call', async function(assert) {
|
||||
const { pretender } = this.server;
|
||||
const allocationId = 'alloc-1';
|
||||
const taskName = 'task-name';
|
||||
|
||||
const allocation = await this.store.findRecord('allocation', allocationId);
|
||||
pretender.handledRequests.length = 0;
|
||||
|
||||
await this.subject().restart(allocation, taskName);
|
||||
const req = pretender.handledRequests[0];
|
||||
assert.equal(
|
||||
`${req.method} ${req.url}`,
|
||||
`PUT /v1/client/allocation/${allocationId}/restart`,
|
||||
`PUT /v1/client/allocation/${allocationId}/restart`
|
||||
);
|
||||
assert.deepEqual(
|
||||
JSON.parse(req.requestBody),
|
||||
{ TaskName: taskName },
|
||||
'Request body is correct'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import { module, test } from 'qunit';
|
||||
import addToPath from 'nomad-ui/utils/add-to-path';
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Only domain',
|
||||
in: ['https://domain.com', '/path'],
|
||||
out: 'https://domain.com/path',
|
||||
},
|
||||
{
|
||||
name: 'Deep path',
|
||||
in: ['https://domain.com/a/path', '/to/nowhere'],
|
||||
out: 'https://domain.com/a/path/to/nowhere',
|
||||
},
|
||||
{
|
||||
name: 'With Query Params',
|
||||
in: ['https://domain.com?interesting=development', '/this-is-an'],
|
||||
out: 'https://domain.com/this-is-an?interesting=development',
|
||||
},
|
||||
];
|
||||
|
||||
module('Unit | Util | addToPath', function() {
|
||||
testCases.forEach(testCase => {
|
||||
test(testCase.name, function(assert) {
|
||||
assert.equal(
|
||||
addToPath.apply(null, testCase.in),
|
||||
testCase.out,
|
||||
`[${testCase.in.join(', ')}] => ${testCase.out}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue