diff --git a/packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js b/packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js new file mode 100644 index 0000000000..0afe862a3e --- /dev/null +++ b/packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js @@ -0,0 +1,78 @@ +/* +Copyright 2022 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 + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const namespace = 'e2e-actions'; +describe('Edit and run Pipeline Run', () => { + before(() => { + cy.exec('kubectl version --client'); + cy.exec(`kubectl create namespace ${namespace} || true`); + }); + + after(() => { + cy.exec(`kubectl delete namespace ${namespace} || true`); + }); + + it('should create pipelinerun on edit and run', function () { + // given pipeline + const uniqueNumber = Date.now(); + const pipelineName = `sp-${uniqueNumber}`; + const pipeline = `apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: ${pipelineName} + namespace: ${namespace} +spec: + tasks: + - name: hello + taskSpec: + steps: + - name: echo + image: busybox + script: | + #!/bin/ash + echo "Hello World!" + `; + cy.exec(`echo "${pipeline}" | kubectl apply -f -`); + // when create first pipeline run + cy.visit( + `/#/pipelineruns/create?namespace=${namespace}&pipelineName=${pipelineName}` + ); + cy.get('[id=create-pipelinerun--namespaces-dropdown]').should( + 'have.value', + namespace + ); + cy.get('[id=create-pipelinerun--pipelines-dropdown]').should( + 'have.value', + pipelineName + ); + cy.contains('button', 'Create').click(); + + cy.get( + `td:has(.bx--link[title*=${pipelineName}-run]) + td:has(.tkn--status[data-reason=Succeeded])`, + { timeout: 15000 } + ).should('have.length', 1); + + // when edit and run to create second pipeline run + cy.contains(`${pipelineName}-run`).parent().click(); + cy.contains('button', 'Actions').click(); + cy.contains('button', 'Edit and run').click(); + cy.get('.cm-content').contains(`name: ${pipelineName}`); + cy.contains('button', 'Create').click(); + + // then + cy.get( + `td:has(.bx--link[title*=${pipelineName}-run]) + td:has(.tkn--status[data-reason=Succeeded])`, + { timeout: 15000 } + ).should('have.length', 2); + }); +}); diff --git a/packages/utils/src/utils/index.js b/packages/utils/src/utils/index.js index dc40740d45..66cf7ea43a 100644 --- a/packages/utils/src/utils/index.js +++ b/packages/utils/src/utils/index.js @@ -356,6 +356,14 @@ export function getGenerateNamePrefixForRerun(name) { return `${root}${rerunIdentifier}`; } +export function getGenerateNamePrefixForNewRun(name) { + let generateName = name; + if (!name.endsWith('-')) { + generateName = `${name}-`; + } + return `${generateName}`; +} + /* getParams required to support 3rd-party consumers of certain dashboard components (e.g. PipelineRun) while they migrate to the Tekton beta diff --git a/src/api/pipelineRuns.js b/src/api/pipelineRuns.js index bf7d0db77c..29f5859fa1 100644 --- a/src/api/pipelineRuns.js +++ b/src/api/pipelineRuns.js @@ -11,7 +11,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { getGenerateNamePrefixForRerun } from '@tektoncd/dashboard-utils'; +import { + getGenerateNamePrefixForNewRun, + getGenerateNamePrefixForRerun +} from '@tektoncd/dashboard-utils'; import deepClone from 'lodash.clonedeep'; import { deleteRequest, get, patch, post } from './comms'; @@ -170,30 +173,46 @@ export function createPipelineRun({ return post(uri, payload).then(({ body }) => body); } -export function rerunPipelineRun(pipelineRun) { - const { annotations, labels, name, namespace } = pipelineRun.metadata; +export function generateNewPipelineRunPayload(pipelineRun, rerun) { + const { annotations, labels, name, namespace, generateName } = + pipelineRun.metadata; const payload = deepClone(pipelineRun); payload.apiVersion = payload.apiVersion || `tekton.dev/${getTektonPipelinesAPIVersion()}`; payload.kind = payload.kind || 'PipelineRun'; + + function getGenerateName() { + if (rerun) { + return getGenerateNamePrefixForRerun(name); + } + + return generateName || getGenerateNamePrefixForNewRun(name); + } + payload.metadata = { - annotations: annotations || {}, - generateName: getGenerateNamePrefixForRerun(name), - labels: { - ...labels, - 'dashboard.tekton.dev/rerunOf': name - }, + annotations, + generateName: getGenerateName(), + labels, namespace }; + if (rerun) { + payload.metadata.labels = { + ...labels, + 'dashboard.tekton.dev/rerunOf': name + }; + } - Object.keys(payload.metadata.labels).forEach(label => { - if (label.startsWith('tekton.dev/')) { - delete payload.metadata.labels[label]; - } - }); + if (payload.metadata.labels) { + Object.keys(payload.metadata.labels).forEach(label => { + if (label.startsWith('tekton.dev/')) { + delete payload.metadata.labels[label]; + } + }); + } - /* + if (payload.metadata.annotations) { + /* This is used by Tekton Pipelines as part of the conversion between v1beta1 and v1 resources. Creating a run with this in place prevents it from actually executing and instead adopts the status of the original TaskRuns. @@ -204,14 +223,27 @@ export function rerunPipelineRun(pipelineRun) { When v1beta1 has been fully removed from Tekton Pipelines we can revisit this and remove all remaining `tekton.dev/*` annotations. - */ - delete payload.metadata.annotations['tekton.dev/v1beta1TaskRuns']; - delete payload.metadata.annotations[ - 'kubectl.kubernetes.io/last-applied-configuration' - ]; + */ + delete payload.metadata.annotations['tekton.dev/v1beta1TaskRuns']; + delete payload.metadata.annotations[ + 'kubectl.kubernetes.io/last-applied-configuration' + ]; + } + Object.keys(payload.metadata).forEach( + i => payload.metadata[i] === undefined && delete payload.metadata[i] + ); delete payload.status; + delete payload.spec?.status; + return { namespace, payload }; +} + +export function rerunPipelineRun(pipelineRun) { + const { namespace, payload } = generateNewPipelineRunPayload( + pipelineRun, + true + ); const uri = getTektonAPI('pipelineruns', { namespace }); return post(uri, payload).then(({ body }) => body); diff --git a/src/api/pipelineRuns.test.js b/src/api/pipelineRuns.test.js index 1be357ea6c..1eb805d810 100644 --- a/src/api/pipelineRuns.test.js +++ b/src/api/pipelineRuns.test.js @@ -11,9 +11,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +import yaml from 'js-yaml'; import * as API from './pipelineRuns'; import * as utils from './utils'; import { rest, server } from '../../config_frontend/msw'; +import { generateNewPipelineRunPayload } from './pipelineRuns'; it('cancelPipelineRun', () => { const name = 'foo'; @@ -326,3 +328,226 @@ it('startPipelineRun', () => { } ); }); + +it('generate new pipeline run minimum for rerun', () => { + const pr = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + name: 'test', + namespace: 'test-namespace' + }, + spec: { + pipelineRef: { + name: 'simple' + } + } + }; + const { namespace, payload } = generateNewPipelineRunPayload(pr, true); + const expected = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + generateName: test-r- + labels: + dashboard.tekton.dev/rerunOf: test + namespace: test-namespace +spec: + pipelineRef: + name: simple +`; + expect(namespace).toEqual('test-namespace'); + expect(yaml.dump(payload)).toEqual(expected); +}); + +it('generate new pipeline run maximum for rerun', () => { + const pr = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + name: 'test', + namespace: 'test-namespace', + annotations: { + keya: 'valuea', + 'kubectl.kubernetes.io/last-applied-configuration': + '{"apiVersion": "tekton.dev/v1beta1", "keya": "valuea"}' + }, + labels: { + keyl: 'valuel', + key2: 'value2', + 'tekton.dev/pipeline': 'foo' + }, + uid: '111-233-33', + resourceVersion: 'aaaa' + }, + spec: { + pipelineRef: { + name: 'simple' + }, + params: [{ name: 'param-1' }, { name: 'param-2' }] + }, + status: { startTime: '0' } + }; + const { namespace, payload } = generateNewPipelineRunPayload(pr, true); + const expected = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + annotations: + keya: valuea + generateName: test-r- + labels: + keyl: valuel + key2: value2 + dashboard.tekton.dev/rerunOf: test + namespace: test-namespace +spec: + pipelineRef: + name: simple + params: + - name: param-1 + - name: param-2 +`; + expect(namespace).toEqual('test-namespace'); + expect(yaml.dump(payload)).toEqual(expected); +}); + +it('generate new pipeline run minimum', () => { + const pr = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + name: 'test', + namespace: 'test-namespace' + }, + spec: { + pipelineRef: { + name: 'simple' + } + } + }; + const { namespace, payload } = generateNewPipelineRunPayload(pr, false); + const expected = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + generateName: test- + namespace: test-namespace +spec: + pipelineRef: + name: simple +`; + expect(namespace).toEqual('test-namespace'); + expect(yaml.dump(payload)).toEqual(expected); +}); +it('generate new pipeline run maximum', () => { + const pr = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + annotations: { + keya: 'valuea', + 'kubectl.kubernetes.io/last-applied-configuration': + '{"apiVersion": "tekton.dev/v1beta1", "keya": "valuea"}' + }, + labels: { + keyl: 'valuel', + key2: 'value2', + 'tekton.dev/pipeline': 'foo' + }, + name: 'test', + namespace: 'test-namespace', + uid: '111-233-33', + resourceVersion: 'aaaa' + }, + spec: { + pipelineRef: { + name: 'simple' + }, + params: [{ name: 'param-1' }, { name: 'param-2' }] + }, + status: { startTime: '0' } + }; + const { namespace, payload } = generateNewPipelineRunPayload(pr, false); + const expected = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + annotations: + keya: valuea + generateName: test- + labels: + keyl: valuel + key2: value2 + namespace: test-namespace +spec: + pipelineRef: + name: simple + params: + - name: param-1 + - name: param-2 +`; + expect(namespace).toEqual('test-namespace'); + expect(yaml.dump(payload)).toEqual(expected); +}); +it('generate new pipeline run last applied configuration should be removed', () => { + const pr = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + annotations: { + 'kubectl.kubernetes.io/last-applied-configuration': + '{"apiVersion": "tekton.dev/v1beta1", "keya": "valuea"}' + }, + name: 'test', + namespace: 'test-namespace' + }, + spec: { + pipelineRef: { + name: 'simple' + } + } + }; + const { namespace, payload } = generateNewPipelineRunPayload(pr, false); + const expected = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + annotations: {} + generateName: test- + namespace: test-namespace +spec: + pipelineRef: + name: simple +`; + expect(namespace).toEqual('test-namespace'); + expect(yaml.dump(payload)).toEqual(expected); +}); + +it('generate new pipeline run tekton.dev labels should be removed', () => { + const pr = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + name: 'test', + namespace: 'test-namespace', + labels: { + 'tekton.dev/pipeline': 'foo', + 'tekton.dev/run': 'foo' + } + }, + spec: { + pipelineRef: { + name: 'simple' + } + } + }; + const { namespace, payload } = generateNewPipelineRunPayload(pr, false); + const expected = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + generateName: test- + labels: {} + namespace: test-namespace +spec: + pipelineRef: + name: simple +`; + expect(namespace).toEqual('test-namespace'); + expect(yaml.dump(payload)).toEqual(expected); +}); diff --git a/src/containers/CreatePipelineRun/CreatePipelineRun.js b/src/containers/CreatePipelineRun/CreatePipelineRun.js index 3ca70e9fef..0bae70e01d 100644 --- a/src/containers/CreatePipelineRun/CreatePipelineRun.js +++ b/src/containers/CreatePipelineRun/CreatePipelineRun.js @@ -40,8 +40,10 @@ import { } from '..'; import { createPipelineRun, + generateNewPipelineRunPayload, getPipelineRunPayload, usePipeline, + usePipelineRun, useSelectedNamespace } from '../../api'; import { isValidLabel } from '../../utils'; @@ -56,7 +58,7 @@ const initialState = { nodeSelector: [], params: {}, paramSpecs: [], - pendingPipelineStatus: '', + pipelinePendingStatus: '', pipelineError: false, pipelineRef: '', pipelineRunName: '', @@ -91,6 +93,11 @@ function CreatePipelineRun() { return urlSearchParams.get('pipelineName') || ''; } + function getPipelineRunName() { + const urlSearchParams = new URLSearchParams(location.search); + return urlSearchParams.get('pipelineRunName') || ''; + } + function getNamespace() { const urlSearchParams = new URLSearchParams(location.search); return ( @@ -426,6 +433,36 @@ function CreatePipelineRun() { } if (isYAMLMode()) { + const externalPipelineRunName = getPipelineRunName(); + if (externalPipelineRunName) { + const { data: pipelineRunObject, isLoading } = usePipelineRun( + { + name: externalPipelineRunName, + namespace: getNamespace() + }, + { disableWebSocket: true } + ); + let payloadYaml = null; + if (pipelineRunObject) { + const { payload } = generateNewPipelineRunPayload( + pipelineRunObject, + false + ); + payloadYaml = yaml.dump(payload); + } + const loadingMessage = intl.formatMessage({ + id: 'dashboard.pipelineRun.loading', + defaultMessage: 'Loading PipelineRun…' + }); + + return ( + <CreateYAMLEditor + code={payloadYaml || ''} + loading={isLoading} + loadingMessage={loadingMessage} + /> + ); + } const pipelineRun = getPipelineRunPayload({ labels: labels.reduce((acc, { key, value }) => { acc[key] = value; diff --git a/src/containers/CreatePipelineRun/CreatePipelineRun.test.js b/src/containers/CreatePipelineRun/CreatePipelineRun.test.js index 272da4a9a4..5d95a327a8 100644 --- a/src/containers/CreatePipelineRun/CreatePipelineRun.test.js +++ b/src/containers/CreatePipelineRun/CreatePipelineRun.test.js @@ -12,7 +12,7 @@ limitations under the License. */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { renderWithRouter } from '../../utils/test'; @@ -23,6 +23,7 @@ import * as PipelineRunsAPI from '../../api/pipelineRuns'; import * as PipelinesAPI from '../../api/pipelines'; import * as ServiceAccountsAPI from '../../api/serviceAccounts'; +const submitButton = allByText => allByText('Create')[0]; const pipelines = [ { metadata: { @@ -110,14 +111,6 @@ describe('CreatePipelineRun', () => { expect(queryByDisplayValue(/bar/i)).toBeFalsy(); }); - it('renders yaml mode', () => { - const { getByRole } = renderWithRouter(<CreatePipelineRun />, { - path: '/create', - route: '/create?mode=yaml' - }); - expect(getByRole(/textbox/)).toBeTruthy(); - }); - it('handles onClose event', () => { jest.spyOn(window.history, 'pushState'); const { getByText } = renderWithRouter(<CreatePipelineRun />); @@ -133,3 +126,101 @@ describe('CreatePipelineRun', () => { expect(window.history.pushState).toHaveBeenCalledTimes(2); }); }); + +const pipelineRunRawGenerateName = { + apiVersion: 'tekton.dev/v1beta1', + kind: 'PipelineRun', + metadata: { + generateName: 'test-pipeline-run-name-', + namespace: 'test-namespace' + }, + spec: { + pipelineSpec: { + tasks: [ + { + name: 'hello', + taskSpec: { + steps: [ + { + image: 'busybox', + name: 'echo', + script: '#!/bin/ash\necho "Hello World!"\n' + } + ] + } + } + ] + } + } +}; + +const expectedPipelineRun = `apiVersion: tekton.dev/v1beta1 +kind: PipelineRun +metadata: + name: run-1111111111 + namespace: test-namespace + labels: {} +spec: + pipelineRef: + name: '' + status: ''`; +const expectedPipelineRunOneLine = expectedPipelineRun.replace(/\r?\n|\r/g, ''); +const findNameRegexp = /name: run-\S+/; +describe('CreatePipelineRun yaml mode', () => { + beforeEach(() => { + jest.clearAllMocks(); + + jest.spyOn(window.history, 'pushState'); + }); + + it('renders yaml mode with namespace', async () => { + jest + .spyOn(PipelineRunsAPI, 'createPipelineRunRaw') + .mockImplementation(() => Promise.resolve({ data: {} })); + jest + .spyOn(PipelineRunsAPI, 'usePipelineRun') + .mockImplementation(() => ({ data: pipelineRunRawGenerateName })); + + const { getByRole } = renderWithRouter(<CreatePipelineRun />, { + path: '/create', + route: '/create?mode=yaml&namespace=test-namespace' + }); + + expect(getByRole(/textbox/)).toBeTruthy(); + let actual = getByRole(/textbox/).textContent; + actual = actual.replace(findNameRegexp, 'name: run-1111111111'); + expect(actual.trim()).toEqual(expectedPipelineRunOneLine); + }); + + it('handle submit. yaml mode with pipelinerun and namespace', async () => { + jest + .spyOn(PipelineRunsAPI, 'createPipelineRunRaw') + .mockImplementation(() => Promise.resolve({ data: {} })); + jest + .spyOn(PipelineRunsAPI, 'usePipelineRun') + .mockImplementation(() => ({ data: pipelineRunRawGenerateName })); + + const { queryAllByText } = renderWithRouter(<CreatePipelineRun />, { + path: '/create', + route: + '/create?mode=yaml&pipelineRunName=test-pipeline-run-name&namespace=test-namespace' + }); + + expect(submitButton(queryAllByText)).toBeTruthy(); + + fireEvent.click(submitButton(queryAllByText)); + + await waitFor(() => { + expect(PipelineRunsAPI.createPipelineRunRaw).toHaveBeenCalledTimes(1); + }); + expect(PipelineRunsAPI.createPipelineRunRaw).toHaveBeenCalledWith( + expect.objectContaining({ + namespace: 'test-namespace', + payload: pipelineRunRawGenerateName + }) + ); + await waitFor(() => { + expect(window.history.pushState).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/containers/CreatePipelineRun/YAMLEditor.js b/src/containers/CreatePipelineRun/YAMLEditor.js index 88f481da43..19e2d4b877 100644 --- a/src/containers/CreatePipelineRun/YAMLEditor.js +++ b/src/containers/CreatePipelineRun/YAMLEditor.js @@ -16,29 +16,41 @@ import { Button, Form, FormGroup, - InlineNotification + InlineNotification, + Loading } from 'carbon-components-react'; import yaml from 'js-yaml'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import CodeMirror from '@uiw/react-codemirror'; import { StreamLanguage } from '@codemirror/language'; import { yaml as codeMirrorYAML } from '@codemirror/legacy-modes/mode/yaml'; import { useNavigate } from 'react-router-dom-v5-compat'; import { createPipelineRunRaw, useSelectedNamespace } from '../../api'; -export function CreateYAMLEditor({ code: initialCode = '' }) { +export function CreateYAMLEditor({ + code: initialCode, + loading = false, + loadingMessage = '' +}) { const intl = useIntl(); const navigate = useNavigate(); const { selectedNamespace } = useSelectedNamespace(); - const [{ code, isCreating, submitError, validationErrorMessage }, setState] = + const [{ isCreating, submitError, validationErrorMessage, code }, setState] = useState({ - code: initialCode, isCreating: false, submitError: '', - validationErrorMessage: '' + validationErrorMessage: '', + code: initialCode }); + useEffect(() => { + setState(state => ({ + ...state, + code: initialCode + })); + }, [loading]); + function validateNamespace(obj) { if (!obj?.metadata?.namespace) { return { @@ -176,14 +188,25 @@ export function CreateYAMLEditor({ code: initialCode = '' }) { lowContrast /> )} - <FormGroup legendText=""> - <CodeMirror - value={code} - height="800px" - theme="dark" - extensions={[StreamLanguage.define(codeMirrorYAML)]} - onChange={onChange} - /> + <FormGroup legendText="" className="tkn--codemirror--form"> + <> + {loading && ( + <div className="tkn--data-loading-overlay"> + <Loading description={loadingMessage} withOverlay={false} /> + <span className="tkn--data-loading-text">{loadingMessage}</span> + </div> + )} + {!loading && ( + <CodeMirror + value={code} + height="800px" + theme="dark" + extensions={[StreamLanguage.define(codeMirrorYAML)]} + onChange={onChange} + editable={!loading} + /> + )} + </> </FormGroup> <Button iconDescription={intl.formatMessage({ @@ -191,7 +214,7 @@ export function CreateYAMLEditor({ code: initialCode = '' }) { defaultMessage: 'Create' })} onClick={handleSubmit} - disabled={isCreating} + disabled={isCreating || loading} > {intl.formatMessage({ id: 'dashboard.actions.createButton', diff --git a/src/containers/CreatePipelineRun/YAMLEditor.test.js b/src/containers/CreatePipelineRun/YAMLEditor.test.js index 10516cf07c..5bb3b5888f 100644 --- a/src/containers/CreatePipelineRun/YAMLEditor.test.js +++ b/src/containers/CreatePipelineRun/YAMLEditor.test.js @@ -19,6 +19,7 @@ import { CreateYAMLEditor } from './YAMLEditor'; import * as PipelineRunsAPI from '../../api/pipelineRuns'; const submitButton = allByText => allByText('Create')[0]; +const cancelButton = allByText => allByText('Cancel')[0]; const pipelineRun = ` apiVersion: tekton.dev/v1beta1 kind: PipelineRun @@ -211,4 +212,17 @@ describe('YAMLEditor', () => { expect(getByText(/Error creating PipelineRun/)).toBeTruthy(); }); }); + + it('during loading loading message should be displayed', () => { + const { getAllByText } = renderWithRouter( + <CreateYAMLEditor loading loadingMessage="wait. test is in progress" /> + ); + expect(getAllByText(/wait. test is in progress/)).toBeTruthy(); + }); + + it('during loading button create should be disabled and button cancel should not', () => { + const { queryAllByText } = renderWithRouter(<CreateYAMLEditor loading />); + expect(submitButton(queryAllByText).disabled).toBe(true); + expect(cancelButton(queryAllByText).disabled).toBe(false); + }); }); diff --git a/src/containers/PipelineRun/PipelineRun.js b/src/containers/PipelineRun/PipelineRun.js index 239253babe..99c59e4078 100644 --- a/src/containers/PipelineRun/PipelineRun.js +++ b/src/containers/PipelineRun/PipelineRun.js @@ -311,6 +311,14 @@ export /* istanbul ignore next */ function PipelineRunContainer() { }); } + function editAndRun() { + navigate( + `${urls.pipelineRuns.create()}?mode=yaml&pipelineRunName=${ + pipelineRun.metadata.name + }&namespace=${pipelineRun.metadata.namespace}` + ); + } + function pipelineRunActions() { if (isReadOnly) { return []; @@ -328,6 +336,13 @@ export /* istanbul ignore next */ function PipelineRunContainer() { return isPending(reason, status); } }, + { + actionText: intl.formatMessage({ + id: 'dashboard.editAndRun.actionText', + defaultMessage: 'Edit and run' + }), + action: editAndRun + }, { actionText: intl.formatMessage({ id: 'dashboard.startPipelineRun.actionText', diff --git a/src/containers/PipelineRuns/PipelineRuns.js b/src/containers/PipelineRuns/PipelineRuns.js index ef4ed82ec3..b5acbdaca7 100644 --- a/src/containers/PipelineRuns/PipelineRuns.js +++ b/src/containers/PipelineRuns/PipelineRuns.js @@ -172,6 +172,14 @@ export function PipelineRuns() { }); } + function editAndRun(pipelineRun) { + navigate( + `${urls.pipelineRuns.create()}?mode=yaml&pipelineRunName=${ + pipelineRun.metadata.name + }&namespace=${pipelineRun.metadata.namespace}` + ); + } + async function handleDelete() { const deletions = toBeDeleted.map(resource => deleteRun(resource)); closeDeleteModal(); @@ -206,6 +214,13 @@ export function PipelineRuns() { return isPending(reason, status); } }, + { + actionText: intl.formatMessage({ + id: 'dashboard.editAndRun.actionText', + defaultMessage: 'Edit and run' + }), + action: editAndRun + }, { actionText: intl.formatMessage({ id: 'dashboard.startPipelineRun.actionText', diff --git a/src/nls/messages_de.json b/src/nls/messages_de.json index 848946c3fa..c0e38dcb59 100644 --- a/src/nls/messages_de.json +++ b/src/nls/messages_de.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "{kind} löschen", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "{kind} nicht gefunden", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "{kind} nicht gefunden", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "Fehler beim Laden von PipelineRun", "dashboard.pipelineRun.failed": "PipelineRun kann nicht geladen werden", "dashboard.pipelineRun.failedMessage": "Details der PipelineRun können nicht geladen werden: {reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "Keine Protokollausgabe", "dashboard.pipelineRun.logFailed": "Das Protokoll kann nicht abgerufen werden", "dashboard.pipelineRun.notFound": "PipelineRun nicht gefunden", diff --git a/src/nls/messages_en.json b/src/nls/messages_en.json index d547b09c30..c7544efd37 100644 --- a/src/nls/messages_en.json +++ b/src/nls/messages_en.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "Delete {kind}", "dashboard.deleteRun.body": "Are you sure you would like to delete Run {name}?", "dashboard.deleteTaskRun.body": "Are you sure you would like to delete TaskRun {name}?", + "dashboard.editAndRun.actionText": "Edit and run", "dashboard.emptyState.allNamespaces": "No matching {kind} found", "dashboard.emptyState.clusterResource": "No matching {kind} found", "dashboard.emptyState.selectedNamespace": "No matching {kind} found in namespace {selectedNamespace}", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "Error loading PipelineRun", "dashboard.pipelineRun.failed": "Cannot load PipelineRun", "dashboard.pipelineRun.failedMessage": "Unable to load PipelineRun: {reason}", + "dashboard.pipelineRun.loading": "Loading PipelineRun…", "dashboard.pipelineRun.logEmpty": "No log available", "dashboard.pipelineRun.logFailed": "Unable to fetch log", "dashboard.pipelineRun.notFound": "PipelineRun not found", diff --git a/src/nls/messages_es.json b/src/nls/messages_es.json index d8cb6406f7..1d6c643b51 100644 --- a/src/nls/messages_es.json +++ b/src/nls/messages_es.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "Suprimir {kind}", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "No se ha encontrado ninguna {kind}", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "No se ha encontrado ninguna {kind}", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "Error al cargar PipelineRun", "dashboard.pipelineRun.failed": "No se puede cargar PipelineRun", "dashboard.pipelineRun.failedMessage": "No se han podido cargar los detalles de PipelineRun: {reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "No hay salida de registros", "dashboard.pipelineRun.logFailed": "No se puede recuperar el registro", "dashboard.pipelineRun.notFound": "No se ha encontrado PipelineRun", diff --git a/src/nls/messages_fr.json b/src/nls/messages_fr.json index 04521b5901..5fb686a5b7 100644 --- a/src/nls/messages_fr.json +++ b/src/nls/messages_fr.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "Suppression des {kind}", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "{kind} introuvable", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "{kind} introuvable", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "Une erreur s'est produite lors du chargement de PipelineRun", "dashboard.pipelineRun.failed": "Impossible de charger PipelineRun", "dashboard.pipelineRun.failedMessage": "Impossible de charger les détails de PipelineRun : {reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "Aucune sortie de journal", "dashboard.pipelineRun.logFailed": "Impossible d'extraire le journal", "dashboard.pipelineRun.notFound": "PipelineRun introuvable", diff --git a/src/nls/messages_it.json b/src/nls/messages_it.json index e371cf1581..46133b96d3 100644 --- a/src/nls/messages_it.json +++ b/src/nls/messages_it.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "Elimina {kind}", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "Nessun {kind} trovato", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "Nessun {kind} trovato", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "Errore nel caricamento dell'esecuzione pipeline", "dashboard.pipelineRun.failed": "Impossibile caricare l'esecuzione pipeline", "dashboard.pipelineRun.failedMessage": "Impossibile caricare i dettagli dell'esecuzione pipeline: {reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "Nessun output di log", "dashboard.pipelineRun.logFailed": "Impossibile richiamare il log", "dashboard.pipelineRun.notFound": "Esecuzione pipeline non trovata", diff --git a/src/nls/messages_ja.json b/src/nls/messages_ja.json index 15d3cc2adf..26439751d6 100644 --- a/src/nls/messages_ja.json +++ b/src/nls/messages_ja.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "{kind}を削除", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "TaskRun {name}を削除してもよろしいですか?", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "すべてのNamespaceに{kind}がありません", "dashboard.emptyState.clusterResource": "{kind}がありません", "dashboard.emptyState.selectedNamespace": "{selectedNamespace} Namespaceに{kind}がありません", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "PipelineRunのロード中にエラーが発生しました", "dashboard.pipelineRun.failed": "PipelineRunをロードできません", "dashboard.pipelineRun.failedMessage": "PipelineRunをロードできません:{reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "ログがありません", "dashboard.pipelineRun.logFailed": "ログを取得できません", "dashboard.pipelineRun.notFound": "PipelineRunが見つかりません", diff --git a/src/nls/messages_ko.json b/src/nls/messages_ko.json index 2fe6ec6eab..da1d603847 100644 --- a/src/nls/messages_ko.json +++ b/src/nls/messages_ko.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "{kind} 삭제", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "{kind}을(를) 찾을 수 없음", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "{kind}을(를) 찾을 수 없음", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "PipelineRun 로드 중 오류 발생", "dashboard.pipelineRun.failed": "PipelineRun을 로드할 수 없음", "dashboard.pipelineRun.failedMessage": "PipelineRun 세부사항을 로드할 수 없습니다. {reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "로그 출력이 없음", "dashboard.pipelineRun.logFailed": "로그를 페치할 수 없음", "dashboard.pipelineRun.notFound": "PipelineRun을 찾을 수 없음", diff --git a/src/nls/messages_pt.json b/src/nls/messages_pt.json index f0b30fe354..a771729234 100644 --- a/src/nls/messages_pt.json +++ b/src/nls/messages_pt.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "Excluir o {kind}", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "Nenhum {kind} localizado", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "Nenhum {kind} localizado", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "Erro ao carregar o PipelineRun", "dashboard.pipelineRun.failed": "Não é possível carregar PipelineRun", "dashboard.pipelineRun.failedMessage": "Não é possível carregar detalhes de PipelineRun: {reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "Nenhuma saída de log", "dashboard.pipelineRun.logFailed": "Não é possível buscar o log", "dashboard.pipelineRun.notFound": "PipelineRun não localizado", diff --git a/src/nls/messages_zh-Hans.json b/src/nls/messages_zh-Hans.json index 7cd4733752..850c1b2c20 100644 --- a/src/nls/messages_zh-Hans.json +++ b/src/nls/messages_zh-Hans.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "删除 {kind}", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "您确定要删除 TaskRun {name} 吗", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "未找到 {kind}", "dashboard.emptyState.clusterResource": "未找到 {kind}", "dashboard.emptyState.selectedNamespace": "未找到 {kind}", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "加载 PipelineRun 时出错", "dashboard.pipelineRun.failed": "无法加载 PipelineRun", "dashboard.pipelineRun.failedMessage": "无法加载 PipelineRun 详细信息:{reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "没有日志输出", "dashboard.pipelineRun.logFailed": "无法访问日志", "dashboard.pipelineRun.notFound": "未找到 PipelineRun", diff --git a/src/nls/messages_zh-Hant.json b/src/nls/messages_zh-Hant.json index 971e301097..5c138ead92 100644 --- a/src/nls/messages_zh-Hant.json +++ b/src/nls/messages_zh-Hant.json @@ -83,6 +83,7 @@ "dashboard.deleteResources.heading": "刪除 {kind}", "dashboard.deleteRun.body": "", "dashboard.deleteTaskRun.body": "", + "dashboard.editAndRun.actionText": "", "dashboard.emptyState.allNamespaces": "找不到 {kind}", "dashboard.emptyState.clusterResource": "", "dashboard.emptyState.selectedNamespace": "找不到 {kind}", @@ -169,6 +170,7 @@ "dashboard.pipelineRun.error": "載入 PipelineRun 時發生錯誤", "dashboard.pipelineRun.failed": "無法載入 PipelineRun", "dashboard.pipelineRun.failedMessage": "無法載入 PipelineRun 詳細資料:{reason}", + "dashboard.pipelineRun.loading": "", "dashboard.pipelineRun.logEmpty": "沒有日誌輸出", "dashboard.pipelineRun.logFailed": "無法提取日誌", "dashboard.pipelineRun.notFound": "找不到 PipelineRun",