Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Add button to fail running deployments #9831

Merged
merged 11 commits into from
Feb 10, 2021
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 @@ -31,4 +31,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,
});
});
});
});