diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts index 3401eb352a..c62ee1dd81 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts @@ -255,6 +255,10 @@ class PipelineRunDetails extends RunDetails { findOutputArtifacts() { return cy.findByTestId('Output-artifacts'); } + + findErrorstate() { + return cy.findByTestId('error-state-title'); + } } 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 1de1d76930..20e5c76180 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 { buildMockJobKF, } from '~/__mocks__'; import { + activeRunsTable, archivedRunsTable, archiveExperimentModal, bulkArchiveExperimentModal, bulkRestoreExperimentModal, + pipelineRunDetails, pipelineRunJobTable, 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,35 @@ 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().should('have.text', 'Error loading pipeline run graph'); + + 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); + }); + it('navigates back to experiments from "Create run" page breadcrumb', () => { pipelineRunsGlobal.findCreateRunButton().click(); cy.findByLabelText('Breadcrumb').findByText(`Experiments - ${projectName}`).click(); @@ -370,7 +409,7 @@ const initIntercepts = () => { { path: { namespace: projectName, serviceName: 'dspa' }, }, - { runs: [] }, + { runs: [mockActiveRuns] }, ); cy.interceptOdh( diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index b4f86b7d77..297b3f80cc 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx @@ -69,7 +69,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, ); const loaded = runLoaded && (versionLoaded || !!run?.pipeline_spec); - const error = versionError || runError; + const error = runError; if (error) { return ( @@ -84,7 +84,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, ); } - if (!loaded || (!executionsLoaded && !executionsError)) { + if ((!loaded && !versionError) || (!executionsLoaded && !executionsError)) { return ( @@ -126,6 +126,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, } loaded={loaded} loadError={error} + versionError={versionError} breadcrumb={ {breadcrumbPath} @@ -164,6 +165,7 @@ const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, graphContent={ { const firstId = ids[0]; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunTabDetails.tsx index f1f5d6ad4d..ff5832b8bd 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..18dcfe697d 100644 --- a/frontend/src/concepts/topology/PipelineTopology.tsx +++ b/frontend/src/concepts/topology/PipelineTopology.tsx @@ -4,21 +4,33 @@ import { SELECTION_EVENT, VisualizationProvider, } from '@patternfly/react-topology'; -import { Bullseye, Spinner } from '@patternfly/react-core'; +import { + Bullseye, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateIcon, + EmptyStateVariant, + PageSection, + Spinner, +} from '@patternfly/react-core'; +import { ExclamationTriangleIcon } from '@patternfly/react-icons'; +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 +49,30 @@ const PipelineTopology: React.FC = ({ return undefined; }, [controller, onSelectionChange]); + if (versionError) { + return ( + + + + } + headingLevel="h2" + /> + + Unable to load this run graph because the pipeline version that it belongs to has been + deleted. + + + + ); + } + if (!nodes.length) { return ; } diff --git a/frontend/src/pages/ApplicationsPage.tsx b/frontend/src/pages/ApplicationsPage.tsx index 4b2a3eed43..73e6f598f4 100644 --- a/frontend/src/pages/ApplicationsPage.tsx +++ b/frontend/src/pages/ApplicationsPage.tsx @@ -24,6 +24,7 @@ type ApplicationsPageProps = { loaded: boolean; empty: boolean; loadError?: Error; + versionError?: Error; children?: React.ReactNode; errorMessage?: string; emptyMessage?: string; @@ -44,6 +45,7 @@ const ApplicationsPage: React.FC = ({ loaded, empty, loadError, + versionError, children, errorMessage, emptyMessage, @@ -97,7 +99,7 @@ const ApplicationsPage: React.FC = ({ ); } - if (!loaded) { + if (!loaded && !versionError) { return ( loadingContent || (