diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts index 7cf99e7a67..2f625b1d56 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts @@ -257,6 +257,10 @@ class PipelineRunDetails extends RunDetails { findOutputArtifacts() { return cy.findByTestId('Output-artifacts'); } + + findErrorState(id: string) { + return cy.findByTestId(id); + } } export const pipelineDetails = new PipelineDetails(); diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/experiments.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/experiments.cy.ts index 770deeae45..cae6adfbb8 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/experiments.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/experiments.cy.ts @@ -12,10 +12,12 @@ import { buildMockRecurringRunKF, } from '~/__mocks__'; import { + activeRunsTable, archivedRunsTable, archiveExperimentModal, bulkArchiveExperimentModal, bulkRestoreExperimentModal, + pipelineRunDetails, pipelineRecurringRunTable, pipelineRunsGlobal, restoreExperimentModal, @@ -27,7 +29,7 @@ import { ProjectModel, RouteModel, } from '~/__tests__/cypress/cypress/utils/models'; -import { RecurringRunStatus, StorageStateKF } from '~/concepts/pipelines/kfTypes'; +import { RecurringRunStatus, RuntimeStateKF, StorageStateKF } from '~/concepts/pipelines/kfTypes'; const projectName = 'test-project-name'; const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' }); @@ -51,6 +53,14 @@ const mockExperiments = [ }), ]; +const mockActiveRuns = buildMockRunKF({ + display_name: 'Test active run 4', + run_id: 'run-4', + experiment_id: 'test-experiment-1', + created_at: '2024-02-10T00:00:00Z', + state: RuntimeStateKF.SUCCEEDED, +}); + describe('Experiments', () => { describe('Active experiments', () => { beforeEach(() => { @@ -251,6 +261,41 @@ describe('Experiments', () => { cy.findByLabelText('Experiment').contains(mockExperiment.display_name); }); + it('should display error state when the pipeline version deleted', () => { + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/runs/:runId', + { + path: { + namespace: projectName, + serviceName: 'dspa', + runId: mockActiveRuns.run_id, + }, + }, + mockActiveRuns, + ); + activeRunsTable.getRowByName('Test active run 4').findColumnName('Test active run 4').click(); + pipelineRunDetails + .findErrorState('run-graph-error-state') + .should('have.text', 'Pipeline run graph unavailable'); + + pipelineRunDetails.findDetailsTab().click(); + pipelineRunDetails.findDetailItem('Name').findValue().contains(mockActiveRuns.display_name); + pipelineRunDetails + .findDetailItem('Pipeline version') + .findValue() + .contains('No pipeline version'); + pipelineRunDetails.findDetailItem('Project').findValue().contains(projectName); + pipelineRunDetails.findDetailItem('Run ID').findValue().contains(mockActiveRuns.run_id); + pipelineRunDetails + .findDetailItem('Workflow name') + .findValue() + .contains(mockActiveRuns.display_name); + pipelineRunDetails.findPipelineSpecTab().click(); + pipelineRunDetails + .findErrorState('pipeline-spec-error-state') + .should('have.text', 'Pipeline spec unavailable'); + }); + it('navigates back to experiments from "Create run" page breadcrumb', () => { pipelineRunsGlobal.findCreateRunButton().click(); cy.findByLabelText('Breadcrumb').findByText(`Experiments - ${projectName}`).click(); @@ -370,7 +415,7 @@ const initIntercepts = () => { { path: { namespace: projectName, serviceName: 'dspa' }, }, - { runs: [] }, + { runs: [mockActiveRuns] }, ); cy.interceptOdh( diff --git a/frontend/src/concepts/dashboard/codeEditor/DashboardCodeEditor.tsx b/frontend/src/concepts/dashboard/codeEditor/DashboardCodeEditor.tsx index 359b3d33f0..66c7881d2b 100644 --- a/frontend/src/concepts/dashboard/codeEditor/DashboardCodeEditor.tsx +++ b/frontend/src/concepts/dashboard/codeEditor/DashboardCodeEditor.tsx @@ -17,7 +17,7 @@ const DashboardCodeEditor: React.FC> = ({ height = 'calc(100% - 38px)', ...props }) => ( -
+
); diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsYAML.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsYAML.tsx index 88001f2edc..29e28f84de 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsYAML.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsYAML.tsx @@ -9,13 +9,29 @@ import { } from '@patternfly/react-core'; import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; import DashboardCodeEditor from '~/concepts/dashboard/codeEditor/DashboardCodeEditor'; +import PipelineVersionError from './PipelineVersionError'; type PipelineDetailsYAMLProps = { filename?: string; content?: Record | null; + versionError?: Error; }; -const PipelineDetailsYAML: React.FC = ({ filename, content }) => { +const PipelineDetailsYAML: React.FC = ({ + versionError, + filename, + content, +}) => { + if (versionError) { + return ( + + ); + } + if (!content) { return ( diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineVersionError.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineVersionError.tsx new file mode 100644 index 0000000000..e514d5cff6 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineVersionError.tsx @@ -0,0 +1,41 @@ +import { + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateIcon, + EmptyStateVariant, + PageSection, +} from '@patternfly/react-core'; +import { ExclamationTriangleIcon } from '@patternfly/react-icons'; +import React from 'react'; + +type PipelineVersionErrorProps = { + title?: string; + description?: string; + testId?: string; +}; + +const PipelineVersionError: React.FC = ({ + title, + description, + testId, +}) => ( + + + + } + headingLevel="h2" + /> + {description} + + +); + +export default PipelineVersionError; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx index b837b67d33..4092ff3161 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetails.tsx @@ -234,7 +234,7 @@ const PipelineDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath }) = hidden={PipelineDetailsTab.YAML !== activeTabKey} className="pf-v5-u-h-100" > - + { const firstId = ids[0]; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx index a6064d33a7..1112361f88 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsTabs.tsx @@ -21,12 +21,14 @@ interface PipelineRunDetailsTabsProps { run: PipelineRunKFv2 | PipelineRecurringRunKFv2 | null; pipelineSpec: PipelineSpecVariable | undefined; graphContent: React.ReactNode; + versionError?: Error; } export const PipelineRunDetailsTabs: React.FC = ({ run, pipelineSpec, graphContent, + versionError, }) => { const [activeKey, setActiveKey] = React.useState(DetailsTabKey.Graph); const isRecurringRun = run && isPipelineRecurringRun(run); @@ -57,7 +59,7 @@ export const PipelineRunDetailsTabs: React.FC = ({ - {!isRecurringRun && pipelineSpec && ( + {!isRecurringRun && ( = ({ id={DetailsTabKey.Graph} eventKey={DetailsTabKey.Graph} className="pf-v5-u-h-100" + data-testid="pipeline-graph-tab" > {graphContent} @@ -83,9 +86,14 @@ export const PipelineRunDetailsTabs: React.FC = ({ eventKey={DetailsTabKey.Spec} hidden={activeKey !== DetailsTabKey.Spec} style={{ flex: 1 }} + data-testid="pipeline-spec-tab" > - - + + diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx index e28f29fcb3..d0c6d6e3e5 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx @@ -73,6 +73,8 @@ const PipelineRunTabDetails: React.FC = ({ run, work ), }, ] + : versionError + ? [{ key: 'Pipeline version', value: 'No pipeline version' }] : []), ...(pipeline ? [ diff --git a/frontend/src/concepts/topology/PipelineTopology.tsx b/frontend/src/concepts/topology/PipelineTopology.tsx index aba5f9e56f..aa8b8b1f6b 100644 --- a/frontend/src/concepts/topology/PipelineTopology.tsx +++ b/frontend/src/concepts/topology/PipelineTopology.tsx @@ -5,20 +5,23 @@ import { VisualizationProvider, } from '@patternfly/react-topology'; import { Bullseye, Spinner } from '@patternfly/react-core'; +import PipelineVersionError from '~/concepts/pipelines/content/pipelinesDetails/PipelineVersionError'; +import PipelineTopologyEmpty from './PipelineTopologyEmpty'; import useTopologyController from './useTopologyController'; import PipelineVisualizationSurface from './PipelineVisualizationSurface'; -import PipelineTopologyEmpty from './PipelineTopologyEmpty'; type PipelineTopologyProps = { selectedIds?: string[]; onSelectionChange?: (selectionIds: string[]) => void; nodes: PipelineNodeModel[]; + versionError?: Error; }; const PipelineTopology: React.FC = ({ nodes, selectedIds, onSelectionChange, + versionError, }) => { const controller = useTopologyController('g1'); @@ -37,6 +40,16 @@ const PipelineTopology: React.FC = ({ return undefined; }, [controller, onSelectionChange]); + if (versionError) { + return ( + + ); + } + if (!nodes.length) { return ; }