From cf5858bdb765ba6bdd9f5e0c9f5fe728b7f5ea80 Mon Sep 17 00:00:00 2001 From: Alan Greene Date: Fri, 19 Aug 2022 12:23:10 +0100 Subject: [PATCH] Add ability to rerun and cancel Runs - Standalone Run resources can be rerun from the list page (i.e. not child of a Pipeline) - Any Run that is currently running (i.e. status == Unknown) can be stopped - Update StatusIcon to render running state to match behaviour described in the Runs documentation --- .../src/components/StatusIcon/StatusIcon.js | 8 ++- .../components/StatusIcon/StatusIcon.test.js | 4 ++ src/api/runs.js | 35 ++++++++++- src/api/runs.test.js | 38 ++++++++++++ src/containers/Run/Run.js | 7 ++- src/containers/Runs/Runs.js | 60 ++++++++++++++++++- src/nls/messages_de.json | 3 + src/nls/messages_en.json | 3 + src/nls/messages_es.json | 3 + src/nls/messages_fr.json | 3 + src/nls/messages_it.json | 3 + src/nls/messages_ja.json | 3 + src/nls/messages_ko.json | 3 + src/nls/messages_pt.json | 3 + src/nls/messages_zh-Hans.json | 3 + src/nls/messages_zh-Hant.json | 3 + 16 files changed, 176 insertions(+), 6 deletions(-) diff --git a/packages/components/src/components/StatusIcon/StatusIcon.js b/packages/components/src/components/StatusIcon/StatusIcon.js index b0e8a4ecb..9d2fd0691 100644 --- a/packages/components/src/components/StatusIcon/StatusIcon.js +++ b/packages/components/src/components/StatusIcon/StatusIcon.js @@ -62,6 +62,7 @@ const typeClassNames = { export default function StatusIcon({ DefaultIcon, hasWarning, + isCustomTask, reason, status, title, @@ -73,8 +74,6 @@ export default function StatusIcon({ (status === 'Unknown' && reason === 'Pending') ) { statusClass = 'pending'; - } else if (isRunning(reason, status)) { - statusClass = 'running'; } else if ( status === 'True' || (status === 'terminated' && reason === 'Completed') @@ -94,6 +93,11 @@ export default function StatusIcon({ (status === 'Unknown' && reason === 'PipelineRunCouldntCancel') ) { statusClass = 'error'; + } else if ( + isRunning(reason, status) || + (isCustomTask && status === 'Unknown') + ) { + statusClass = 'running'; } const Icon = icons[type]?.[statusClass] || DefaultIcon; diff --git a/packages/components/src/components/StatusIcon/StatusIcon.test.js b/packages/components/src/components/StatusIcon/StatusIcon.test.js index 6892ef57f..0c96b29bc 100644 --- a/packages/components/src/components/StatusIcon/StatusIcon.test.js +++ b/packages/components/src/components/StatusIcon/StatusIcon.test.js @@ -43,4 +43,8 @@ describe('StatusIcon', () => { it('gracefully handles unsupported state', () => { render(); }); + + // it('renders completed step', () => { + // render(); + // }); }); diff --git a/src/api/runs.js b/src/api/runs.js index f4bbfe426..0af4d4180 100644 --- a/src/api/runs.js +++ b/src/api/runs.js @@ -11,7 +11,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { deleteRequest, get } from './comms'; +import { getGenerateNamePrefixForRerun } from '@tektoncd/dashboard-utils'; +import deepClone from 'lodash.clonedeep'; + +import { deleteRequest, get, patch, post } from './comms'; import { getQueryParams, getTektonAPI, @@ -62,3 +65,33 @@ export function useRun(params, queryConfig) { webSocketURL }); } + +export function cancelRun({ name, namespace }) { + const payload = [ + { op: 'replace', path: '/spec/status', value: 'RunCancelled' } + ]; + + const uri = getTektonAPI('runs', { name, namespace, version: 'v1alpha1' }); + return patch(uri, payload); +} + +export function rerunRun(run) { + const { annotations, labels, name, namespace } = run.metadata; + + const payload = deepClone(run); + payload.metadata = { + annotations, + generateName: getGenerateNamePrefixForRerun(name), + labels: { + ...labels, + 'dashboard.tekton.dev/rerunOf': name + }, + namespace + }; + + delete payload.status; + delete payload.spec?.status; + + const uri = getTektonAPI('runs', { namespace, version: 'v1alpha1' }); + return post(uri, payload).then(({ body }) => body); +} diff --git a/src/api/runs.test.js b/src/api/runs.test.js index dc520cea4..ce5380f16 100644 --- a/src/api/runs.test.js +++ b/src/api/runs.test.js @@ -16,6 +16,23 @@ import fetchMock from 'fetch-mock'; import * as API from './runs'; import * as utils from './utils'; +it('cancelRun', () => { + const name = 'foo'; + const namespace = 'foospace'; + const returnedRun = { fake: 'Run' }; + const payload = [ + { op: 'replace', path: '/spec/status', value: 'RunCancelled' } + ]; + fetchMock.patch(`end:${name}`, returnedRun); + return API.cancelRun({ name, namespace }).then(response => { + expect(fetchMock.lastOptions()).toMatchObject({ + body: JSON.stringify(payload) + }); + expect(response).toEqual(returnedRun); + fetchMock.restore(); + }); +}); + it('deleteRun', () => { const name = 'foo'; const data = { fake: 'Run' }; @@ -85,3 +102,24 @@ it('useRun', () => { }) ); }); + +it('rerunRun', () => { + const filter = 'end:/runs/'; + const originalRun = { + metadata: { name: 'fake_run' }, + spec: { status: 'fake_status' }, + status: 'fake_status' + }; + const newRun = { metadata: { name: 'fake_run_rerun' } }; + fetchMock.post(filter, { body: newRun, status: 201 }); + return API.rerunRun(originalRun).then(data => { + const body = JSON.parse(fetchMock.lastCall(filter)[1].body); + expect(body.metadata.generateName).toMatch( + new RegExp(originalRun.metadata.name) + ); + expect(body.status).toBeUndefined(); + expect(body.spec.status).toBeUndefined(); + expect(data).toEqual(newRun); + fetchMock.restore(); + }); +}); diff --git a/src/containers/Run/Run.js b/src/containers/Run/Run.js index 6c73d83cc..9f5a3c36a 100644 --- a/src/containers/Run/Run.js +++ b/src/containers/Run/Run.js @@ -58,7 +58,12 @@ function getRunStatusIcon(run) { } const { reason, status } = getStatus(run); return ( - + ); } diff --git a/src/containers/Runs/Runs.js b/src/containers/Runs/Runs.js index af51ed4f4..1a5f53368 100644 --- a/src/containers/Runs/Runs.js +++ b/src/containers/Runs/Runs.js @@ -43,7 +43,9 @@ import { import { ListPageLayout } from '..'; import { + cancelRun, deleteRun, + rerunRun, useIsReadOnly, useRuns, useSelectedNamespace @@ -85,7 +87,12 @@ function getRunStatus(run) { function getRunStatusIcon(run) { const { reason, status } = getStatus(run); return ( - + ); } @@ -169,6 +176,17 @@ function Runs({ intl }) { setToBeDeleted([]); } + function cancel(run) { + cancelRun({ + name: run.metadata.name, + namespace: run.metadata.namespace + }); + } + + function rerun(run) { + rerunRun(run); + } + function deleteResource(run) { const { name, namespace: resourceNamespace } = run.metadata; return deleteRun({ name, namespace: resourceNamespace }).catch(err => { @@ -196,7 +214,44 @@ function Runs({ intl }) { } return [ - // TODO: rerun? + { + action: rerun, + actionText: intl.formatMessage({ + id: 'dashboard.rerun.actionText', + defaultMessage: 'Rerun' + }), + disable: resource => !!resource.metadata.labels?.['tekton.dev/pipeline'] + }, + { + actionText: intl.formatMessage({ + id: 'dashboard.cancelTaskRun.actionText', + defaultMessage: 'Stop' + }), + action: cancel, + disable: resource => { + const { status } = getStatus(resource); + return status && status !== 'Unknown'; + }, + modalProperties: { + heading: intl.formatMessage({ + id: 'dashboard.cancelRun.heading', + defaultMessage: 'Stop Run' + }), + primaryButtonText: intl.formatMessage({ + id: 'dashboard.cancelRun.primaryText', + defaultMessage: 'Stop Run' + }), + body: resource => + intl.formatMessage( + { + id: 'dashboard.cancelRun.body', + defaultMessage: + 'Are you sure you would like to stop Run {name}?' + }, + { name: resource.metadata.name } + ) + } + }, { actionText: intl.formatMessage({ id: 'dashboard.actions.deleteButton', @@ -208,6 +263,7 @@ function Runs({ intl }) { const { reason, status } = getStatus(resource); return isRunning(reason, status); }, + hasDivider: true, modalProperties: { danger: true, heading: intl.formatMessage( diff --git a/src/nls/messages_de.json b/src/nls/messages_de.json index c21b4517a..c135ff878 100644 --- a/src/nls/messages_de.json +++ b/src/nls/messages_de.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "PipelineRun stoppen", "dashboard.cancelPipelineRun.primaryText": "PipelineRun stoppen", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "", diff --git a/src/nls/messages_en.json b/src/nls/messages_en.json index 7f5c8df2c..492c2ad51 100644 --- a/src/nls/messages_en.json +++ b/src/nls/messages_en.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "Stop PipelineRun", "dashboard.cancelPipelineRun.primaryText": "Stop PipelineRun", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "Allow any currently executing tasks to complete but do not schedule any new non-finally tasks, then execute finally tasks", + "dashboard.cancelRun.body": "Are you sure you would like to stop Run {name}?", + "dashboard.cancelRun.heading": "Stop Run", + "dashboard.cancelRun.primaryText": "Stop Run", "dashboard.cancelTaskRun.actionText": "Stop", "dashboard.cancelTaskRun.body": "Are you sure you would like to stop TaskRun {name}?", "dashboard.cancelTaskRun.heading": "Stop TaskRun", diff --git a/src/nls/messages_es.json b/src/nls/messages_es.json index a8d95c68d..48c6198d6 100644 --- a/src/nls/messages_es.json +++ b/src/nls/messages_es.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "Detener PipelineRun", "dashboard.cancelPipelineRun.primaryText": "Detener PipelineRun", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "", diff --git a/src/nls/messages_fr.json b/src/nls/messages_fr.json index 036329eee..5c9819d02 100644 --- a/src/nls/messages_fr.json +++ b/src/nls/messages_fr.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "Arrêt de PipelineRun", "dashboard.cancelPipelineRun.primaryText": "Arrêter PipelineRun", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "", diff --git a/src/nls/messages_it.json b/src/nls/messages_it.json index ab74cdf0f..3d72c7cec 100644 --- a/src/nls/messages_it.json +++ b/src/nls/messages_it.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "Arresta esecuzione pipeline", "dashboard.cancelPipelineRun.primaryText": "Arresta esecuzione pipeline", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "", diff --git a/src/nls/messages_ja.json b/src/nls/messages_ja.json index 6608898e3..ad3424b22 100644 --- a/src/nls/messages_ja.json +++ b/src/nls/messages_ja.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "PipelineRunを停止", "dashboard.cancelPipelineRun.primaryText": "PipelineRunを停止", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "停止", "dashboard.cancelTaskRun.body": "TaskRun {name}を停止してもよろしいですか?", "dashboard.cancelTaskRun.heading": "TaskRunを停止", diff --git a/src/nls/messages_ko.json b/src/nls/messages_ko.json index 1fddfd29b..03dff12a6 100644 --- a/src/nls/messages_ko.json +++ b/src/nls/messages_ko.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "PipelineRun 중지", "dashboard.cancelPipelineRun.primaryText": "PipelineRun 중지", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "", diff --git a/src/nls/messages_pt.json b/src/nls/messages_pt.json index 5b2952c99..3357ca909 100644 --- a/src/nls/messages_pt.json +++ b/src/nls/messages_pt.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "Parar o PipelineRun", "dashboard.cancelPipelineRun.primaryText": "Parar o PipelineRun", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "", diff --git a/src/nls/messages_zh-Hans.json b/src/nls/messages_zh-Hans.json index ca6936b30..e6b5d7df1 100644 --- a/src/nls/messages_zh-Hans.json +++ b/src/nls/messages_zh-Hans.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "停止 PipelineRun", "dashboard.cancelPipelineRun.primaryText": "停止 PipelineRun", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "停止", "dashboard.cancelTaskRun.body": "您确定要停止 TaskRun {name} 吗", "dashboard.cancelTaskRun.heading": "停止 TaskRun", diff --git a/src/nls/messages_zh-Hant.json b/src/nls/messages_zh-Hant.json index 3a1cce1d8..c22d776ad 100644 --- a/src/nls/messages_zh-Hant.json +++ b/src/nls/messages_zh-Hant.json @@ -36,6 +36,9 @@ "dashboard.cancelPipelineRun.heading": "停止 PipelineRun", "dashboard.cancelPipelineRun.primaryText": "停止 PipelineRun", "dashboard.cancelPipelineRun.stoppedRunFinally.description": "", + "dashboard.cancelRun.body": "", + "dashboard.cancelRun.heading": "", + "dashboard.cancelRun.primaryText": "", "dashboard.cancelTaskRun.actionText": "", "dashboard.cancelTaskRun.body": "", "dashboard.cancelTaskRun.heading": "",