mirror of
https://github.com/kemko/nomad.git
synced 2026-01-03 08:55:43 +03:00
Add button to fail running deployments (#9831)
This closes #8744 and #9826. It necessitated some customisation options for TwoStepButton. One is inlineText, which puts the confirmation text in the same line as the buttons. Also, there was a single-use configuration option named isInfoAction that I removed in favour of passing a set of class configuration options like this: @classes={{hash idleButton="is-warning" confirmationMessage="inherit-color" cancelButton="is-danger is-important" confirmButton="is-warning"}}
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import Watchable from './watchable';
|
||||
|
||||
export default class DeploymentAdapter extends Watchable {
|
||||
fail(deployment) {
|
||||
const id = deployment.get('id');
|
||||
const url = urlForAction(this.urlForFindRecord(id, 'deployment'), '/fail');
|
||||
return this.ajax(url, 'POST', {
|
||||
data: {
|
||||
DeploymentId: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
promote(deployment) {
|
||||
const id = deployment.get('id');
|
||||
const url = urlForAction(this.urlForFindRecord(id, 'deployment'), '/promote');
|
||||
|
||||
@@ -24,4 +24,22 @@ export default class LatestDeployment extends Component {
|
||||
}
|
||||
})
|
||||
promote;
|
||||
|
||||
@task(function*() {
|
||||
try {
|
||||
yield this.get('job.latestDeployment.content').fail();
|
||||
} catch (err) {
|
||||
let message = messageFromAdapterError(err);
|
||||
|
||||
if (err instanceof ForbiddenError) {
|
||||
message = 'Your ACL token does not grant permission to fail deployments.';
|
||||
}
|
||||
|
||||
this.handleError({
|
||||
title: 'Could Not Fail Deployment',
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
})
|
||||
fail;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import { next } from '@ember/runloop';
|
||||
import { equal } from '@ember/object/computed';
|
||||
import { task, waitForEvent } from 'ember-concurrency';
|
||||
import RSVP from 'rsvp';
|
||||
import { classNames } from '@ember-decorators/component';
|
||||
import { classNames, classNameBindings } from '@ember-decorators/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
@classNames('two-step-button')
|
||||
@classNameBindings('inlineText:has-inline-text')
|
||||
export default class TwoStepButton extends Component {
|
||||
idleText = '';
|
||||
cancelText = '';
|
||||
@@ -17,7 +18,7 @@ export default class TwoStepButton extends Component {
|
||||
awaitingConfirmation = false;
|
||||
disabled = false;
|
||||
alignRight = false;
|
||||
isInfoAction = false;
|
||||
inlineText = false;
|
||||
onConfirm() {}
|
||||
onCancel() {}
|
||||
|
||||
|
||||
@@ -69,4 +69,9 @@ export default class Deployment extends Model {
|
||||
assert('A deployment needs to requirePromotion to be promoted', this.requiresPromotion);
|
||||
return this.store.adapterFor('deployment').promote(this);
|
||||
}
|
||||
|
||||
fail() {
|
||||
assert('A deployment must be running to be failed', this.isRunning);
|
||||
return this.store.adapterFor('deployment').fail(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
font-size: $body-size;
|
||||
line-height: 1;
|
||||
|
||||
&.has-inline-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
margin-left: 0.5ch;
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
position: static;
|
||||
margin-right: 0.5ch;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -212,7 +212,11 @@
|
||||
<TwoStepButton
|
||||
data-test-force
|
||||
@alignRight={{true}}
|
||||
@isInfoAction={{true}}
|
||||
@classes={{hash
|
||||
idleButton="is-warning"
|
||||
confirmationMessage="inherit-color"
|
||||
cancelButton="is-danger is-important"
|
||||
confirmButton="is-warning"}}
|
||||
@idleText="Force Drain"
|
||||
@cancelText="Cancel"
|
||||
@confirmText="Yes, Force Drain"
|
||||
|
||||
@@ -14,14 +14,32 @@
|
||||
<span class="tag is-outlined {{this.job.latestDeployment.statusClass}}" data-test-deployment-status="{{this.job.latestDeployment.statusClass}}">
|
||||
{{this.job.latestDeployment.status}}
|
||||
</span>
|
||||
{{#if this.job.latestDeployment.requiresPromotion}}
|
||||
<button
|
||||
data-test-promote-canary
|
||||
type="button"
|
||||
class="button is-warning is-small pull-right {{if this.promote.isRunning "is-loading"}}"
|
||||
disabled={{this.promote.isRunning}}
|
||||
onclick={{perform this.promote}}>Promote Canary</button>
|
||||
{{/if}}
|
||||
<div class="pull-right">
|
||||
{{#if this.job.latestDeployment.isRunning}}
|
||||
<TwoStepButton
|
||||
data-test-fail
|
||||
@classes={{hash
|
||||
idleButton="is-danger"
|
||||
confirmationMessage="inherit-color"
|
||||
confirmButton="is-danger"}}
|
||||
@idleText="Fail Deployment"
|
||||
@cancelText="Cancel"
|
||||
@confirmText="Yes, Fail"
|
||||
@confirmationMessage="Are you sure?"
|
||||
@inlineText={{true}}
|
||||
@awaitingConfirmation={{this.fail.isRunning}}
|
||||
@disabled={{this.fail.isRunning}}
|
||||
@onConfirm={{perform this.fail}} />
|
||||
{{/if}}
|
||||
{{#if this.job.latestDeployment.requiresPromotion}}
|
||||
<button
|
||||
data-test-promote-canary
|
||||
type="button"
|
||||
class="button is-warning is-small {{if this.promote.isRunning "is-loading"}}"
|
||||
disabled={{this.promote.isRunning}}
|
||||
onclick={{perform this.promote}}>Promote Canary</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body with-foot">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<button
|
||||
data-test-idle-button
|
||||
type="button"
|
||||
class="button {{if this.isInfoAction "is-warning" "is-danger is-outlined"}} is-important is-small"
|
||||
class="button {{if this.classes.idleButton this.classes.idleButton "is-danger is-outlined"}} is-important is-small"
|
||||
disabled={{this.disabled}}
|
||||
onclick={{action "promptForConfirmation"}}>
|
||||
{{this.idleText}}
|
||||
@@ -10,13 +10,13 @@
|
||||
{{else if this.isPendingConfirmation}}
|
||||
<span
|
||||
data-test-confirmation-message
|
||||
class="confirmation-text {{if this.isInfoAction "inherit-color"}} {{if this.alignRight "is-right-aligned"}}">
|
||||
class="confirmation-text {{this.classes.confirmationMessage}} {{if this.alignRight "is-right-aligned"}} {{if this.inlineText "has-text-inline"}}">
|
||||
{{this.confirmationMessage}}
|
||||
</span>
|
||||
<button
|
||||
data-test-cancel-button
|
||||
type="button"
|
||||
class="button {{if this.isInfoAction "is-danger is-important" "is-dark"}} is-outlined is-small"
|
||||
class="button is-outlined is-small {{if this.classes.cancelButton this.classes.cancelButton "is-dark"}}"
|
||||
disabled={{this.awaitingConfirmation}}
|
||||
onclick={{action (queue
|
||||
(action "setToIdle")
|
||||
@@ -26,7 +26,7 @@
|
||||
</button>
|
||||
<button
|
||||
data-test-confirm-button
|
||||
class="button {{if this.isInfoAction "is-warning" "is-danger"}} is-small {{if this.awaitingConfirmation "is-loading"}}"
|
||||
class="button is-small {{if this.awaitingConfirmation "is-loading"}} {{if this.classes.confirmButton this.classes.confirmButton "is-danger"}}"
|
||||
disabled={{this.awaitingConfirmation}}
|
||||
onclick={{action "confirm"}}
|
||||
type="button">
|
||||
|
||||
@@ -191,6 +191,11 @@ export default function() {
|
||||
});
|
||||
|
||||
this.get('/deployment/:id');
|
||||
|
||||
this.post('/deployment/fail/:id', function() {
|
||||
return new Response(204, {}, '');
|
||||
});
|
||||
|
||||
this.post('/deployment/promote/:id', function() {
|
||||
return new Response(204, {}, '');
|
||||
});
|
||||
|
||||
@@ -19,6 +19,27 @@ export let Standard = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export let Styled = () => {
|
||||
return {
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Two-Step Button with class overrides</h5>
|
||||
<br><br>
|
||||
<TwoStepButton
|
||||
@idleText="Scary Action"
|
||||
@cancelText="Nvm"
|
||||
@confirmText="Yep"
|
||||
@confirmationMessage="Wait, really? Like...seriously?"
|
||||
@classes={{hash
|
||||
idleButton="is-danger is-large"
|
||||
confirmationMessage="badge is-warning"
|
||||
confirmButton="is-large"
|
||||
cancelButton="is-hollow"
|
||||
}}
|
||||
/>
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export let InTitle = () => {
|
||||
return {
|
||||
template: hbs`
|
||||
@@ -37,6 +58,32 @@ export let InTitle = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export let InlineText = () => {
|
||||
return {
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Two-Step Button with inline confirmation message</h5>
|
||||
<br><br>
|
||||
<TwoStepButton
|
||||
@idleText="Scary Action"
|
||||
@cancelText="Nvm"
|
||||
@confirmText="Yep"
|
||||
@confirmationMessage="Really?"
|
||||
@inlineText={{true}}
|
||||
/>
|
||||
<br><br>
|
||||
<span style="padding-left: 4rem"></span>
|
||||
<TwoStepButton
|
||||
@idleText="Scary Action"
|
||||
@cancelText="Nvm"
|
||||
@confirmText="Yep"
|
||||
@confirmationMessage="Really?"
|
||||
@alignRight={{true}}
|
||||
@inlineText={{true}}
|
||||
/>
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export let LoadingState = () => {
|
||||
return {
|
||||
template: hbs`
|
||||
|
||||
@@ -226,4 +226,62 @@ module('Integration | Component | job-page/service', function(hooks) {
|
||||
|
||||
assert.notOk(find('[data-test-job-error-title]'), 'Error message is dismissable');
|
||||
});
|
||||
|
||||
test('Active deployment can be failed', async function(assert) {
|
||||
this.server.create('node');
|
||||
const mirageJob = makeMirageJob(this.server, { activeDeployment: true });
|
||||
|
||||
await this.store.findAll('job');
|
||||
|
||||
const job = this.store.peekAll('job').findBy('plainId', mirageJob.id);
|
||||
const deployment = await job.get('latestDeployment');
|
||||
|
||||
this.setProperties(commonProperties(job));
|
||||
await render(commonTemplate);
|
||||
|
||||
await click('[data-test-active-deployment] [data-test-idle-button]');
|
||||
await click('[data-test-active-deployment] [data-test-confirm-button]');
|
||||
|
||||
const requests = this.server.pretender.handledRequests;
|
||||
|
||||
assert.ok(
|
||||
requests
|
||||
.filterBy('method', 'POST')
|
||||
.findBy('url', `/v1/deployment/fail/${deployment.get('id')}`),
|
||||
'A fail POST request was made'
|
||||
);
|
||||
});
|
||||
|
||||
test('When failing the active deployment fails, an error is shown', async function(assert) {
|
||||
this.server.pretender.post('/v1/deployment/fail/:id', () => [403, {}, '']);
|
||||
|
||||
this.server.create('node');
|
||||
const mirageJob = makeMirageJob(this.server, { activeDeployment: true });
|
||||
|
||||
await this.store.findAll('job');
|
||||
|
||||
const job = this.store.peekAll('job').findBy('plainId', mirageJob.id);
|
||||
|
||||
this.setProperties(commonProperties(job));
|
||||
await render(commonTemplate);
|
||||
|
||||
await click('[data-test-active-deployment] [data-test-idle-button]');
|
||||
await click('[data-test-active-deployment] [data-test-confirm-button]');
|
||||
|
||||
assert.equal(
|
||||
find('[data-test-job-error-title]').textContent,
|
||||
'Could Not Fail Deployment',
|
||||
'Appropriate error is shown'
|
||||
);
|
||||
assert.ok(
|
||||
find('[data-test-job-error-body]').textContent.includes('ACL'),
|
||||
'The error message mentions ACLs'
|
||||
);
|
||||
|
||||
await componentA11yAudit(this.element, assert);
|
||||
|
||||
await click('[data-test-job-error-close]');
|
||||
|
||||
assert.notOk(find('[data-test-job-error-title]'), 'Error message is dismissable');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,11 +42,13 @@ module('Unit | Adapter | Deployment', function(hooks) {
|
||||
{
|
||||
variation: '',
|
||||
region: null,
|
||||
fail: id => `POST /v1/deployment/fail/${id}`,
|
||||
promote: id => `POST /v1/deployment/promote/${id}`,
|
||||
},
|
||||
{
|
||||
variation: 'with non-default region',
|
||||
region: 'region-2',
|
||||
fail: id => `POST /v1/deployment/fail/${id}?region=region-2`,
|
||||
promote: id => `POST /v1/deployment/promote/${id}?region=region-2`,
|
||||
},
|
||||
];
|
||||
@@ -64,5 +66,17 @@ module('Unit | Adapter | Deployment', function(hooks) {
|
||||
All: true,
|
||||
});
|
||||
});
|
||||
|
||||
test(`fail makes the correct API call ${testCase.variation}`, async function(assert) {
|
||||
const deployment = await this.initialize({ region: testCase.region });
|
||||
await this.subject().fail(deployment);
|
||||
|
||||
const request = this.server.pretender.handledRequests[0];
|
||||
|
||||
assert.equal(`${request.method} ${request.url}`, testCase.fail(deployment.id));
|
||||
assert.deepEqual(JSON.parse(request.requestBody), {
|
||||
DeploymentId: deployment.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user