Skip to content

Commit

Permalink
Add button to fail running deployments (#9831)
Browse files Browse the repository at this point in the history
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"}}
  • Loading branch information
backspace committed Feb 10, 2021
1 parent 06744e0 commit d98265d
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 15 deletions.
10 changes: 10 additions & 0 deletions ui/app/adapters/deployment.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
18 changes: 18 additions & 0 deletions ui/app/components/job-page/parts/latest-deployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
5 changes: 3 additions & 2 deletions ui/app/components/two-step-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand All @@ -17,7 +18,7 @@ export default class TwoStepButton extends Component {
awaitingConfirmation = false;
disabled = false;
alignRight = false;
isInfoAction = false;
inlineText = false;
onConfirm() {}
onCancel() {}

Expand Down
5 changes: 5 additions & 0 deletions ui/app/models/deployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
14 changes: 14 additions & 0 deletions ui/app/styles/components/two-step-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion ui/app/templates/clients/client/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 26 additions & 8 deletions ui/app/templates/components/job-page/parts/latest-deployment.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
8 changes: 4 additions & 4 deletions ui/app/templates/components/two-step-button.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
<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}}
</button>
{{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")
Expand All @@ -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">
Expand Down
5 changes: 5 additions & 0 deletions ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {}, '');
});
Expand Down
47 changes: 47 additions & 0 deletions ui/stories/components/two-step-button.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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`
Expand Down
58 changes: 58 additions & 0 deletions ui/tests/integration/components/job-page/service-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
14 changes: 14 additions & 0 deletions ui/tests/unit/adapters/deployment-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
},
];
Expand All @@ -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,
});
});
});
});

0 comments on commit d98265d

Please sign in to comment.