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(