diff --git a/base/200-clusterrole-tenant.yaml b/base/200-clusterrole-tenant.yaml index fb209bf74..66d338130 100644 --- a/base/200-clusterrole-tenant.yaml +++ b/base/200-clusterrole-tenant.yaml @@ -48,7 +48,7 @@ rules: - taskruns - pipelines - pipelineruns - - runs + - customruns verbs: - get - list diff --git a/overlays/patches/read-write/clusterrole-tenant-patch.yaml b/overlays/patches/read-write/clusterrole-tenant-patch.yaml index 69e9aee87..9cc35435c 100644 --- a/overlays/patches/read-write/clusterrole-tenant-patch.yaml +++ b/overlays/patches/read-write/clusterrole-tenant-patch.yaml @@ -23,7 +23,7 @@ - taskruns - pipelines - pipelineruns - - runs + - customruns verbs: - create - update diff --git a/packages/e2e/cypress/fixtures/kinds.json b/packages/e2e/cypress/fixtures/kinds.json index 763576a68..d4207a1aa 100644 --- a/packages/e2e/cypress/fixtures/kinds.json +++ b/packages/e2e/cypress/fixtures/kinds.json @@ -4,7 +4,7 @@ { "label": "Tasks", "path": "/tasks" }, { "label": "ClusterTasks", "path": "/clustertasks" }, { "label": "TaskRuns", "path": "/taskruns" }, - { "label": "Runs", "path": "/runs" }, + { "label": "CustomRuns", "path": "/customruns" }, { "label": "EventListeners", "path": "/eventlisteners" }, { "label": "Triggers", "path": "/triggers" }, { "label": "TriggerBindings", "path": "/triggerbindings" }, diff --git a/packages/utils/src/utils/router.js b/packages/utils/src/utils/router.js index 5b1bc1093..7a6abd4f6 100644 --- a/packages/utils/src/utils/router.js +++ b/packages/utils/src/utils/router.js @@ -42,6 +42,17 @@ export const paths = { return '/clustertriggerbindings/:clusterTriggerBindingName'; } }, + customRuns: { + all() { + return '/customruns'; + }, + byName() { + return byNamespace({ path: '/customruns/:runName' }); + }, + byNamespace() { + return byNamespace({ path: '/customruns' }); + } + }, eventListeners: { all() { return '/eventlisteners'; @@ -129,17 +140,6 @@ export const paths = { return '/:type/:name'; } }, - runs: { - all() { - return '/runs'; - }, - byName() { - return byNamespace({ path: '/runs/:runName' }); - }, - byNamespace() { - return byNamespace({ path: '/runs' }); - } - }, settings() { return '/settings'; }, diff --git a/packages/utils/src/utils/router.test.js b/packages/utils/src/utils/router.test.js index 443899ea9..470f0ad6e 100644 --- a/packages/utils/src/utils/router.test.js +++ b/packages/utils/src/utils/router.test.js @@ -252,14 +252,14 @@ describe('rawCRD', () => { }); }); -describe('runs', () => { +describe('customRuns', () => { it('all', () => { - expect(urls.runs.all()).toEqual(generatePath(paths.runs.all())); + expect(urls.customRuns.all()).toEqual(generatePath(paths.customRuns.all())); }); it('byName', () => { - expect(urls.runs.byName({ namespace, runName })).toEqual( - generatePath(paths.runs.byName(), { + expect(urls.customRuns.byName({ namespace, runName })).toEqual( + generatePath(paths.customRuns.byName(), { namespace, runName }) @@ -267,8 +267,8 @@ describe('runs', () => { }); it('byNamespace', () => { - expect(urls.runs.byNamespace({ namespace })).toEqual( - generatePath(paths.runs.byNamespace(), { namespace }) + expect(urls.customRuns.byNamespace({ namespace })).toEqual( + generatePath(paths.customRuns.byNamespace(), { namespace }) ); }); }); diff --git a/src/api/runs.js b/src/api/customRuns.js similarity index 57% rename from src/api/runs.js rename to src/api/customRuns.js index 0af4d4180..f9f8a9405 100644 --- a/src/api/runs.js +++ b/src/api/customRuns.js @@ -1,5 +1,5 @@ /* -Copyright 2022 The Tekton Authors +Copyright 2022-2023 The Tekton Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -22,60 +22,72 @@ import { useResource } from './utils'; -export function deleteRun({ name, namespace }) { - const uri = getTektonAPI('runs', { name, namespace, version: 'v1alpha1' }); +export function deleteCustomRun({ name, namespace }) { + const uri = getTektonAPI('customruns', { + name, + namespace, + version: 'v1beta1' + }); return deleteRequest(uri); } -function getRunsAPI({ filters, isWebSocket, name, namespace }) { +function getCustomRunsAPI({ filters, isWebSocket, name, namespace }) { return getTektonAPI( - 'runs', - { isWebSocket, namespace, version: 'v1alpha1' }, + 'customruns', + { isWebSocket, namespace, version: 'v1beta1' }, getQueryParams({ filters, name }) ); } -export function getRuns({ filters = [], namespace } = {}) { - const uri = getRunsAPI({ filters, namespace }); +export function getCustomRuns({ filters = [], namespace } = {}) { + const uri = getCustomRunsAPI({ filters, namespace }); return get(uri); } -export function getRun({ name, namespace }) { - const uri = getTektonAPI('runs', { name, namespace, version: 'v1alpha1' }); +export function getCustomRun({ name, namespace }) { + const uri = getTektonAPI('customruns', { + name, + namespace, + version: 'v1beta1' + }); return get(uri); } -export function useRuns(params) { - const webSocketURL = getRunsAPI({ ...params, isWebSocket: true }); +export function useCustomRuns(params) { + const webSocketURL = getCustomRunsAPI({ ...params, isWebSocket: true }); return useCollection({ - api: getRuns, - kind: 'Run', + api: getCustomRuns, + kind: 'CustomRun', params, webSocketURL }); } -export function useRun(params, queryConfig) { - const webSocketURL = getRunsAPI({ ...params, isWebSocket: true }); +export function useCustomRun(params, queryConfig) { + const webSocketURL = getCustomRunsAPI({ ...params, isWebSocket: true }); return useResource({ - api: getRun, - kind: 'Run', + api: getCustomRun, + kind: 'CustomRun', params, queryConfig, webSocketURL }); } -export function cancelRun({ name, namespace }) { +export function cancelCustomRun({ name, namespace }) { const payload = [ { op: 'replace', path: '/spec/status', value: 'RunCancelled' } ]; - const uri = getTektonAPI('runs', { name, namespace, version: 'v1alpha1' }); + const uri = getTektonAPI('customruns', { + name, + namespace, + version: 'v1beta1' + }); return patch(uri, payload); } -export function rerunRun(run) { +export function rerunCustomRun(run) { const { annotations, labels, name, namespace } = run.metadata; const payload = deepClone(run); @@ -92,6 +104,6 @@ export function rerunRun(run) { delete payload.status; delete payload.spec?.status; - const uri = getTektonAPI('runs', { namespace, version: 'v1alpha1' }); + const uri = getTektonAPI('customruns', { namespace, version: 'v1beta1' }); return post(uri, payload).then(({ body }) => body); } diff --git a/src/api/runs.test.js b/src/api/customRuns.test.js similarity index 73% rename from src/api/runs.test.js rename to src/api/customRuns.test.js index 6f001d177..f40ab3a1a 100644 --- a/src/api/runs.test.js +++ b/src/api/customRuns.test.js @@ -11,12 +11,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as API from './runs'; +import * as API from './customRuns'; import * as utils from './utils'; import * as comms from './comms'; import { rest, server } from '../../config_frontend/msw'; -it('cancelRun', () => { +it('cancelCustomRun', () => { const name = 'foo'; const namespace = 'foospace'; const payload = [ @@ -25,84 +25,86 @@ it('cancelRun', () => { jest .spyOn(comms, 'patch') .mockImplementation((uri, body) => Promise.resolve(body)); - return API.cancelRun({ name, namespace }).then(() => { + return API.cancelCustomRun({ name, namespace }).then(() => { expect(comms.patch).toHaveBeenCalled(); expect(comms.patch.mock.lastCall[1]).toEqual(payload); }); }); -it('deleteRun', () => { +it('deleteCustomRun', () => { const name = 'foo'; - const data = { fake: 'Run' }; + const data = { fake: 'CustomRun' }; server.use( rest.delete(new RegExp(`/${name}$`), (req, res, ctx) => res(ctx.json(data))) ); - return API.deleteRun({ name }).then(run => { + return API.deleteCustomRun({ name }).then(run => { expect(run).toEqual(data); }); }); -it('getRun', () => { +it('getCustomRun', () => { const name = 'foo'; - const data = { fake: 'Run' }; + const data = { fake: 'CustomRun' }; server.use( rest.get(new RegExp(`/${name}$`), (req, res, ctx) => res(ctx.json(data))) ); - return API.getRun({ name }).then(run => { + return API.getCustomRun({ name }).then(run => { expect(run).toEqual(data); }); }); -it('getRuns', () => { +it('getCustomRuns', () => { const data = { items: 'Runs' }; - server.use(rest.get(/\/runs\//, (req, res, ctx) => res(ctx.json(data)))); - return API.getRuns({ filters: [] }).then(runs => { + server.use( + rest.get(/\/customruns\//, (req, res, ctx) => res(ctx.json(data))) + ); + return API.getCustomRuns({ filters: [] }).then(runs => { expect(runs).toEqual(data); }); }); -it('useRuns', () => { +it('useCustomRuns', () => { const query = { fake: 'query' }; const params = { fake: 'params' }; jest.spyOn(utils, 'useCollection').mockImplementation(() => query); - expect(API.useRuns(params)).toEqual(query); + expect(API.useCustomRuns(params)).toEqual(query); expect(utils.useCollection).toHaveBeenCalledWith( expect.objectContaining({ - api: API.getRuns, - kind: 'Run', + api: API.getCustomRuns, + kind: 'CustomRun', params }) ); }); -it('useRun', () => { +it('useCustomRun', () => { const query = { fake: 'query' }; const params = { fake: 'params' }; jest.spyOn(utils, 'useResource').mockImplementation(() => query); - expect(API.useRun(params)).toEqual(query); + expect(API.useCustomRun(params)).toEqual(query); expect(utils.useResource).toHaveBeenCalledWith( expect.objectContaining({ - api: API.getRun, - kind: 'Run', + api: API.getCustomRun, + kind: 'CustomRun', params }) ); const queryConfig = { fake: 'queryConfig' }; - API.useRun(params, queryConfig); + API.useCustomRun(params, queryConfig); expect(utils.useResource).toHaveBeenCalledWith( expect.objectContaining({ - api: API.getRun, - kind: 'Run', + api: API.getCustomRun, + kind: 'CustomRun', params, queryConfig }) ); }); -it('rerunRun', () => { +it('rerunCustomRun', () => { const originalRun = { metadata: { name: 'fake_run' }, spec: { status: 'fake_status' }, @@ -112,7 +114,7 @@ it('rerunRun', () => { .spyOn(comms, 'post') .mockImplementation((uri, body) => Promise.resolve(body)); - return API.rerunRun(originalRun).then(() => { + return API.rerunCustomRun(originalRun).then(() => { expect(comms.post).toHaveBeenCalled(); const sentBody = comms.post.mock.lastCall[1]; const { metadata, spec, status } = sentBody; diff --git a/src/api/index.js b/src/api/index.js index b9c22b028..caa1108fc 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -36,12 +36,12 @@ export { NamespaceContext, useSelectedNamespace } from './utils'; export * from './clusterInterceptors'; export * from './clusterTasks'; export * from './clusterTriggerBindings'; +export * from './customRuns'; export * from './eventListeners'; export * from './extensions'; export * from './interceptors'; export * from './pipelineRuns'; export * from './pipelines'; -export * from './runs'; export * from './serviceAccounts'; export * from './taskRuns'; export * from './tasks'; diff --git a/src/containers/App/App.js b/src/containers/App/App.js index 955c01a8c..748d1efc0 100644 --- a/src/containers/App/App.js +++ b/src/containers/App/App.js @@ -47,6 +47,8 @@ import { CreatePipelineRun, CreateTaskRun, CustomResourceDefinition, + CustomRun, + CustomRuns, EventListener, EventListeners, Extension, @@ -61,8 +63,6 @@ import { Pipelines, ReadWriteRoute, ResourceList, - Run, - Runs, Settings, SideNav, TaskRun, @@ -327,19 +327,19 @@ export function App({ lang }) { - + - + - + - + - + - + diff --git a/src/containers/Run/Run.js b/src/containers/CustomRun/CustomRun.js similarity index 93% rename from src/containers/Run/Run.js rename to src/containers/CustomRun/CustomRun.js index 65c6c0ba8..ca9be8102 100644 --- a/src/containers/Run/Run.js +++ b/src/containers/CustomRun/CustomRun.js @@ -32,7 +32,12 @@ import { import { getStatus, urls, useTitleSync } from '@tektoncd/dashboard-utils'; import { InlineNotification } from 'carbon-components-react'; -import { deleteRun, rerunRun, useIsReadOnly, useRun } from '../../api'; +import { + deleteCustomRun, + rerunCustomRun, + useCustomRun, + useIsReadOnly +} from '../../api'; import { getViewChangeHandler } from '../../utils'; import { NotFound } from '..'; @@ -88,7 +93,7 @@ function getRunStatusTooltip(run) { return `${reason} - ${message}`; } -function Run() { +function CustomRun() { const intl = useIntl(); const location = useLocation(); const navigate = useNavigate(); @@ -101,7 +106,7 @@ function Run() { const isReadOnly = useIsReadOnly(); useTitleSync({ - page: 'Run', + page: 'CustomRun', resourceName }); @@ -109,12 +114,12 @@ function Run() { data: run, error, isFetching - } = useRun({ + } = useCustomRun({ name: resourceName, namespace }); - const additionalFields = run?.spec?.ref || run?.spec?.spec || {}; + const additionalFields = run?.spec?.customRef || run?.spec?.customSpec || {}; const { apiVersion, kind, name: customTaskName } = additionalFields; const headersForParameters = [ @@ -142,12 +147,12 @@ function Run() { })) || []; function deleteResource() { - deleteRun({ + deleteCustomRun({ name: run.metadata.name, namespace: run.metadata.namespace }) .then(() => { - navigate(urls.runs.byNamespace({ namespace })); + navigate(urls.customRuns.byNamespace({ namespace })); }) .catch(err => { err.response.text().then(text => { @@ -165,11 +170,11 @@ function Run() { } function rerun() { - rerunRun(run) + rerunCustomRun(run) .then(newRun => { setShowNotification({ kind: 'success', - logsURL: urls.runs.byName({ + logsURL: urls.customRuns.byName({ namespace, runName: newRun.metadata.name }), @@ -229,7 +234,7 @@ function Run() { id: 'dashboard.deleteResources.heading', defaultMessage: 'Delete {kind}' }, - { kind: 'Run' } + { kind: 'CustomRun' } ), primaryButtonText: intl.formatMessage({ id: 'dashboard.actions.deleteButton', @@ -238,9 +243,9 @@ function Run() { body: resource => intl.formatMessage( { - id: 'dashboard.deleteRun.body', + id: 'dashboard.deleteCustomRun.body', defaultMessage: - 'Are you sure you would like to delete Run {name}?' + 'Are you sure you would like to delete CustomRun {name}?' }, { name: resource.metadata.name } ) @@ -254,8 +259,8 @@ function Run() { @@ -379,4 +384,4 @@ function Run() { ); } -export default Run; +export default CustomRun; diff --git a/src/containers/Runs/index.js b/src/containers/CustomRun/index.js similarity index 87% rename from src/containers/Runs/index.js rename to src/containers/CustomRun/index.js index 0c48500d9..d986e1ae1 100644 --- a/src/containers/Runs/index.js +++ b/src/containers/CustomRun/index.js @@ -1,5 +1,5 @@ /* -Copyright 2022 The Tekton Authors +Copyright 2022-2023 The Tekton Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -11,4 +11,4 @@ See the License for the specific language governing permissions and limitations under the License. */ /* istanbul ignore file */ -export { default } from './Runs'; +export { default } from './CustomRun'; diff --git a/src/containers/Runs/Runs.js b/src/containers/CustomRuns/CustomRuns.js similarity index 88% rename from src/containers/Runs/Runs.js rename to src/containers/CustomRuns/CustomRuns.js index 6ca4562a5..699244399 100644 --- a/src/containers/Runs/Runs.js +++ b/src/containers/CustomRuns/CustomRuns.js @@ -1,5 +1,5 @@ /* -Copyright 2022 The Tekton Authors +Copyright 2022-2023 The Tekton Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -43,14 +43,14 @@ import { import { ListPageLayout } from '..'; import { - cancelRun, - deleteRun, - rerunRun, + cancelCustomRun, + deleteCustomRun, + rerunCustomRun, + useCustomRuns, useIsReadOnly, - useRuns, useSelectedNamespace } from '../../api'; -import { sortRunsByStartTime } from '../../utils'; +import { sortRunsByCreationTime } from '../../utils'; function getRunTriggerInfo(run) { const { labels = {} } = run.metadata; @@ -105,13 +105,13 @@ function getRunStatusTooltip(run) { return `${reason}: ${message}`; } -function Runs() { +function CustomRuns() { const intl = useIntl(); const location = useLocation(); const params = useParams(); const filters = getFilters(location); - useTitleSync({ page: 'Runs' }); + useTitleSync({ page: 'CustomRuns' }); const { selectedNamespace } = useSelectedNamespace(); const { namespace = selectedNamespace } = params; @@ -133,20 +133,20 @@ function Runs() { data: runs = [], error, isLoading - } = useRuns({ + } = useCustomRuns({ filters, namespace }); - sortRunsByStartTime(runs); + sortRunsByCreationTime(runs); function getError() { if (error) { return { error, title: intl.formatMessage({ - id: 'dashboard.runs.error', - defaultMessage: 'Error loading Runs' + id: 'dashboard.customRuns.error', + defaultMessage: 'Error loading CustomRuns' }) }; } @@ -178,28 +178,30 @@ function Runs() { } function cancel(run) { - cancelRun({ + cancelCustomRun({ name: run.metadata.name, namespace: run.metadata.namespace }); } function rerun(run) { - rerunRun(run); + rerunCustomRun(run); } function deleteResource(run) { const { name, namespace: resourceNamespace } = run.metadata; - return deleteRun({ name, namespace: resourceNamespace }).catch(err => { - err.response.text().then(text => { - const statusCode = err.response.status; - let errorMessage = `error code ${statusCode}`; - if (text) { - errorMessage = `${text} (error code ${statusCode})`; - } - setDeleteError(errorMessage); - }); - }); + return deleteCustomRun({ name, namespace: resourceNamespace }).catch( + err => { + err.response.text().then(text => { + const statusCode = err.response.status; + let errorMessage = `error code ${statusCode}`; + if (text) { + errorMessage = `${text} (error code ${statusCode})`; + } + setDeleteError(errorMessage); + }); + } + ); } async function handleDelete() { @@ -235,19 +237,19 @@ function Runs() { }, modalProperties: { heading: intl.formatMessage({ - id: 'dashboard.cancelRun.heading', - defaultMessage: 'Stop Run' + id: 'dashboard.cancelCustomRun.heading', + defaultMessage: 'Stop CustomRun' }), primaryButtonText: intl.formatMessage({ - id: 'dashboard.cancelRun.primaryText', - defaultMessage: 'Stop Run' + id: 'dashboard.cancelCustomRun.primaryText', + defaultMessage: 'Stop CustomRun' }), body: resource => intl.formatMessage( { - id: 'dashboard.cancelRun.body', + id: 'dashboard.cancelCustomRun.body', defaultMessage: - 'Are you sure you would like to stop Run {name}?' + 'Are you sure you would like to stop CustomRun {name}?' }, { name: resource.metadata.name } ) @@ -272,7 +274,7 @@ function Runs() { id: 'dashboard.deleteResources.heading', defaultMessage: 'Delete {kind}' }, - { kind: 'Runs' } + { kind: 'CustomRuns' } ), primaryButtonText: intl.formatMessage({ id: 'dashboard.actions.deleteButton', @@ -281,9 +283,9 @@ function Runs() { body: resource => intl.formatMessage( { - id: 'dashboard.deleteRun.body', + id: 'dashboard.deleteCustomRun.body', defaultMessage: - 'Are you sure you would like to delete Run {name}?' + 'Are you sure you would like to delete CustomRun {name}?' }, { name: resource.metadata.name } ) @@ -339,13 +341,14 @@ function Runs() { error={getError()} filters={filters} resources={runs} - title="Runs" + title="CustomRuns" > {({ resources }) => { const runsFormatted = resources.map(run => { const { creationTimestamp } = run.metadata; - const additionalFields = run?.spec?.ref || run?.spec?.spec || {}; + const additionalFields = + run?.spec?.customRef || run?.spec?.customSpec || {}; const { apiVersion, kind, name: customTaskName } = additionalFields; const customTaskTooltipParts = [ `${intl.formatMessage({ @@ -396,7 +399,7 @@ function Runs() { {showDeleteModal ? ( TaskRuns - - Runs + + CustomRuns {isTriggersInstalled && ( { + const aTime = (a.metadata || {}).creationTimestamp; + const bTime = (b.metadata || {}).creationTimestamp; + if (!aTime && !bTime) { + return 0; + } + if (!aTime) { + return -1; + } + if (!bTime) { + return 1; + } + return -1 * aTime.localeCompare(bTime); + }); +} + export async function followLogs(stepName, stepStatus, taskRun) { const { namespace } = taskRun.metadata; const { podName } = taskRun.status || {}; diff --git a/src/utils/index.test.js b/src/utils/index.test.js index d0d40ee41..fbc62b91c 100644 --- a/src/utils/index.test.js +++ b/src/utils/index.test.js @@ -1,5 +1,5 @@ /* -Copyright 2019-2022 The Tekton Authors +Copyright 2019-2023 The Tekton Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -25,6 +25,7 @@ import { getTheme, getViewChangeHandler, setTheme, + sortRunsByCreationTime, sortRunsByStartTime } from '.'; @@ -60,6 +61,36 @@ describe('sortRunsByStartTime', () => { }); }); +describe('sortRunsByCreationTime', () => { + it('should handle missing creation time or metadata', () => { + const a = { name: 'a', metadata: { creationTimestamp: '0' } }; + const b = { name: 'b', metadata: {} }; + const c = { name: 'c', metadata: { creationTimestamp: '2' } }; + const d = { name: 'd', metadata: { creationTimestamp: '1' } }; + const e = { name: 'e', metadata: {} }; + const f = { name: 'f', metadata: { creationTimestamp: '3' } }; + const g = { name: 'g' }; + + const runs = [a, b, c, d, e, f, g]; + /* + sort is stable on all modern browsers so + input order is preserved for b and e + */ + const sortedRuns = [b, e, g, f, c, d, a]; + sortRunsByCreationTime(runs); + expect(runs).toEqual(sortedRuns); + }); + + it('should leave the order unchanged if no creationTimestamps specified', () => { + const a = { name: 'a' }; + const b = { name: 'b' }; + const runs = [a, b]; + const sortedRuns = [a, b]; + sortRunsByCreationTime(runs); + expect(runs).toEqual(sortedRuns); + }); +}); + describe('fetchLogs', () => { it('should return the pod logs', () => { const namespace = 'default';