From 800f87aac630edea5e7d17acdc707ac0c4576421 Mon Sep 17 00:00:00 2001 From: Jeff Puzzo Date: Tue, 19 Mar 2024 17:08:04 -0400 Subject: [PATCH] [RHOAIENG-2985] Experiments - Run Integration --- .../cypress/e2e/pipelines/Experiments.cy.ts | 108 ++++++++++++------ .../e2e/pipelines/PipelineCreateRuns.cy.ts | 53 +++------ .../cypress/e2e/pipelines/PipelineRuns.cy.ts | 14 +-- .../cypress/e2e/pipelines/Pipelines.cy.ts | 2 +- .../e2e/pipelines/PipelinesTopology.cy.ts | 25 ++-- .../cypress/pages/pipelines/experiments.ts | 4 +- .../pages/pipelines/pipelineRunsGlobal.ts | 6 +- .../cypress/pages/pipelines/topology.ts | 12 ++ frontend/src/api/pipelines/custom.ts | 13 ++- frontend/src/app/AppRoutes.tsx | 4 +- .../pipelines/apiHooks/usePipelineRunJobs.ts | 10 +- .../pipelines/apiHooks/usePipelineRuns.ts | 20 ++-- .../content/createRun/CloneRunPage.tsx | 2 +- .../pipelines/content/createRun/RunPage.tsx | 6 +- .../content/createRun/RunPageFooter.tsx | 22 +++- .../contentSections/RunTypeSection.tsx | 13 ++- .../content/createRun/submitUtils.ts | 20 ++-- .../content/createRun/useCloneRunData.ts | 6 +- .../pipelineRun/PipelineRunDetails.tsx | 4 +- .../pipelineRun/PipelineRunDetailsActions.tsx | 17 ++- .../pipelineRunJob/PipelineRunJobDetails.tsx | 4 +- .../PipelineRunJobDetailsActions.tsx | 15 ++- .../content/tables/PipelineFilterBar.tsx | 2 +- .../pipelines/content/tables/columns.ts | 13 ++- .../tables/experiment/ExperimentTableRow.tsx | 47 +++++--- .../tables/pipelineRun/PipelineRunTable.tsx | 19 ++- .../pipelineRun/PipelineRunTableRow.tsx | 27 +++-- .../pipelineRun/PipelineRunTableRowTitle.tsx | 15 ++- .../pipelineRun/PipelineRunTableToolbar.tsx | 27 +++-- .../pipelineRunJob/PipelineRunJobTableRow.tsx | 21 +++- .../PipelineRunJobTableToolbar.tsx | 14 ++- .../content/tables/usePipelineTable.ts | 13 ++- frontend/src/concepts/pipelines/types.ts | 10 +- frontend/src/concepts/pipelines/utils.ts | 5 + .../GlobalPipelineExperimentsRoutes.tsx | 46 +++++++- .../global/GlobalPipelineCoreDetails.tsx | 40 +++++++ .../global/PipelineCoreApplicationPage.tsx | 2 +- .../ExperimentRunsListBreadcrumb.tsx | 22 ++++ .../global/experiments/GlobalExperiments.tsx | 3 +- .../experiments/GlobalExperimentsTabs.tsx | 3 +- .../experiments/useExperimentByParams.ts | 20 ++++ .../pipelines/global/runs/ActiveRuns.tsx | 17 ++- .../pipelines/global/runs/ArchiveRunModal.tsx | 2 +- .../pipelines/global/runs/ArchivedRuns.tsx | 3 +- .../global/runs/BulkArchiveRunModal.tsx | 2 +- .../global/runs/CreateRunEmptyState.tsx | 46 -------- .../global/runs/GlobalPipelineRuns.tsx | 18 ++- .../global/runs/GlobalPipelineRunsTabs.tsx | 22 ++-- .../pipelines/global/runs/ScheduledRuns.tsx | 15 ++- .../src/pages/pipelines/global/runs/types.ts | 4 +- frontend/src/routes/pipelines/experiments.ts | 66 +++++++++++ frontend/src/routes/pipelines/global.ts | 7 +- frontend/src/routes/pipelines/index.ts | 2 + frontend/src/routes/pipelines/runs.ts | 77 +++++++++++++ frontend/src/utilities/NavData.tsx | 4 +- 55 files changed, 708 insertions(+), 306 deletions(-) create mode 100644 frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx create mode 100644 frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts delete mode 100644 frontend/src/pages/pipelines/global/runs/CreateRunEmptyState.tsx create mode 100644 frontend/src/routes/pipelines/experiments.ts create mode 100644 frontend/src/routes/pipelines/runs.ts diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Experiments.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Experiments.cy.ts index 4682f1fc48..62e28eac7d 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Experiments.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Experiments.cy.ts @@ -1,25 +1,27 @@ /* eslint-disable camelcase */ -import { buildMockExperimentKF, buildMockRunKF } from '~/__mocks__'; -import { mockDashboardConfig } from '~/__mocks__/mockDashboardConfig'; -import { mockDataSciencePipelineApplicationK8sResource } from '~/__mocks__/mockDataSciencePipelinesApplicationK8sResource'; -import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; -import { buildMockPipelineV2, buildMockPipelines } from '~/__mocks__/mockPipelinesProxy'; import { + buildMockExperimentKF, + mockDashboardConfig, + mockDataSciencePipelineApplicationK8sResource, + mockK8sResourceList, + buildMockPipelineV2, + buildMockPipelines, buildMockPipelineVersionV2, buildMockPipelineVersionsV2, -} from '~/__mocks__/mockPipelineVersionsProxy'; -import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; -import { mockRouteK8sResource } from '~/__mocks__/mockRouteK8sResource'; -import { mockStatus } from '~/__mocks__/mockStatus'; + mockProjectK8sResource, + mockRouteK8sResource, + mockStatus, +} from '~/__mocks__'; import { experimentsTabs } from '~/__tests__/cypress/cypress/pages/pipelines/experiments'; -import { RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; +import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url.cy'; +import { pipelineRunsGlobal } from '~/__tests__/cypress/cypress/pages/pipelines'; const projectName = 'test-project-name'; const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' }); const initialMockPipelineVersion = buildMockPipelineVersionV2({ pipeline_id: initialMockPipeline.pipeline_id, }); -const mockExperimentArray = [ +const mockExperiments = [ buildMockExperimentKF({ display_name: 'Test experiment 1', experiment_id: '1', @@ -34,24 +36,10 @@ const mockExperimentArray = [ }), ]; -const runs = Array.from({ length: 5 }, (_, i) => - buildMockRunKF({ - display_name: 'Test triggered run 1', - run_id: `run-${i}`, - pipeline_version_reference: { - pipeline_id: initialMockPipeline.pipeline_id, - pipeline_version_id: initialMockPipelineVersion.pipeline_version_id, - }, - experiment_id: '1', - created_at: '2024-02-01T00:00:00Z', - state: RuntimeStateKF.SUCCEEDED, - }), -); - -describe('Pipeline Experiments', () => { +describe('Experiments', () => { beforeEach(() => { initIntercepts(); - experimentsTabs.mockGetExperiments(mockExperimentArray); + experimentsTabs.mockGetExperiments(mockExperiments); experimentsTabs.visit(projectName); }); @@ -69,19 +57,56 @@ describe('Pipeline Experiments', () => { // Verify initial run rows exist experimentsTabs.getActiveExperimentsTable().findRows().should('have.length', 3); - // Select the "Name" filter, enter a value to filter by + // Select the "Experiment" filter, enter a value to filter by experimentsTabs.getActiveExperimentsTable().selectFilterByName('Experiment'); experimentsTabs.getActiveExperimentsTable().findFilterTextField().type('Test experiment 2'); - // Mock runs (filtered by typed run name) + // Mock experiments (filtered by typed experiment name) experimentsTabs.mockGetExperiments( - mockExperimentArray.filter((exp) => exp.display_name.includes('Test experiment 2')), + mockExperiments.filter((exp) => exp.display_name.includes('Test experiment 2')), ); - // Verify only rows with the typed run name exist + // Verify only rows with the typed experiment name exist experimentsTabs.getActiveExperimentsTable().findRows().should('have.length', 1); experimentsTabs.getActiveExperimentsTable().findRowByName('Test experiment 2'); }); + + describe('Runs page', () => { + const activeExperimentsTable = experimentsTabs.getActiveExperimentsTable(); + const [mockExperiment] = mockExperiments; + + beforeEach(() => { + activeExperimentsTable.findRowByName(mockExperiment.display_name).find('a').click(); + }); + + it('navigates to the runs page when clicking an experiment name', () => { + verifyRelativeURL(`/experiments/${projectName}/${mockExperiment.experiment_id}/runs`); + cy.findByLabelText('Breadcrumb').findByText('Experiments'); + }); + + it('has "Experiment" value pre-filled when on the "Create run" page', () => { + pipelineRunsGlobal.findCreateRunButton().click(); + cy.findByLabelText('Experiment').contains(mockExperiment.display_name); + }); + + it('navigates back to experiments from "Create run" page breadcrumb', () => { + pipelineRunsGlobal.findCreateRunButton().click(); + cy.findByLabelText('Breadcrumb').findByText(`Experiments - ${projectName}`).click(); + verifyRelativeURL(`/experiments/${projectName}`); + }); + + it('navigates back to experiment runs page from "Create run" page breadcrumb', () => { + pipelineRunsGlobal.findCreateRunButton().click(); + cy.findByLabelText('Breadcrumb').findByText(mockExperiment.display_name).click(); + verifyRelativeURL(`/experiments/${projectName}/${mockExperiment.experiment_id}/runs`); + }); + + it('has "Experiment" value pre-filled when on the "Schedule run" page', () => { + pipelineRunsGlobal.findSchedulesTab().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); + cy.findByLabelText('Experiment').contains(mockExperiment.display_name); + }); + }); }); const initIntercepts = () => { @@ -113,7 +138,9 @@ const initIntercepts = () => { { pathname: '/api/k8s/apis/project.openshift.io/v1/projects', }, - mockK8sResourceList([mockProjectK8sResource({ k8sName: projectName })]), + mockK8sResourceList([ + mockProjectK8sResource({ k8sName: projectName, displayName: projectName }), + ]), ); cy.intercept( @@ -134,6 +161,21 @@ const initIntercepts = () => { { pathname: '/api/proxy/apis/v2beta1/runs', }, - { runs }, + { runs: [] }, + ); + cy.intercept( + { + method: 'POST', + pathname: '/api/proxy/apis/v2beta1/recurringruns', + }, + { + recurringRuns: [], + }, + ); + cy.intercept( + { + pathname: `/api/proxy/apis/v2beta1/experiments/${mockExperiments[0].experiment_id}`, + }, + mockExperiments[0], ); }; diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts index a7116687a2..96c229dd29 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineCreateRuns.cy.ts @@ -379,13 +379,8 @@ describe('Pipeline create runs', () => { mockPipelineVersion.pipeline_id, ); - // Mock jobs list with newly created job - pipelineRunJobTable - .mockGetJobs([...initialMockRecurringRuns, buildMockJobKF(createRecurringRunParams)]) - .as('refreshRecurringRuns'); - // Navigate to the 'Create run' page - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); verifyRelativeURL(`/pipelineRuns/${projectName}/pipelineRun/create?runType=scheduled`); createSchedulePage.find(); @@ -433,16 +428,10 @@ describe('Pipeline create runs', () => { }); }); - // Should show newly created schedule in the table - cy.wait('@refreshRecurringRuns').then((interception) => { - expect(interception.request.body).to.eql({ - path: '/apis/v2beta1/recurringruns', - method: 'GET', - host: 'https://ds-pipeline-pipelines-definition-test-project-name.apps.user.com', - queryParams: { sort_by: 'created_at desc', page_size: 10 }, - }); - }); - pipelineRunJobTable.findRowByName('New job'); + // Should be redirected to the schedule details page + verifyRelativeURL( + `/pipelineRuns/${projectName}/pipelineRunJob/view/${createRecurringRunParams.recurring_run_id}`, + ); }); it('duplicates a schedule', () => { @@ -466,11 +455,6 @@ describe('Pipeline create runs', () => { cloneSchedulePage.mockGetPipeline(mockPipeline); cloneSchedulePage.mockGetExperiment(mockExperiment); - // Mock jobs list with newly cloned job - pipelineRunJobTable - .mockGetJobs([...initialMockRecurringRuns, mockDuplicateRecurringRun]) - .as('refreshRecurringRuns'); - // Navigate to clone run page for a given schedule pipelineRunJobTable.selectRowActionByName(mockRecurringRun.display_name, 'Duplicate'); verifyRelativeURL( @@ -523,21 +507,14 @@ describe('Pipeline create runs', () => { }); }); - // Should show newly cloned schedule in the table - cy.wait('@refreshRecurringRuns').then((interception) => { - expect(interception.request.body).to.eql({ - path: '/apis/v2beta1/recurringruns', - method: 'GET', - host: 'https://ds-pipeline-pipelines-definition-test-project-name.apps.user.com', - queryParams: { sort_by: 'created_at desc', page_size: 10 }, - }); - }); - - pipelineRunJobTable.findRowByName('Duplicate of Test job'); + // Should be redirected to the schedule details page + verifyRelativeURL( + `/pipelineRuns/${projectName}/pipelineRunJob/view/${mockDuplicateRecurringRun.recurring_run_id}`, + ); }); it('shows cron & periodic fields', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); createSchedulePage.findScheduledRunTypeSelector().click(); createSchedulePage.findScheduledRunTypeSelectorPeriodic().click(); @@ -551,7 +528,7 @@ describe('Pipeline create runs', () => { }); it('should start concurrent at the max, 10', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); createSchedulePage.findMaxConcurrencyFieldMinus().should('be.enabled'); createSchedulePage.findMaxConcurrencyFieldPlus().should('be.disabled'); @@ -559,7 +536,7 @@ describe('Pipeline create runs', () => { }); it('should allow the concurrency to update via +/-', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); createSchedulePage.findMaxConcurrencyFieldMinus().click(); createSchedulePage.findMaxConcurrencyFieldMinus().click(); @@ -570,7 +547,7 @@ describe('Pipeline create runs', () => { }); it('should not allow concurrency to go under or above the bounds', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); createSchedulePage.findMaxConcurrencyFieldValue().fill('0'); createSchedulePage.findMaxConcurrencyFieldValue().should('have.value', 1); @@ -580,7 +557,7 @@ describe('Pipeline create runs', () => { }); it('should hide and show date toggles', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); createSchedulePage.findStartDatePickerDate().should('not.be.visible'); createSchedulePage.findStartDatePickerTime().should('not.be.visible'); @@ -596,7 +573,7 @@ describe('Pipeline create runs', () => { }); it('should see catch up is enabled by default', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); createSchedulePage.findCatchUpSwitchValue().should('be.checked'); createSchedulePage.findCatchUpSwitch().click(); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineRuns.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineRuns.cy.ts index d30c000dfc..6df8f47d65 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineRuns.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelineRuns.cy.ts @@ -257,14 +257,14 @@ describe('Pipeline runs', () => { }); describe('Table filter', () => { - it('filter by run name', () => { + it('filter by name', () => { // Verify initial run rows exist activeRunsTable.findRows().should('have.length', 3); - // Select the "Name" filter, enter a value to filter by + // Select the "Run" filter, enter a value to filter by pipelineRunsGlobal .findActiveRunsToolbar() - .within(() => pipelineRunsGlobal.selectFilterByName('Name')); + .within(() => pipelineRunsGlobal.selectFilterByName('Run')); pipelineRunsGlobal .findActiveRunsToolbar() .within(() => pipelineRunFilterBar.findNameInput().type('run 1')); @@ -504,7 +504,7 @@ describe('Pipeline runs', () => { }); it('navigate to create run page', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); verifyRelativeURL(`/pipelineRuns/${projectName}/pipelineRun/create?runType=scheduled`); }); }); @@ -536,7 +536,7 @@ describe('Pipeline runs', () => { describe('Navigation', () => { it('navigate to create scheduled run page', () => { - pipelineRunsGlobal.findCreateScheduleButton().click(); + pipelineRunsGlobal.findScheduleRunButton().click(); verifyRelativeURL(`/pipelineRuns/${projectName}/pipelineRun/create?runType=scheduled`); }); it('navigate to clone scheduled run page', () => { @@ -561,8 +561,8 @@ describe('Pipeline runs', () => { // Verify initial job rows exist pipelineRunJobTable.findRows().should('have.length', 3); - // Select the "Name" filter, enter a value to filter by - pipelineRunJobTable.selectFilterByName('Name'); + // Select the "Schedule" filter, enter a value to filter by + pipelineRunJobTable.selectFilterByName('Schedule'); pipelineRunJobTable.findFilterTextField().type('test-pipeline'); // Mock jobs (filtered by typed job name) diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts index 1d4215cd87..fcf71f6cb9 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts @@ -266,7 +266,7 @@ describe('Pipelines', () => { pipelinesTable.findRowByName('New pipeline version'); }); - it.only('delete a single pipeline', () => { + it('delete a single pipeline', () => { createDeletePipelineIntercept(initialMockPipeline.pipeline_id).as('deletePipeline'); pipelinesTable.mockGetPipelineVersions([], initialMockPipeline.pipeline_id); pipelinesGlobal.visit(projectName); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts index 3c1a0568f2..2dee67b2a5 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesTopology.cy.ts @@ -143,7 +143,7 @@ const initIntercepts = () => { cy.intercept( { method: 'POST', - pathname: `/api/proxy/apis/v2beta1/runs/`, + pathname: '/api/proxy/apis/v2beta1/runs', }, { runs: [mockRun] }, ); @@ -184,6 +184,7 @@ describe('Pipeline topology', () => { describe('Navigation', () => { beforeEach(() => { initIntercepts(); + pipelineDetails.visit(projectId, mockVersion.pipeline_id, mockVersion.pipeline_version_id); // https://issues.redhat.com/browse/RHOAIENG-4562 // Bypass intermittent Cypress error: // Failed to execute 'importScripts' on 'WorkerGlobalScope': The script at 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs/base/worker/workerMain.js' failed to load. @@ -191,30 +192,22 @@ describe('Pipeline topology', () => { }); it('Test pipeline details create run navigation', () => { - pipelineDetails.visit(projectId, mockVersion.pipeline_id, mockVersion.pipeline_version_id); - pipelineDetails.findActionsDropdown().click(); - cy.findByText('Create run').click(); + pipelineDetails.selectActionDropdownItem('Create run'); verifyRelativeURL(`/pipelineRuns/${projectId}/pipelineRun/create`); }); it('navigates to "Schedule run" page on "Schedule run" click', () => { - pipelineDetails.visit(projectId, mockVersion.pipeline_id, mockVersion.pipeline_version_id); - pipelineDetails.findActionsDropdown().click(); - cy.findByText('Schedule run').click(); + pipelineDetails.selectActionDropdownItem('Schedule run'); verifyRelativeURL(`/pipelineRuns/${projectId}/pipelineRun/create?runType=scheduled`); }); it('Test pipeline details view runs navigation', () => { - pipelineDetails.visit(projectId, mockVersion.pipeline_id, mockVersion.pipeline_version_id); - pipelineDetails.findActionsDropdown().click(); - cy.findByText('View runs').click(); + pipelineDetails.selectActionDropdownItem('View runs'); verifyRelativeURL(`/pipelineRuns/${projectId}?runType=active`); }); it('navigates to "Schedules" on "View schedules" click', () => { - pipelineDetails.visit(projectId, mockVersion.pipeline_id, mockVersion.pipeline_version_id); - pipelineDetails.findActionsDropdown().click(); - cy.findByText('View schedules').click(); + pipelineDetails.selectActionDropdownItem('View schedules'); verifyRelativeURL(`/pipelineRuns/${projectId}?runType=scheduled`); }); }); @@ -228,15 +221,13 @@ describe('Pipeline topology', () => { it('Test pipeline run duplicate navigation', () => { pipelineRunDetails.visit(projectId, mockRun.run_id); - pipelineRunDetails.findActionsDropdown().click(); - cy.findByText('Duplicate').click(); + pipelineRunDetails.selectActionDropdownItem('Duplicate'); verifyRelativeURL(`/pipelineRuns/${projectId}/pipelineRun/clone/${mockRun.run_id}`); }); it('Test pipeline job duplicate navigation', () => { pipelineRunJobDetails.visit(projectId, mockJob.recurring_run_id); - pipelineRunJobDetails.findActionsDropdown().click(); - cy.findByText('Duplicate run').click(); + pipelineRunJobDetails.selectActionDropdownItem('Duplicate'); verifyRelativeURL( `/pipelineRuns/${projectId}/pipelineRun/cloneJob/${mockJob.recurring_run_id}?runType=scheduled`, ); diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/experiments.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/experiments.ts index 78cd7c524b..9e330939ee 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/experiments.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/experiments.ts @@ -4,9 +4,7 @@ import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes'; class ExperimentsTabs { visit(namespace?: string, tab?: string) { - cy.visitWithLogin( - `/pipelineExperiments${namespace ? `/${namespace}` : ''}${tab ? `/${tab}` : ''}`, - ); + cy.visitWithLogin(`/experiments${namespace ? `/${namespace}` : ''}${tab ? `/${tab}` : ''}`); this.wait(); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunsGlobal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunsGlobal.ts index d4384b0ea8..6580a50c4c 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunsGlobal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineRunsGlobal.ts @@ -25,11 +25,11 @@ class PipelineRunsGlobal { } findActiveRunsTab() { - return cy.findByRole('tab', { name: 'Active runs tab' }); + return cy.findByRole('tab', { name: 'Active tab' }); } findArchivedRunsTab() { - return cy.findByRole('tab', { name: 'Archived runs tab' }); + return cy.findByRole('tab', { name: 'Archived tab' }); } findProjectSelect() { @@ -40,7 +40,7 @@ class PipelineRunsGlobal { return cy.findByRole('button', { name: 'Create run' }); } - findCreateScheduleButton() { + findScheduleRunButton() { return cy.findByRole('button', { name: 'Schedule run' }); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts index 70419f16e5..e702fb7989 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts @@ -110,6 +110,10 @@ class PipelineDetails extends PipelinesTopology { selectStepByName(name: string): void { this.findStepSelect().click().findByText(name).click(); } + + selectActionDropdownItem(label: string) { + this.findActionsDropdown().click().findByRole('menuitem', { name: label }).click(); + } } class PipelineRunJobDetails extends RunDetails { @@ -121,6 +125,10 @@ class PipelineRunJobDetails extends RunDetails { findActionsDropdown() { return cy.findByTestId('pipeline-run-job-details-actions'); } + + selectActionDropdownItem(label: string) { + this.findActionsDropdown().click().findByRole('menuitem', { name: label }).click(); + } } class PipelineRunDetails extends RunDetails { @@ -168,6 +176,10 @@ class PipelineRunDetails extends RunDetails { selectStepByName(name: string): void { this.findStepSelect().click().findByText(name).click(); } + + selectActionDropdownItem(label: string) { + this.findActionsDropdown().click().findByRole('menuitem', { name: label }).click(); + } } class PipelineRunBottomDrawer extends Contextual { diff --git a/frontend/src/api/pipelines/custom.ts b/frontend/src/api/pipelines/custom.ts index c6319f0280..79fa50c38f 100644 --- a/frontend/src/api/pipelines/custom.ts +++ b/frontend/src/api/pipelines/custom.ts @@ -132,7 +132,7 @@ export const listPipelineRuns: ListPipelinesRunAPI = (hostPath) => (opts, params { ...pipelineParamsToQuery(params), // eslint-disable-next-line camelcase - experiment_id: params?.experiment_id, + experiment_id: params?.experimentId, }, opts, ), @@ -178,7 +178,16 @@ export const listPipelineArchivedRuns: ListPipelinesRunAPI = (hostPath) => (opts export const listPipelineRunJobs: ListPipelinesRunJobAPI = (hostPath) => (opts, params) => handlePipelineFailures( - proxyGET(hostPath, '/apis/v2beta1/recurringruns', pipelineParamsToQuery(params), opts), + proxyGET( + hostPath, + '/apis/v2beta1/recurringruns', + { + ...pipelineParamsToQuery(params), + // eslint-disable-next-line camelcase + experiment_id: params?.experimentId, + }, + opts, + ), ); export const listPipelineVersions: ListPipelineVersionsAPI = diff --git a/frontend/src/app/AppRoutes.tsx b/frontend/src/app/AppRoutes.tsx index 9976ca7ec4..257118e588 100644 --- a/frontend/src/app/AppRoutes.tsx +++ b/frontend/src/app/AppRoutes.tsx @@ -3,7 +3,7 @@ import { Route, Routes } from 'react-router-dom'; import ApplicationsPage from '~/pages/ApplicationsPage'; import UnauthorizedError from '~/pages/UnauthorizedError'; import { useUser } from '~/redux/selectors'; -import { globPipelineRunsAll, globPipelinesAll } from '~/routes'; +import { globExperimentsAll, globPipelineRunsAll, globPipelinesAll } from '~/routes'; import { useCheckJupyterEnabled } from '~/utilities/notebookControllerUtils'; const InstalledApplications = React.lazy( @@ -84,7 +84,7 @@ const AppRoutes: React.FC = () => { } /> } /> - } /> + } /> } /> diff --git a/frontend/src/concepts/pipelines/apiHooks/usePipelineRunJobs.ts b/frontend/src/concepts/pipelines/apiHooks/usePipelineRunJobs.ts index fcf4d46035..1780e95b38 100644 --- a/frontend/src/concepts/pipelines/apiHooks/usePipelineRunJobs.ts +++ b/frontend/src/concepts/pipelines/apiHooks/usePipelineRunJobs.ts @@ -3,19 +3,21 @@ import { PipelineRunJobKFv2 } from '~/concepts/pipelines/kfTypes'; import { FetchState } from '~/utilities/useFetchState'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import usePipelineQuery from '~/concepts/pipelines/apiHooks/usePipelineQuery'; -import { PipelineListPaged, PipelineOptions } from '~/concepts/pipelines/types'; +import { PipelineListPaged, PipelineRunOptions } from '~/concepts/pipelines/types'; const usePipelineRunJobs = ( - options?: PipelineOptions, + options?: PipelineRunOptions, ): FetchState> => { const { api } = usePipelinesAPI(); + const experimentId = options?.experimentId; + return usePipelineQuery( React.useCallback( (opts, params) => api - .listPipelineRunJobs(opts, params) + .listPipelineRunJobs(opts, { ...params, ...(experimentId && { experimentId }) }) .then((result) => ({ ...result, items: result.recurringRuns })), - [api], + [api, experimentId], ), options, ); diff --git a/frontend/src/concepts/pipelines/apiHooks/usePipelineRuns.ts b/frontend/src/concepts/pipelines/apiHooks/usePipelineRuns.ts index e9286af946..5640c8f886 100644 --- a/frontend/src/concepts/pipelines/apiHooks/usePipelineRuns.ts +++ b/frontend/src/concepts/pipelines/apiHooks/usePipelineRuns.ts @@ -3,37 +3,39 @@ import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; import { FetchState } from '~/utilities/useFetchState'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import usePipelineQuery from '~/concepts/pipelines/apiHooks/usePipelineQuery'; -import { PipelineListPaged, PipelineOptions } from '~/concepts/pipelines/types'; +import { PipelineListPaged, PipelineRunOptions } from '~/concepts/pipelines/types'; export const usePipelineActiveRuns = ( - options?: PipelineOptions, + options?: PipelineRunOptions, ): FetchState> => { const { api } = usePipelinesAPI(); + const experimentId = options?.experimentId; return usePipelineQuery( React.useCallback( (opts, params) => api - .listPipelineActiveRuns(opts, params) + .listPipelineActiveRuns(opts, { ...params, ...(experimentId && { experimentId }) }) .then((result) => ({ ...result, items: result.runs })), - [api], + [api, experimentId], ), options, ); }; export const usePipelineArchivedRuns = ( - options?: PipelineOptions, + options?: PipelineRunOptions, ): FetchState> => { const { api } = usePipelinesAPI(); + const experimentId = options?.experimentId; return usePipelineQuery( React.useCallback( (opts, params) => api - .listPipelineArchivedRuns(opts, params) + .listPipelineArchivedRuns(opts, { ...params, ...(experimentId && { experimentId }) }) .then((result) => ({ ...result, items: result.runs })), - [api], + [api, experimentId], ), options, ); @@ -41,7 +43,7 @@ export const usePipelineArchivedRuns = ( export const usePipelineRunsByExperiment = ( experimentId: string, - options?: PipelineOptions, + options?: PipelineRunOptions, ): FetchState> => { const { api } = usePipelinesAPI(); @@ -50,7 +52,7 @@ export const usePipelineRunsByExperiment = ( (opts, params) => api // eslint-disable-next-line camelcase - .listPipelineRuns(opts, { ...params, experiment_id: experimentId }) + .listPipelineRuns(opts, { ...params, experimentId }) .then((result) => ({ ...result, items: result.runs })), [api, experimentId], ), diff --git a/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx index 4099cd98a7..e465da1ba3 100644 --- a/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/CloneRunPage.tsx @@ -12,7 +12,7 @@ import { runTypeCategory } from './types'; const CloneRunPage: React.FC = ({ breadcrumbPath, contextPath }) => { const [run, loaded, error] = useCloneRunData(); - const { runTypeString } = useGetSearchParamValues([PipelineRunSearchParam.RunType]); + const { runType: runTypeString } = useGetSearchParamValues([PipelineRunSearchParam.RunType]); const runType = asEnumMember(runTypeString, PipelineRunType); const title = `Duplicate ${runTypeCategory[runType || PipelineRunType.Active]}`; diff --git a/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx b/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx index d6fc1fa801..ce6177e9ec 100644 --- a/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/RunPage.tsx @@ -36,7 +36,7 @@ type RunPageProps = { }; const RunPage: React.FC = ({ cloneRun, contextPath, testId }) => { - const { namespace } = useParams(); + const { namespace, experimentId } = useParams(); const location = useLocation(); const { runType, triggerType: triggerTypeString } = useGetSearchParamValues([ PipelineRunSearchParam.RunType, @@ -50,7 +50,7 @@ const RunPage: React.FC = ({ cloneRun, contextPath, testId }) => { const [cloneRunPipelineVersion] = usePipelineVersionById(cloneRunPipelineId, cloneRunVersionId); const [cloneRunPipeline] = usePipelineById(cloneRunPipelineId); - const [cloneRunExperiment] = useExperimentById(cloneRunExperimentId); + const [runExperiment] = useExperimentById(cloneRunExperimentId || experimentId); const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; @@ -77,7 +77,7 @@ const RunPage: React.FC = ({ cloneRun, contextPath, testId }) => { }, pipeline: location.state?.lastPipeline || cloneRunPipeline, version: location.state?.lastVersion || cloneRunPipelineVersion, - experiment: cloneRunExperiment, + experiment: runExperiment, }); const onValueChange = React.useCallback( diff --git a/frontend/src/concepts/pipelines/content/createRun/RunPageFooter.tsx b/frontend/src/concepts/pipelines/content/createRun/RunPageFooter.tsx index 02deb4b6c9..96614e1cac 100644 --- a/frontend/src/concepts/pipelines/content/createRun/RunPageFooter.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/RunPageFooter.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Alert, Button, Split, SplitItem, Stack, StackItem } from '@patternfly/react-core'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { RunFormData } from '~/concepts/pipelines/content/createRun/types'; import { isFilledRunFormData, @@ -12,6 +12,8 @@ import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; import { useGetSearchParamValues } from '~/utilities/useGetSearchParamValues'; import { PipelineRunType } from '~/pages/pipelines/global/runs'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { isRunSchedule } from '~/concepts/pipelines/utils'; +import { routePipelineRunDetails, routePipelineRunJobDetails } from '~/routes'; type RunPageFooterProps = { data: RunFormData; @@ -19,6 +21,7 @@ type RunPageFooterProps = { }; const RunPageFooter: React.FC = ({ data, contextPath }) => { + const { experimentId } = useParams(); const { api } = usePipelinesAPI(); const { runType } = useGetSearchParamValues([PipelineRunSearchParam.RunType]); const navigate = useNavigate(); @@ -49,11 +52,18 @@ const RunPageFooter: React.FC = ({ data, contextPath }) => { setSubmitting(true); setError(null); handleSubmit(data, api) - .then((path) => { - navigate({ - pathname: `${contextPath}/${path}`, - search: runType ? `?${PipelineRunSearchParam.RunType}=${runType}` : '', - }); + .then((resource) => { + let detailsPath = isRunSchedule(resource) + ? routePipelineRunJobDetails(resource.recurring_run_id) + : routePipelineRunDetails(resource.run_id); + + if (isExperimentsAvailable && experimentId) { + detailsPath = isRunSchedule(resource) + ? resource.recurring_run_id + : resource.run_id; + } + + navigate(`${contextPath}/${detailsPath}`); }) .catch((e) => { setSubmitting(false); diff --git a/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx b/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx index a28f1f6513..b71556d29e 100644 --- a/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx @@ -9,7 +9,8 @@ import { runPageSectionTitles, } from '~/concepts/pipelines/content/createRun/const'; import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; -import { routePipelineRunsNamespace } from '~/routes'; +import { runsBaseRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; interface RunTypeSectionProps { runType: PipelineRunType; @@ -17,8 +18,9 @@ interface RunTypeSectionProps { export const RunTypeSection: React.FC = ({ runType }) => { const navigate = useNavigate(); - const { namespace } = useParams(); + const { namespace, experimentId } = useParams(); const [isAlertOpen, setIsAlertOpen] = React.useState(true); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; let runTypeValue = 'Run once immediately after creation'; let alertProps = { @@ -31,7 +33,7 @@ export const RunTypeSection: React.FC = ({ runType }) => { runTypeValue = 'Schedule recurring run'; alertProps = { title: 'Go to Active runs to create a run that executes once immediately after creation.', - label: `Go to ${PipelineRunTabTitle.Active}`, + label: `Go to ${PipelineRunTabTitle.Active} runs`, navSearch: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Active}`, }; } @@ -54,7 +56,10 @@ export const RunTypeSection: React.FC = ({ runType }) => { variant="link" onClick={() => navigate({ - pathname: routePipelineRunsNamespace(namespace), + pathname: runsBaseRoute( + namespace, + isExperimentsAvailable ? experimentId : undefined, + ), search: alertProps.navSearch, }) } diff --git a/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts b/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts index fbf8c8582c..9108f605e1 100644 --- a/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts +++ b/frontend/src/concepts/pipelines/content/createRun/submitUtils.ts @@ -10,6 +10,8 @@ import { CreatePipelineRunKFData, DateTimeKF, InputDefinitionParameterType, + PipelineRunJobKFv2, + PipelineRunKFv2, PipelineVersionKFv2, RecurringRunMode, RuntimeConfigParameters, @@ -20,12 +22,11 @@ import { isFilledRunFormData, } from '~/concepts/pipelines/content/createRun/utils'; import { convertPeriodicTimeToSeconds } from '~/utilities/time'; -import { routePipelineRunDetails } from '~/routes'; const createRun = async ( formData: SafeRunFormData, createPipelineRun: PipelineAPIs['createPipelineRun'], -): Promise => { +): Promise => { /* eslint-disable camelcase */ const data: CreatePipelineRunKFData = { display_name: formData.nameDesc.name, @@ -42,9 +43,7 @@ const createRun = async ( }; /* eslint-enable camelcase */ - return createPipelineRun({}, data).then((runResource) => - routePipelineRunDetails(runResource.run_id), - ); + return createPipelineRun({}, data); }; const convertDateDataToKFDateTime = (dateData?: RunDateTime): DateTimeKF | null => { @@ -59,7 +58,7 @@ const convertDateDataToKFDateTime = (dateData?: RunDateTime): DateTimeKF | null const createJob = async ( formData: SafeRunFormData, createPipelineRunJob: PipelineAPIs['createPipelineRunJob'], -): Promise => { +): Promise => { if (formData.runType.type !== RunTypeOption.SCHEDULED) { return Promise.reject(new Error('Cannot create a schedule with incomplete data.')); } @@ -103,13 +102,16 @@ const createJob = async ( service_account: '', experiment_id: formData.experiment?.experiment_id || '', }; - /* eslint-enable camelcase */ - return createPipelineRunJob({}, data).then(() => ''); + + return createPipelineRunJob({}, data); }; /** Returns the relative path to navigate to from the namespace qualified route */ -export const handleSubmit = (formData: RunFormData, api: PipelineAPIs): Promise => { +export const handleSubmit = ( + formData: RunFormData, + api: PipelineAPIs, +): Promise => { if (!isFilledRunFormData(formData)) { throw new Error('Form data was incomplete.'); } diff --git a/frontend/src/concepts/pipelines/content/createRun/useCloneRunData.ts b/frontend/src/concepts/pipelines/content/createRun/useCloneRunData.ts index f2a70127bd..1b64908c82 100644 --- a/frontend/src/concepts/pipelines/content/createRun/useCloneRunData.ts +++ b/frontend/src/concepts/pipelines/content/createRun/useCloneRunData.ts @@ -8,9 +8,9 @@ const useCloneRunData = (): [ loaded: boolean, error: Error | undefined, ] => { - const { pipelineRunId, pipelineRunJobId } = useParams(); - const [run, runLoaded, runError] = usePipelineRunById(pipelineRunId); - const [job, jobLoaded, jobError] = usePipelineRunJobById(pipelineRunJobId); + const { runId, recurringRunId } = useParams(); + const [run, runLoaded, runError] = usePipelineRunById(runId); + const [job, jobLoaded, jobError] = usePipelineRunJobById(recurringRunId); if (jobLoaded || jobError) { return [job, jobLoaded, jobError]; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx index c5b6a52338..7791b8a6ff 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails.tsx @@ -38,10 +38,10 @@ import { routePipelineRunsNamespace } from '~/routes'; import PipelineJobReferenceName from '~/concepts/pipelines/content/PipelineJobReferenceName'; const PipelineRunDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, contextPath }) => { - const { pipelineRunId } = useParams(); + const { runId } = useParams(); const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); - const [runResource, runLoaded, runError] = usePipelineRunById(pipelineRunId, true); + const [runResource, runLoaded, runError] = usePipelineRunById(runId, true); const [version, versionLoaded, versionError] = usePipelineVersionById( runResource?.pipeline_version_reference.pipeline_id, runResource?.pipeline_version_reference.pipeline_version_id, diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsActions.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsActions.tsx index 361d9a85ed..1ae570e70b 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsActions.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetailsActions.tsx @@ -5,11 +5,12 @@ import { DropdownSeparator, DropdownToggle, } from '@patternfly/react-core/deprecated'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import useNotification from '~/utilities/useNotification'; import { PipelineRunKFv2, RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; -import { routePipelineRunCloneNamespace } from '~/routes'; +import { cloneRunRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; type PipelineRunDetailsActionsProps = { run?: PipelineRunKFv2 | null; @@ -18,9 +19,11 @@ type PipelineRunDetailsActionsProps = { const PipelineRunDetailsActions: React.FC = ({ onDelete, run }) => { const navigate = useNavigate(); + const { experimentId } = useParams(); const { namespace, api } = usePipelinesAPI(); const notification = useNotification(); const [open, setOpen] = React.useState(false); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; return ( = ({ o , navigate(routePipelineRunCloneNamespace(namespace, run.run_id))} + onClick={() => + navigate( + cloneRunRoute( + namespace, + run.run_id, + isExperimentsAvailable ? experimentId : undefined, + ), + ) + } > Duplicate , diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx index 510fbac0db..bdb13816ec 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails.tsx @@ -39,10 +39,10 @@ const PipelineRunJobDetails: PipelineCoreDetailsPageComponent = ({ breadcrumbPath, contextPath, }) => { - const { pipelineRunJobId } = useParams(); + const { recurringRunId } = useParams(); const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); - const [job, jobLoaded, jobError] = usePipelineRunJobById(pipelineRunJobId); + const [job, jobLoaded, jobError] = usePipelineRunJobById(recurringRunId); const [version, versionLoaded, versionError] = usePipelineVersionById( job?.pipeline_version_reference.pipeline_id, job?.pipeline_version_reference.pipeline_version_id, diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetailsActions.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetailsActions.tsx index 61a16843f5..c472f90ad3 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetailsActions.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetailsActions.tsx @@ -5,12 +5,13 @@ import { DropdownSeparator, DropdownToggle, } from '@patternfly/react-core/deprecated'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { PipelineRunJobKFv2 } from '~/concepts/pipelines/kfTypes'; -import { routePipelineRunJobCloneNamespace } from '~/routes'; +import { cloneScheduleRoute } from '~/routes'; import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; import { PipelineRunType } from '~/pages/pipelines/global/runs'; +import { useIsAreaAvailable, SupportedArea } from '~/concepts/areas'; type PipelineRunJobDetailsActionsProps = { job?: PipelineRunJobKFv2; @@ -24,6 +25,8 @@ const PipelineRunJobDetailsActions: React.FC const navigate = useNavigate(); const { namespace } = usePipelinesAPI(); const [open, setOpen] = React.useState(false); + const { experimentId } = useParams(); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; return ( key="clone-run" onClick={() => navigate({ - pathname: routePipelineRunJobCloneNamespace(namespace, job.recurring_run_id), + pathname: cloneScheduleRoute( + namespace, + job.recurring_run_id, + isExperimentsAvailable ? experimentId : undefined, + ), search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, }) } > - Duplicate run + Duplicate , , onDelete()}> diff --git a/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx b/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx index de4497c3d5..013e4fb135 100644 --- a/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx +++ b/frontend/src/concepts/pipelines/content/tables/PipelineFilterBar.tsx @@ -19,7 +19,7 @@ type FilterOptionRenders = { type Child = React.ReactElement; type PipelineFilterBarProps = { children: Child | Child[]; - filterOptions: Record; + filterOptions: { [key in Options]?: string }; filterOptionRenders: Record React.ReactNode>; filterData: Record; onFilterUpdate: (filterType: Options, value?: string | { label: string; value: string }) => void; diff --git a/frontend/src/concepts/pipelines/content/tables/columns.ts b/frontend/src/concepts/pipelines/content/tables/columns.ts index 3370f10d32..060cc5c73d 100644 --- a/frontend/src/concepts/pipelines/content/tables/columns.ts +++ b/frontend/src/concepts/pipelines/content/tables/columns.ts @@ -16,7 +16,7 @@ export const pipelineColumns: SortableData[] = [ expandTableColumn(), checkboxTableColumn(), { - label: 'Pipeline name', + label: 'Pipeline', field: 'name', sortable: (a, b) => a.display_name.localeCompare(b.display_name), width: 40, @@ -87,16 +87,15 @@ export const experimentColumns: SortableData[] = [ export const pipelineRunColumns: SortableData[] = [ checkboxTableColumn(), { - label: 'Name', + label: 'Run', field: 'name', sortable: true, - width: 20, }, { label: 'Experiment', field: 'experiment', sortable: false, - width: 10, + width: 15, }, { label: 'Pipeline version', @@ -108,16 +107,19 @@ export const pipelineRunColumns: SortableData[] = [ label: 'Started', field: 'created_at', sortable: true, + width: 15, }, { label: 'Duration', field: 'duration', sortable: false, + width: 15, }, { label: 'Status', field: 'status', sortable: true, + width: 10, }, kebabTableColumn(), ]; @@ -125,7 +127,7 @@ export const pipelineRunColumns: SortableData[] = [ export const pipelineRunJobColumns: SortableData[] = [ checkboxTableColumn(), { - label: 'Name', + label: 'Schedule', field: 'name', sortable: true, width: 20, @@ -158,6 +160,7 @@ export const pipelineRunJobColumns: SortableData[] = [ label: 'Created', field: 'created_at', sortable: true, + width: 10, }, kebabTableColumn(), ]; diff --git a/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx index 748cadb4f3..8f558c8d46 100644 --- a/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/experiment/ExperimentTableRow.tsx @@ -1,7 +1,12 @@ -import * as React from 'react'; +import React from 'react'; +import { Link } from 'react-router-dom'; + import { ActionsColumn, IAction, Td, Tr } from '@patternfly/react-table'; + import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes'; import { CheckboxTd } from '~/components/table'; +import { experimentRunsRoute } from '~/routes'; +import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { ExperimentCreated, LastExperimentRuns } from './renderUtils'; type ExperimentTableRowProps = { @@ -16,21 +21,29 @@ const ExperimentTableRow: React.FC = ({ onToggleCheck, experiment, actionColumnItems, -}) => ( - - - {experiment.display_name} - {experiment.description} - - - - - - - - - - -); +}) => { + const { namespace } = usePipelinesAPI(); + + return ( + + + + + {experiment.display_name} + + + {experiment.description} + + + + + + + + + + + ); +}; export default ExperimentTableRow; diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx index 270ae9d62f..136b4975cb 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Button } from '@patternfly/react-core'; import { TableVariant } from '@patternfly/react-table'; @@ -20,8 +20,9 @@ import { BulkArchiveRunModal } from '~/pages/pipelines/global/runs/BulkArchiveRu import { BulkRestoreRunModal } from '~/pages/pipelines/global/runs/BulkRestoreRunModal'; import { ArchiveRunModal } from '~/pages/pipelines/global/runs/ArchiveRunModal'; import { RestoreRunModal } from '~/pages/pipelines/global/runs/RestoreRunModal'; -import { routePipelineRunCreateNamespace } from '~/routes'; import { useSetVersionFilter } from '~/concepts/pipelines/content/tables/useSetVersionFilter'; +import { createRunRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; type PipelineRunTableProps = { runs: PipelineRunKFv2[]; @@ -52,6 +53,7 @@ const PipelineRunTable: React.FC = ({ ...tableProps }) => { const navigate = useNavigate(); + const { experimentId } = useParams(); const { namespace, refreshAllAPI } = usePipelinesAPI(); const filterToolbarProps = usePipelineFilter(setFilter); const { @@ -61,6 +63,7 @@ const PipelineRunTable: React.FC = ({ isSelected, setSelections: setSelectedIds, } = useCheckboxTable(runs.map(({ run_id: runId }) => runId)); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); const [isArchiveModalOpen, setIsArchiveModalOpen] = React.useState(false); const [isRestoreModalOpen, setIsRestoreModalOpen] = React.useState(false); @@ -90,12 +93,14 @@ const PipelineRunTable: React.FC = ({ return ( ); - }, [runType, selectedIds.length, navigate, namespace]); + }, [runType, selectedIds.length, navigate, isExperimentsAvailable, experimentId, namespace]); useSetVersionFilter(filterToolbarProps.onFilterUpdate); @@ -114,7 +119,11 @@ const PipelineRunTable: React.FC = ({ onPerPageSelect={(_, newSize) => setPageSize(newSize)} itemCount={totalSize} data={runs} - columns={pipelineRunColumns} + columns={ + isExperimentsAvailable && experimentId + ? pipelineRunColumns.filter((column) => column.field !== 'experiment') + : pipelineRunColumns + } enablePagination="compact" emptyTableView={ diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx index 039b120db2..0833c5a1a4 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ActionsColumn, IAction, Td, Tr } from '@patternfly/react-table'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { PipelineRunKFv2, RuntimeStateKF } from '~/concepts/pipelines/kfTypes'; import { CheckboxTd } from '~/components/table'; import { @@ -19,7 +19,8 @@ import { PipelineRunType } from '~/pages/pipelines/global/runs'; import { RestoreRunModal } from '~/pages/pipelines/global/runs/RestoreRunModal'; import { ArchiveRunModal } from '~/pages/pipelines/global/runs/ArchiveRunModal'; import { useGetSearchParamValues } from '~/utilities/useGetSearchParamValues'; -import { routePipelineRunCloneNamespace } from '~/routes'; +import { cloneRunRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; type PipelineRunTableRowProps = { isChecked: boolean; @@ -35,6 +36,7 @@ const PipelineRunTableRow: React.FC = ({ run, }) => { const { runType } = useGetSearchParamValues([PipelineRunSearchParam.RunType]); + const { experimentId } = useParams(); const { namespace, api, refreshAllAPI } = usePipelinesAPI(); const notification = useNotification(); const navigate = useNavigate(); @@ -42,12 +44,15 @@ const PipelineRunTableRow: React.FC = ({ const { version, loaded: isVersionLoaded, error: versionError } = usePipelineRunVersionInfo(run); const [isRestoreModalOpen, setIsRestoreModalOpen] = React.useState(false); const [isArchiveModalOpen, setIsArchiveModalOpen] = React.useState(false); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; const actions: IAction[] = React.useMemo(() => { const cloneAction: IAction = { title: 'Duplicate', onClick: () => { - navigate(routePipelineRunCloneNamespace(namespace, run.run_id)); + navigate( + cloneRunRoute(namespace, run.run_id, isExperimentsAvailable ? experimentId : undefined), + ); }, }; @@ -91,15 +96,17 @@ const PipelineRunTableRow: React.FC = ({ }, ]; }, [ - api, - namespace, - notification, - run.run_id, - run.state, runType, + run.state, + run.run_id, navigate, + isExperimentsAvailable, + experimentId, + namespace, onDelete, + api, refreshAllAPI, + notification, ]); return ( @@ -108,7 +115,9 @@ const PipelineRunTableRow: React.FC = ({ - {experiment?.display_name || 'Default'} + {!(isExperimentsAvailable && experimentId) && ( + {experiment?.display_name || 'Default'} + )} = ({ run }) => { const { namespace } = usePipelinesAPI(); + const { experimentId } = useParams(); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; return ( + {run.display_name} } diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableToolbar.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableToolbar.tsx index e544c270ba..2c4dcf9b49 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableToolbar.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableToolbar.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { TextInput, ToolbarItem } from '@patternfly/react-core'; +import { useParams } from 'react-router-dom'; import PipelineFilterBar from '~/concepts/pipelines/content/tables/PipelineFilterBar'; import SimpleDropdownSelect from '~/components/SimpleDropdownSelect'; import { FilterOptions } from '~/concepts/pipelines/content/tables/usePipelineFilter'; @@ -8,14 +9,7 @@ import { RuntimeStateKF, runtimeStateLabels } from '~/concepts/pipelines/kfTypes import DashboardDatePicker from '~/components/DashboardDatePicker'; import PipelineVersionSelect from '~/concepts/pipelines/content/pipelineSelector/CustomPipelineVersionSelect'; import { PipelineRunVersionsContext } from '~/pages/pipelines/global/runs/PipelineRunVersionsContext'; - -const options = { - [FilterOptions.NAME]: 'Name', - [FilterOptions.EXPERIMENT]: 'Experiment', - [FilterOptions.PIPELINE_VERSION]: 'Pipeline version', - [FilterOptions.CREATED_AT]: 'Started', - [FilterOptions.STATUS]: 'Status', -}; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; export type FilterProps = Pick< React.ComponentProps, @@ -33,12 +27,27 @@ const PipelineRunTableToolbar: React.FC = ({ ...toolbarProps }) => { const { versions } = React.useContext(PipelineRunVersionsContext); + const { experimentId } = useParams(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const { [RuntimeStateKF.RUNTIME_STATE_UNSPECIFIED]: unspecifiedState, ...statusRuntimeStates } = runtimeStateLabels; + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; + + const options = React.useMemo( + () => ({ + [FilterOptions.NAME]: 'Run', + ...(!(isExperimentsAvailable && experimentId) && { + [FilterOptions.EXPERIMENT]: 'Experiment', + }), + [FilterOptions.PIPELINE_VERSION]: 'Pipeline version', + [FilterOptions.CREATED_AT]: 'Started', + [FilterOptions.STATUS]: 'Status', + }), + [experimentId, isExperimentsAvailable], + ); return ( - + = ({ job, }) => { const navigate = useNavigate(); + const { experimentId } = useParams(); const { namespace, api, refreshAllAPI } = usePipelinesAPI(); const { version, loaded, error } = usePipelineRunVersionInfo(job); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; return ( @@ -39,7 +42,13 @@ const PipelineRunJobTableRow: React.FC = ({ + {job.display_name} } @@ -79,7 +88,11 @@ const PipelineRunJobTableRow: React.FC = ({ title: 'Duplicate', onClick: () => { navigate({ - pathname: routePipelineRunJobCloneNamespace(namespace, job.recurring_run_id), + pathname: cloneScheduleRoute( + namespace, + job.recurring_run_id, + isExperimentsAvailable ? experimentId : undefined, + ), search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, }); }, diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRunJob/PipelineRunJobTableToolbar.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRunJob/PipelineRunJobTableToolbar.tsx index d18fe0edd6..ce96be42b9 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRunJob/PipelineRunJobTableToolbar.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRunJob/PipelineRunJobTableToolbar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Button, TextInput, ToolbarItem } from '@patternfly/react-core'; @@ -10,10 +10,11 @@ import PipelineVersionSelect from '~/concepts/pipelines/content/pipelineSelector import { PipelineRunVersionsContext } from '~/pages/pipelines/global/runs/PipelineRunVersionsContext'; import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; import { PipelineRunType } from '~/pages/pipelines/global/runs'; -import { routePipelineRunCreateNamespace } from '~/routes'; +import { scheduleRunRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; const options = { - [FilterOptions.NAME]: 'Name', + [FilterOptions.NAME]: 'Schedule', [FilterOptions.PIPELINE_VERSION]: 'Pipeline version', }; @@ -31,8 +32,10 @@ const PipelineRunJobTableToolbar: React.FC = ({ ...toolbarProps }) => { const navigate = useNavigate(); + const { experimentId } = useParams(); const { namespace } = usePipelinesAPI(); const { versions } = React.useContext(PipelineRunVersionsContext); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; return ( @@ -61,7 +64,10 @@ const PipelineRunJobTableToolbar: React.FC = ({ variant="primary" onClick={() => navigate({ - pathname: routePipelineRunCreateNamespace(namespace), + pathname: scheduleRunRoute( + namespace, + isExperimentsAvailable ? experimentId : undefined, + ), search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, }) } diff --git a/frontend/src/concepts/pipelines/content/tables/usePipelineTable.ts b/frontend/src/concepts/pipelines/content/tables/usePipelineTable.ts index 64192dcdab..316d765e7f 100644 --- a/frontend/src/concepts/pipelines/content/tables/usePipelineTable.ts +++ b/frontend/src/concepts/pipelines/content/tables/usePipelineTable.ts @@ -1,6 +1,12 @@ import * as React from 'react'; +import { useParams } from 'react-router-dom'; import { PipelineCoreResourceKFv2 } from '~/concepts/pipelines/kfTypes'; -import { PipelineListPaged, PipelineOptions, PipelinesFilter } from '~/concepts/pipelines/types'; +import { + PipelineListPaged, + PipelineOptions, + PipelineRunOptions, + PipelinesFilter, +} from '~/concepts/pipelines/types'; import { FetchState } from '~/utilities/useFetchState'; export type TableSortProps = { @@ -49,10 +55,11 @@ export const getTablePagingProps = ( const createUsePipelineTable = ( - useState: (options: PipelineOptions) => FetchState>, + useState: (options: PipelineOptions | PipelineRunOptions) => FetchState>, ) => // providing a limit overrides pageSize and prevents paging (limit?: number): [FetchState>, TableProps] => { + const { experimentId } = useParams(); const [page, setPage] = React.useState(1); const [pageSize, setPageSize] = React.useState(limit || 10); const [sortField, setSortField] = React.useState(); @@ -60,7 +67,7 @@ const createUsePipelineTable = const [filter, setFilter] = React.useState(); const [initialLoaded, setInitialLoaded] = React.useState(false); - const state = useState({ page, pageSize, sortField, sortDirection, filter }); + const state = useState({ page, pageSize, sortField, sortDirection, filter, experimentId }); const loaded = state[1]; // Track the first load so that the full page spinner is first shown. diff --git a/frontend/src/concepts/pipelines/types.ts b/frontend/src/concepts/pipelines/types.ts index 3a4757a1da..bfbfa572f8 100644 --- a/frontend/src/concepts/pipelines/types.ts +++ b/frontend/src/concepts/pipelines/types.ts @@ -30,12 +30,10 @@ export type PipelineParams = { sortDirection?: 'asc' | 'desc'; filter?: PipelinesFilter; }; -export type PipelineParamsWithExperiments = PipelineParams & { experiment_id?: string }; +export type PipelineRunParams = PipelineParams & { experimentId?: string }; export type PipelineOptions = Omit & { page?: number }; -export type PipelineOptionsWithExperiments = PipelineOptions & { - experiment_id?: string; -}; +export type PipelineRunOptions = Omit & { page?: number }; export type PipelineListPaged = { totalSize: number; @@ -97,11 +95,11 @@ export type ListPipelines = ( ) => Promise; export type ListPipelineRuns = ( opts: K8sAPIOptions, - params?: PipelineParamsWithExperiments, + params?: PipelineRunParams, ) => Promise; export type ListPipelineRunJobs = ( opts: K8sAPIOptions, - params?: PipelineParams, + params?: PipelineRunParams, ) => Promise; export type ListPipelineVersions = ( opts: K8sAPIOptions, diff --git a/frontend/src/concepts/pipelines/utils.ts b/frontend/src/concepts/pipelines/utils.ts index 790900e9c6..006bb883f9 100644 --- a/frontend/src/concepts/pipelines/utils.ts +++ b/frontend/src/concepts/pipelines/utils.ts @@ -5,6 +5,7 @@ import { } from '~/concepts/pipelines/content/configurePipelinesServer/const'; import { ELYRA_SECRET_NAME } from '~/concepts/pipelines/elyra/const'; import { allSettledPromises } from '~/utilities/allSettledPromises'; +import { PipelineRunJobKFv2, PipelineRunKFv2 } from './kfTypes'; export const deleteServer = async (namespace: string, crName: string): Promise => { const dspa = await getPipelinesCR(namespace, crName); @@ -28,3 +29,7 @@ export const deleteServer = async (namespace: string, crName: string): Promise /^secret-[a-z0-9]{6}$/.test(name); + +export const isRunSchedule = ( + resource: PipelineRunKFv2 | PipelineRunJobKFv2, +): resource is PipelineRunJobKFv2 => !!(resource as PipelineRunJobKFv2).trigger; diff --git a/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx b/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx index 8dbf4c5aca..b9bd1c3216 100644 --- a/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx +++ b/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx @@ -8,6 +8,14 @@ import { experimentsPageTitle, } from '~/pages/pipelines/global/experiments/const'; import GlobalExperiments from '~/pages/pipelines/global/experiments/GlobalExperiments'; +import PipelineRunDetails from '~/concepts/pipelines/content/pipelinesDetails/pipelineRun/PipelineRunDetails'; +import PipelineRunJobDetails from '~/concepts/pipelines/content/pipelinesDetails/pipelineRunJob/PipelineRunJobDetails'; +import { experimentsBaseRoute } from '~/routes'; +import CreateRunPage from '~/concepts/pipelines/content/createRun/CreateRunPage'; +import CloneRunPage from '~/concepts/pipelines/content/createRun/CloneRunPage'; +import { GlobalExperimentDetails } from './global/GlobalPipelineCoreDetails'; +import GlobalPipelineRuns from './global/runs/GlobalPipelineRuns'; +import { ExperimentRunsListBreadcrumb } from './global/experiments/ExperimentRunsListBreadcrumb'; const GlobalPipelineExperimentsRoutes: React.FC = () => ( @@ -17,13 +25,49 @@ const GlobalPipelineExperimentsRoutes: React.FC = () => ( `/pipelineExperiments/${namespace}`} + getInvalidRedirectPath={experimentsBaseRoute} /> } > } /> } /> } /> + } + description="Manage and view your experiment and runs." + getRedirectPath={experimentsBaseRoute} + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + } + /> + } + /> } /> diff --git a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx index bc886bf8d3..baa9c87bea 100644 --- a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx +++ b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx @@ -5,6 +5,8 @@ import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { getProjectDisplayName } from '~/pages/projects/utils'; import { PipelineCoreDetailsPageComponent } from '~/concepts/pipelines/content/types'; import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; +import { experimentRunsRoute, experimentSchedulesRoute, experimentsBaseRoute } from '~/routes'; +import { useExperimentByParams } from './experiments/useExperimentByParams'; type GlobalPipelineCoreDetailsProps = { pageName: string; @@ -38,4 +40,42 @@ const GlobalPipelineCoreDetails: React.FC = ({ ); }; +export const GlobalExperimentDetails: React.FC< + Pick & { + isSchedule?: boolean; + } +> = ({ BreadcrumbDetailsComponent, isSchedule }) => { + const experiment = useExperimentByParams(); + const experimentId = experiment?.experiment_id; + const { namespace, project } = usePipelinesAPI(); + + return ( + + + + Experiments - {getProjectDisplayName(project)} + + , + + {experiment?.display_name ? ( + + {experiment.display_name} + + ) : ( + 'Loading...' + )} + , + ]} + contextPath={ + isSchedule + ? experimentSchedulesRoute(namespace, experimentId) + : experimentRunsRoute(namespace, experimentId) + } + /> + + ); +}; + export default GlobalPipelineCoreDetails; diff --git a/frontend/src/pages/pipelines/global/PipelineCoreApplicationPage.tsx b/frontend/src/pages/pipelines/global/PipelineCoreApplicationPage.tsx index 1eba19f24a..4ccdd5e767 100644 --- a/frontend/src/pages/pipelines/global/PipelineCoreApplicationPage.tsx +++ b/frontend/src/pages/pipelines/global/PipelineCoreApplicationPage.tsx @@ -4,7 +4,7 @@ import NoPipelineServer from '~/concepts/pipelines/NoPipelineServer'; import PipelineCoreProjectSelector from '~/pages/pipelines/global/PipelineCoreProjectSelector'; import { PipelineServerTimedOut, usePipelinesAPI } from '~/concepts/pipelines/context'; -type PipelineCoreApplicationPageProps = { +export type PipelineCoreApplicationPageProps = { children: React.ReactNode; getRedirectPath: (namespace: string) => string; overrideChildPadding?: boolean; diff --git a/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx new file mode 100644 index 0000000000..115f099539 --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Breadcrumb, BreadcrumbItem, Truncate } from '@patternfly/react-core'; + +import { experimentsRootPath } from '~/routes'; +import { useExperimentByParams } from './useExperimentByParams'; + +export const ExperimentRunsListBreadcrumb: React.FC = () => { + const experiment = useExperimentByParams(); + + return ( + + + Experiments + + + + + + + ); +}; diff --git a/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx b/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx index bedb0d368a..ac3528851c 100644 --- a/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx +++ b/frontend/src/pages/pipelines/global/experiments/GlobalExperiments.tsx @@ -5,6 +5,7 @@ import PipelineCoreApplicationPage from '~/pages/pipelines/global/PipelineCoreAp import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; import PipelineAndVersionContextProvider from '~/concepts/pipelines/content/PipelineAndVersionContext'; import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatiblePipelineServer'; +import { experimentsBaseRoute } from '~/routes'; import { ExperimentListTabs, experimentsPageDescription, experimentsPageTitle } from './const'; import GlobalExperimentsTabs from './GlobalExperimentsTabs'; @@ -20,7 +21,7 @@ const GlobalExperiments: React.FC = ({ tab }) => { title={experimentsPageTitle} description={experimentsPageDescription} headerAction={} - getRedirectPath={(namespace) => `/pipelineExperiments/${namespace}`} + getRedirectPath={experimentsBaseRoute} overrideChildPadding > diff --git a/frontend/src/pages/pipelines/global/experiments/GlobalExperimentsTabs.tsx b/frontend/src/pages/pipelines/global/experiments/GlobalExperimentsTabs.tsx index 38c95040f8..e23cf5e547 100644 --- a/frontend/src/pages/pipelines/global/experiments/GlobalExperimentsTabs.tsx +++ b/frontend/src/pages/pipelines/global/experiments/GlobalExperimentsTabs.tsx @@ -3,6 +3,7 @@ import { PageSection, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; import { useNavigate } from 'react-router'; import '~/pages/pipelines/global/runs/GlobalPipelineRunsTabs.scss'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; +import { experimentsTabRoute } from '~/routes'; import { ExperimentListTabs } from './const'; import ActiveExperimentsList from './ActiveExperimentsList'; import ArchivedExperimentsList from './ArchivedExperimentsList'; @@ -18,7 +19,7 @@ const GlobalExperimentsTabs: React.FC = ({ tab }) => return ( navigate(`/pipelineExperiments/${namespace}/${tabId}`)} + onSelect={(_event, tabId) => navigate(experimentsTabRoute(namespace, tabId))} aria-label="Experiments page tabs" role="region" className="odh-pipeline-runs-page-tabs" diff --git a/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts b/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts new file mode 100644 index 0000000000..367c6e75bb --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts @@ -0,0 +1,20 @@ +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { experimentsRootPath } from '~/routes'; +import useExperimentById from '~/concepts/pipelines/apiHooks/useExperimentById'; +import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes'; + +export const useExperimentByParams = (): ExperimentKFv2 | null => { + const navigate = useNavigate(); + const { experimentId } = useParams(); + const [experiment, isExperimentLoaded, experimentError] = useExperimentById(experimentId); + + // Redirect users to the Experiments list page when failing to retrieve the experiment from route params. + React.useEffect(() => { + if (isExperimentLoaded && experimentError) { + navigate(experimentsRootPath); + } + }, [experimentError, isExperimentLoaded, navigate]); + + return experiment; +}; diff --git a/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx b/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx index 1c43768664..943eadfbc7 100644 --- a/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx @@ -17,14 +17,16 @@ import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; import PipelineRunTable from '~/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable'; import { usePipelineActiveRunsTable } from '~/concepts/pipelines/content/tables/pipelineRun/usePipelineRunTable'; import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; -import { routePipelineRunCreateNamespace } from '~/routes'; +import { createRunRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { PipelineRunTabTitle, PipelineRunType } from './types'; export const ActiveRuns: React.FC = () => { const navigate = useNavigate(); - const { namespace } = useParams(); + const { namespace, experimentId } = useParams(); const [[{ items: runs, totalSize }, loaded, error], { initialLoaded, ...tableProps }] = usePipelineActiveRunsTable(); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; if (error) { return ( @@ -59,9 +61,9 @@ export const ActiveRuns: React.FC = () => { /> - To get started, create a run. Alternatively, click the{' '} - {PipelineRunTabTitle.Schedules} tab to create, manage, and execute schedules. A - schedule is a job consisting of one or more recurring runs. + To get started, create a run. Alternatively, go to the{' '} + {PipelineRunTabTitle.Schedules} tab and create a schedule to execute recurring + runs. @@ -70,7 +72,10 @@ export const ActiveRuns: React.FC = () => { variant="primary" onClick={() => navigate({ - pathname: routePipelineRunCreateNamespace(namespace), + pathname: createRunRoute( + namespace, + isExperimentsAvailable ? experimentId : undefined, + ), search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Active}`, }) } diff --git a/frontend/src/pages/pipelines/global/runs/ArchiveRunModal.tsx b/frontend/src/pages/pipelines/global/runs/ArchiveRunModal.tsx index 28bade3010..3ae7d38a0c 100644 --- a/frontend/src/pages/pipelines/global/runs/ArchiveRunModal.tsx +++ b/frontend/src/pages/pipelines/global/runs/ArchiveRunModal.tsx @@ -70,7 +70,7 @@ export const ArchiveRunModal: React.FC = ({ run, onCancel > - The run will be archived and sent to the Archived runs tab, where it can be + The run will be archived and sent to the Archived runs tab, where it can be restored. diff --git a/frontend/src/pages/pipelines/global/runs/ArchivedRuns.tsx b/frontend/src/pages/pipelines/global/runs/ArchivedRuns.tsx index 309b1711d0..caa33ae950 100644 --- a/frontend/src/pages/pipelines/global/runs/ArchivedRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/ArchivedRuns.tsx @@ -49,7 +49,8 @@ export const ArchivedRuns: React.FC = () => { /> - Archive a run before you delete it. Archived runs can be restored later. + Archive a run to remove it from the Active runs tab. Archived runs can be restored + later, or deleted. ); diff --git a/frontend/src/pages/pipelines/global/runs/BulkArchiveRunModal.tsx b/frontend/src/pages/pipelines/global/runs/BulkArchiveRunModal.tsx index 7ac9bac69d..676a87e3b9 100644 --- a/frontend/src/pages/pipelines/global/runs/BulkArchiveRunModal.tsx +++ b/frontend/src/pages/pipelines/global/runs/BulkArchiveRunModal.tsx @@ -71,7 +71,7 @@ export const BulkArchiveRunModal: React.FC = ({ runs, > - {runs.length} runs will be archived and sent to the Archived runs tab. + {runs.length} runs will be archived and sent to the Archived runs tab. diff --git a/frontend/src/pages/pipelines/global/runs/CreateRunEmptyState.tsx b/frontend/src/pages/pipelines/global/runs/CreateRunEmptyState.tsx deleted file mode 100644 index 26ed1802bf..0000000000 --- a/frontend/src/pages/pipelines/global/runs/CreateRunEmptyState.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import { - Button, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStateActions, - EmptyStateHeader, - EmptyStateFooter, -} from '@patternfly/react-core'; -import { PlusCircleIcon } from '@patternfly/react-icons'; -import { useNavigate } from 'react-router-dom'; -import { usePipelinesAPI } from '~/concepts/pipelines/context'; - -type CreateRunEmptyStateProps = { - title: string; - description: string; -}; - -const CreateRunEmptyState: React.FC = ({ title, description }) => { - const { namespace } = usePipelinesAPI(); - const navigate = useNavigate(); - - return ( - - } - headingLevel="h2" - /> - {description} - - - - - - - ); -}; - -export default CreateRunEmptyState; diff --git a/frontend/src/pages/pipelines/global/runs/GlobalPipelineRuns.tsx b/frontend/src/pages/pipelines/global/runs/GlobalPipelineRuns.tsx index 9005ec062c..cf45e7dfa5 100644 --- a/frontend/src/pages/pipelines/global/runs/GlobalPipelineRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/GlobalPipelineRuns.tsx @@ -10,12 +10,24 @@ import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatibl import { routePipelineRunsNamespace } from '~/routes'; import GlobalPipelineRunsTabs from './GlobalPipelineRunsTabs'; -const GlobalPipelineRuns: React.FC = () => ( +type GlobalPipelineRunsProps = Partial< + Pick< + React.ComponentProps, + 'breadcrumb' | 'description' | 'getRedirectPath' + > +>; + +const GlobalPipelineRuns: React.FC = ({ + breadcrumb, + description = pipelineRunsPageDescription, + getRedirectPath = routePipelineRunsNamespace, +}) => ( diff --git a/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx b/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx index 4d71747c86..8af548dedb 100644 --- a/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx +++ b/frontend/src/pages/pipelines/global/runs/GlobalPipelineRunsTabs.tsx @@ -47,26 +47,28 @@ const GlobalPipelineRunsTab: React.FC = () => { + Archived runs} - aria-label={`${PipelineRunTabTitle.Archived} tab`} + eventKey={PipelineRunType.Scheduled} + title={{PipelineRunTabTitle.Schedules}} + aria-label={`${PipelineRunTabTitle.Schedules} tab`} className="odh-pipeline-runs-page-tabs__content" - data-testid="archived-runs-tab" + data-testid="schedules-tab" > - + + {PipelineRunTabTitle.Schedules}} - aria-label={`${PipelineRunTabTitle.Schedules} tab`} + eventKey={PipelineRunType.Archived} + title={{PipelineRunTabTitle.Archived}} + aria-label={`${PipelineRunTabTitle.Archived} tab`} className="odh-pipeline-runs-page-tabs__content" - data-testid="schedules-tab" + data-testid="archived-runs-tab" > - + diff --git a/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx b/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx index 6a4d4244ff..37f5e9488c 100644 --- a/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx @@ -17,14 +17,16 @@ import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; import PipelineRunJobTable from '~/concepts/pipelines/content/tables/pipelineRunJob/PipelineRunJobTable'; import usePipelineRunJobTable from '~/concepts/pipelines/content/tables/pipelineRunJob/usePipelineRunJobTable'; import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; -import { routePipelineRunCreateNamespace } from '~/routes'; +import { scheduleRunRoute } from '~/routes'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { PipelineRunType } from './types'; const ScheduledRuns: React.FC = () => { const navigate = useNavigate(); - const { namespace } = useParams(); + const { namespace, experimentId } = useParams(); const [[{ items: jobs, totalSize }, loaded, error], { initialLoaded, ...tableProps }] = usePipelineRunJobTable(); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; if (error) { return ( @@ -59,8 +61,8 @@ const ScheduledRuns: React.FC = () => { /> - Schedules dictate when and how many times a run is executed. To get started, create a - schedule. + Schedules dictate when and how many times a run is executed. To get started, schedule + runs. @@ -69,7 +71,10 @@ const ScheduledRuns: React.FC = () => { variant="primary" onClick={() => navigate({ - pathname: routePipelineRunCreateNamespace(namespace), + pathname: scheduleRunRoute( + namespace, + isExperimentsAvailable ? experimentId : undefined, + ), search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, }) } diff --git a/frontend/src/pages/pipelines/global/runs/types.ts b/frontend/src/pages/pipelines/global/runs/types.ts index dd6946969f..b85e9324d2 100644 --- a/frontend/src/pages/pipelines/global/runs/types.ts +++ b/frontend/src/pages/pipelines/global/runs/types.ts @@ -5,7 +5,7 @@ export enum PipelineRunType { } export enum PipelineRunTabTitle { - Active = 'Active runs', - Archived = 'Archived runs', + Active = 'Active', + Archived = 'Archived', Schedules = 'Schedules', } diff --git a/frontend/src/routes/pipelines/experiments.ts b/frontend/src/routes/pipelines/experiments.ts new file mode 100644 index 0000000000..23ef2e63b0 --- /dev/null +++ b/frontend/src/routes/pipelines/experiments.ts @@ -0,0 +1,66 @@ +export const experimentsRootPath = '/experiments'; +export const globExperimentsAll = `${experimentsRootPath}/*`; + +export const experimentsBaseRoute = (namespace: string | undefined): string => + !namespace ? experimentsRootPath : `${experimentsRootPath}/${namespace}`; + +export const experimentsTabRoute = ( + namespace: string | undefined, + tabId: string | number, +): string => `${experimentsBaseRoute(namespace)}/${tabId}`; + +export const experimentsCreateRunRoute = ( + namespace: string | undefined, + experimentId: string, +): string => `${experimentRunsRoute(namespace, experimentId)}/create`; + +export const experimentsScheduleRunRoute = ( + namespace: string | undefined, + experimentId: string, +): string => `${experimentSchedulesRoute(namespace, experimentId)}/create`; + +export const experimentsCloneRunRoute = ( + namespace: string | undefined, + experimentId: string, + runId: string, +): string => `${experimentRunsRoute(namespace, experimentId)}/clone/${runId}`; + +export const experimentsCloneScheduleRoute = ( + namespace: string | undefined, + experimentId: string, + recurringRunId: string, +): string => `${experimentSchedulesRoute(namespace, experimentId)}/clone/${recurringRunId}`; + +export const experimentRunsRoute = ( + namespace: string | undefined, + experimentId: string | undefined, +): string => + !experimentId + ? experimentsBaseRoute(namespace) + : `${experimentsBaseRoute(namespace)}/${experimentId}/runs`; + +export const experimentSchedulesRoute = ( + namespace: string | undefined, + experimentId: string | undefined, +): string => + !experimentId + ? experimentsBaseRoute(namespace) + : `${experimentsBaseRoute(namespace)}/${experimentId}/schedules`; + +export const experimentRunDetailsRoute = ( + namespace: string, + experimentId: string | undefined, + runId: string, +): string => + !experimentId || !runId + ? experimentsBaseRoute(namespace) + : `${experimentRunsRoute(namespace, experimentId)}/${runId}`; + +export const experimentScheduleDetailsRoute = ( + namespace: string, + experimentId: string | undefined, + recurringRunId: string, +): string => + !experimentId || !recurringRunId + ? experimentsBaseRoute(namespace) + : `${experimentSchedulesRoute(namespace, experimentId)}/${recurringRunId}`; diff --git a/frontend/src/routes/pipelines/global.ts b/frontend/src/routes/pipelines/global.ts index 5cfdc12c8c..2b6d75b6be 100644 --- a/frontend/src/routes/pipelines/global.ts +++ b/frontend/src/routes/pipelines/global.ts @@ -3,8 +3,8 @@ export const globNamespaceAll = `/${globNamespace}?/*`; const globPipelineId = ':pipelineId'; const globPipelineVersionId = ':pipelineVersionId'; -const globPipelineRunId = ':pipelineRunId'; -const globPipelineRunJobId = ':pipelineRunJobId'; +const globPipelineRunId = ':runId'; +const globPipelineRunJobId = ':recurringRunId'; // pipelines and versions const globPipeline = 'pipeline'; @@ -48,7 +48,8 @@ export const routePipelineRunCloneNamespace = (namespace: string, runId: string) // pipeline run jobs const globPipelineRunJob = 'pipelineRunJob'; -const routePipelineRunJobDetails = (jobId: string): string => `${globPipelineRunJob}/view/${jobId}`; +export const routePipelineRunJobDetails = (jobId: string): string => + `${globPipelineRunJob}/view/${jobId}`; export const globPipelineRunJobDetails = routePipelineRunJobDetails(globPipelineRunJobId); export const routePipelineRunJobDetailsNamespace = (namespace: string, jobId: string): string => `${routePipelineRunsNamespace(namespace)}/${routePipelineRunJobDetails(jobId)}`; diff --git a/frontend/src/routes/pipelines/index.ts b/frontend/src/routes/pipelines/index.ts index cc3f74adbf..ff446d2ad8 100644 --- a/frontend/src/routes/pipelines/index.ts +++ b/frontend/src/routes/pipelines/index.ts @@ -1,2 +1,4 @@ export * from './global'; export * from './project'; +export * from './experiments'; +export * from './runs'; diff --git a/frontend/src/routes/pipelines/runs.ts b/frontend/src/routes/pipelines/runs.ts new file mode 100644 index 0000000000..4849039b1f --- /dev/null +++ b/frontend/src/routes/pipelines/runs.ts @@ -0,0 +1,77 @@ +import { + routePipelineRunCloneNamespace, + routePipelineRunCreateNamespace, + routePipelineRunDetailsNamespace, + routePipelineRunJobCloneNamespace, + routePipelineRunJobDetailsNamespace, + routePipelineRunsNamespace, +} from './global'; +import { + experimentRunsRoute, + experimentRunDetailsRoute, + experimentScheduleDetailsRoute, + experimentsCloneRunRoute, + experimentsCloneScheduleRoute, + experimentsCreateRunRoute, + experimentsScheduleRunRoute, +} from './experiments'; + +export const runsBaseRoute = ( + namespace: string | undefined, + experimentId: string | undefined, +): string => + experimentId + ? experimentRunsRoute(namespace, experimentId) + : routePipelineRunsNamespace(namespace); + +export const cloneScheduleRoute = ( + namespace: string, + recurringRunId: string, + experimentId: string | undefined, +): string => + experimentId + ? experimentsCloneScheduleRoute(namespace, experimentId, recurringRunId) + : routePipelineRunJobCloneNamespace(namespace, recurringRunId); + +export const scheduleRunRoute = ( + namespace: string | undefined, + experimentId: string | undefined, +): string => + experimentId + ? experimentsScheduleRunRoute(namespace, experimentId) + : routePipelineRunCreateNamespace(namespace); + +export const createRunRoute = ( + namespace: string | undefined, + experimentId: string | undefined, +): string => + experimentId + ? experimentsCreateRunRoute(namespace, experimentId) + : routePipelineRunCreateNamespace(namespace); + +export const scheduleDetailsRoute = ( + namespace: string, + recurringRunId: string, + experimentId: string | undefined, +): string => + experimentId + ? experimentScheduleDetailsRoute(namespace, experimentId, recurringRunId) + : routePipelineRunJobDetailsNamespace(namespace, recurringRunId); + +export const runDetailsRoute = ( + namespace: string, + runId: string, + experimentId: string | undefined, +): string => + experimentId + ? experimentRunDetailsRoute(namespace, experimentId, runId) + : routePipelineRunDetailsNamespace(namespace, runId); + +export const cloneRunRoute = ( + namespace: string, + runId: string, + experimentId: string | undefined, +): string => + experimentId + ? experimentsCloneRunRoute(namespace, experimentId, runId) + : routePipelineRunCloneNamespace(namespace, runId); diff --git a/frontend/src/utilities/NavData.tsx b/frontend/src/utilities/NavData.tsx index e01bc41409..b554658472 100644 --- a/frontend/src/utilities/NavData.tsx +++ b/frontend/src/utilities/NavData.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import { useUser } from '~/redux/selectors'; -import { routePipelineRuns, routePipelines } from '~/routes'; +import { experimentsRootPath, routePipelineRuns, routePipelines } from '~/routes'; type NavDataCommon = { id: string; @@ -74,7 +74,7 @@ const useDSPipelinesNav = (): NavDataItem[] => { { id: 'experiments-and-runs', label: 'Experiments and runs', - href: '/pipelineExperiments', // TODO: make sure this is better handled later + href: experimentsRootPath, }, ], });