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

feat: Table cell for actions #1169

Merged
merged 4 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions app/components/pipeline/jobs/table/cell/actions/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { isInactivePipeline } from 'screwdriver-ui/utils/pipeline';
import {
isRestartButtonDisabled,
isStartButtonDisabled,
isStopButtonDisabled,
shouldDisplayNotice
} from './util';

export default class PipelineJobsTableCellActionsComponent extends Component {
@service shuttle;

@tracked latestBuild;

@tracked showStartEventModal = false;

@tracked showStopBuildModal = false;

@tracked showRestartJobModal = false;

isPipelineInactive = true;

latestEvent;

latestCommitEvent;

job;

constructor() {
super(...arguments);

this.job = { ...this.args.record.job };

this.args.record.onCreate(this.job, builds => {
if (builds.length > 0) {
this.latestBuild = builds[builds.length - 1];
}
});

this.isPipelineInactive = isInactivePipeline(this.args.record.pipeline);
}

willDestroy() {
super.willDestroy();

this.args.record.onDestroy(this.args.record.job);
}

get notice() {
const { job } = this.args.record;

return shouldDisplayNotice(this.args.record.pipeline, job)
? `This will create a new event and then trigger any jobs that are downstream of this job (${job.name}). Due consider whether job can be successfully completed without having any upstream jobs having been run (i.e., does this job depend on any metadata that was previously set?)`
minghay marked this conversation as resolved.
Show resolved Hide resolved
: null;
}

get startButtonDisabled() {
return isStartButtonDisabled(this.isPipelineInactive, this.args.record.job);
}

get stopButtonDisabled() {
return isStopButtonDisabled(this.latestBuild);
}

get restartButtonDisabled() {
return isRestartButtonDisabled(
this.isPipelineInactive,
this.args.record.job,
this.latestBuild
);
}

@action
openStartEventModal() {
this.showStartEventModal = true;
}

@action
closeStartEventModal() {
this.showStartEventModal = false;
}

@action
openStopBuildModal() {
this.showStopBuildModal = true;
}

@action
closeStopBuildModal() {
this.showStopBuildModal = false;
}

@action
async openRestartJobModal() {
const eventId = this.latestBuild.meta.build
? this.latestBuild.meta.build.eventId
: await this.shuttle
.fetchFromApi('get', `/builds/${this.latestBuild.id}`)
.then(response => {
return response.eventId;
});

Promise.all([
this.shuttle.fetchFromApi('get', `/events/${eventId}`),
this.shuttle.fetchFromApi(
'get',
`/pipelines/${this.args.record.pipeline.id}/latestCommitEvent`
)
]).then(([latestEvent, latestCommitEvent]) => {
this.latestEvent = latestEvent;
this.latestCommitEvent = latestCommitEvent;
this.job.status = this.latestBuild.status;
this.showRestartJobModal = true;
});
}

@action
closeRestartJobModal() {
this.showRestartJobModal = false;
}
}
29 changes: 29 additions & 0 deletions app/components/pipeline/jobs/table/cell/actions/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@use 'screwdriver-button' as button;
@use 'screwdriver-colors' as colors;

@mixin styles {
.actions-cell {
display: flex;

@include button.styles;

.btn {
display: flex;
border-color: transparent;
padding: 0;

&:disabled {
color: colors.$sd-disabled;
}

&:hover {
background-color: transparent;
}

svg {
font-size: 1.25rem;
margin: auto;
}
}
}
}
69 changes: 69 additions & 0 deletions app/components/pipeline/jobs/table/cell/actions/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<div class="actions-cell">
<BsButton
@type="primary"
@outline=true
@value="start"
disabled={{this.startButtonDisabled}}
@onClick={{fn this.openStartEventModal}}
>
<FaIcon
@icon="play-circle"
@fixedWidth="true"
@prefix="far"
@title="Start a new event"
/>
</BsButton>
<BsButton
@type="danger"
@outline=true
disabled={{this.stopButtonDisabled}}
@onClick={{fn this.openStopBuildModal}}
>
<FaIcon
@icon="stop-circle"
@fixedWidth="true"
@prefix="far"
@title="Stop build"
/>
</BsButton>
<BsButton
@type="primary"
@outline=true
@value="restart"
disabled={{this.restartButtonDisabled}}
@onClick={{fn this.openRestartJobModal}}
>
<FaIcon
@icon="redo-alt"
@fixedWidth="true"
@prefix="fas"
@title="Restart from latest build"
/>
</BsButton>

{{#if this.showStartEventModal}}
<Pipeline::Modal::StartEvent
@pipeline={{@record.pipeline}}
@jobs={{@record.jobs}}
@job={{@record.job}}
@notice={{this.notice}}
@closeModal={{this.closeStartEventModal}}
/>
{{/if}}
{{#if this.showStopBuildModal}}
<Pipeline::Modal::StopBuild
@buildId={{this.latestBuild.id}}
@closeModal={{this.closeStopBuildModal}}
/>
{{/if}}
{{#if this.showRestartJobModal}}
<Pipeline::Modal::ConfirmAction
@pipeline={{@record.pipeline}}
@event={{this.latestEvent}}
@jobs={{@record.jobs}}
@latestCommitEvent={{this.latestCommitEvent}}
@job={{this.job}}
@closeModal={{this.closeRestartJobModal}}
/>
{{/if}}
</div>
69 changes: 69 additions & 0 deletions app/components/pipeline/jobs/table/cell/actions/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { unfinishedStatuses } from 'screwdriver-ui/utils/build';

/**
* Checks if the job can be triggered
* @param job {object} Job object in the shape returned by the API
* @returns {boolean}
*/
export const canJobBeTriggered = job => {
if (job.state === 'DISABLED') {
return false;
}

return (
job?.permutations?.[0]?.annotations?.[
'screwdriver.cd/manualStartEnabled'
] !== false
);
};

/**
* Check if the start button should be disabled
* @param isPipelineInactive {boolean}
minghay marked this conversation as resolved.
Show resolved Hide resolved
* @param job {object} Job object in the shape returned by the API
* @returns {boolean}
*/
export const isStartButtonDisabled = (isPipelineInactive, job) => {
if (isPipelineInactive) {
return true;
}

return !canJobBeTriggered(job);
};

/**
* Check if the stop button should be disabled
* @param build {object} Build object in the shape returned by the API
* @returns {boolean}
*/
export const isStopButtonDisabled = build => {
return build ? !unfinishedStatuses.includes(build.status) : true;
};

/**
* Check if the restart button should be disabled
* @param isPipelineInactive {boolean}
* @param job {object} Job object in the shape returned by the API
* @param build {object} Build object in the shape returned by the API
* @returns {boolean}
*/
export const isRestartButtonDisabled = (isPipelineInactive, job, build) => {
if (isStartButtonDisabled(isPipelineInactive, job)) {
return true;
}

return build ? unfinishedStatuses.includes(build.status) : true;
};

/**
* Check if the notice should be displayed.
* @param pipeline {object} Pipeline object in the shape returned by the API
* @param job {object} Job object in the shape returned by the API
* @returns {boolean}
*/
export const shouldDisplayNotice = (pipeline, job) => {
const { edges } = pipeline.workflowGraph;
const jobName = job.name;

return !edges.some(edge => edge.dest === jobName && edge.src.startsWith('~'));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'screwdriver-ui/tests/helpers';
import { clearRender, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';

module(
'Integration | Component | pipeline/jobs/table/cell/actions',
function (hooks) {
setupRenderingTest(hooks);

test('it renders', async function (assert) {
const job = { id: 123, name: 'main', state: 'ENABLED' };
const pipeline = { state: 'INACTIVE' };

this.setProperties({
record: {
job,
pipeline,
onCreate: () => {},
onDestroy: () => {}
}
});

await render(
hbs`<Pipeline::Jobs::Table::Cell::Actions
@record={{this.record}}
/>`
);

assert.dom('button').exists({ count: 3 });
});

test('it calls onCreate', async function (assert) {
const onCreate = sinon.spy();
const job = { id: 123, name: 'main', state: 'ENABLED' };
const pipeline = { state: 'INACTIVE' };

this.setProperties({
record: {
job,
pipeline,
onCreate,
onDestroy: () => {}
}
});

await render(
hbs`<Pipeline::Jobs::Table::Cell::Actions
@record={{this.record}}
/>`
);

assert.equal(onCreate.calledOnce, true);
assert.equal(onCreate.calledWith(job), true);
});

test('it calls onDestroy', async function (assert) {
const onDestroy = sinon.spy();
const job = { id: 123, name: 'main', state: 'ENABLED' };
const pipeline = { state: 'INACTIVE' };

this.setProperties({
record: {
job,
pipeline,
onCreate: () => {},
onDestroy
}
});

await render(
hbs`<Pipeline::Jobs::Table::Cell::Actions
@record={{this.record}}
/>`
);
await clearRender();

assert.equal(onDestroy.calledOnce, true);
assert.equal(onDestroy.calledWith(job), true);
});
}
);
Loading