From ce61bac6decb24648ce88454dffcd0acca0636d7 Mon Sep 17 00:00:00 2001 From: Juntao Wang Date: Mon, 3 Jun 2024 12:30:59 -0400 Subject: [PATCH] Refine logic actions for runs on an archived experiment page --- .../tests/mocked/pipelines/experiments.cy.ts | 68 +++++++++++++- .../tests/mocked/pipelines/pipelines.cy.ts | 8 +- .../mocked/pipelines/pipelinesTopology.cy.ts | 2 +- .../pipelines/content/createRun/RunForm.tsx | 1 + .../ProjectAndExperimentSection.tsx | 17 +++- .../content/createRun/submitUtils.ts | 6 +- .../content/createRun/useRunFormData.ts | 20 +++- .../content/experiment/ExperimentSelector.tsx | 43 ++++++--- .../pipelineSelector/useCreateSelectors.ts | 25 +++-- .../pipeline/PipelineDetailsActions.tsx | 2 +- .../tables/experiment/useExperimentTable.ts | 4 +- .../tables/pipeline/PipelinesTableRow.tsx | 2 +- .../tables/pipelineRun/PipelineRunTable.tsx | 42 +++++++-- .../pipelineRun/PipelineRunTableRow.tsx | 32 +++---- .../PipelineRunTableRowExperiment.tsx | 35 +++++++ .../PipelineRunJobTableToolbar.tsx | 31 +------ .../PipelineVersionTableRow.tsx | 2 +- .../pipelines/content/tables/renderUtils.tsx | 4 +- .../GlobalPipelineExperimentsRoutes.tsx | 92 ++++++++++--------- .../global/GlobalPipelineCoreDetails.tsx | 4 +- .../experiments/ExperimentRunsContext.tsx | 40 ++++++++ .../ExperimentRunsListBreadcrumb.tsx | 8 +- .../compareRuns/ManageRunsPage.tsx | 4 +- .../experiments/useExperimentByParams.ts | 7 +- .../pipelines/global/runs/ActiveRuns.tsx | 22 ++++- .../global/runs/CreateScheduleButton.tsx | 46 ++++++++++ .../pipelines/global/runs/ScheduledRuns.tsx | 28 +----- 27 files changed, 421 insertions(+), 174 deletions(-) create mode 100644 frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRowExperiment.tsx create mode 100644 frontend/src/pages/pipelines/global/experiments/ExperimentRunsContext.tsx create mode 100644 frontend/src/pages/pipelines/global/runs/CreateScheduleButton.tsx 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 8749b0185d..3cdcccbbc2 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 @@ -8,11 +8,15 @@ import { buildMockPipelines, mockProjectK8sResource, mockRouteK8sResource, + buildMockRunKF, + buildMockJobKF, } from '~/__mocks__'; import { + archivedRunsTable, archiveExperimentModal, bulkArchiveExperimentModal, bulkRestoreExperimentModal, + pipelineRunJobTable, pipelineRunsGlobal, restoreExperimentModal, } from '~/__tests__/cypress/cypress/pages/pipelines'; @@ -23,6 +27,7 @@ import { ProjectModel, RouteModel, } from '~/__tests__/cypress/cypress/utils/models'; +import { RecurringRunStatus, StorageStateKF } from '~/concepts/pipelines/kfTypes'; const projectName = 'test-project-name'; const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' }); @@ -199,7 +204,7 @@ describe('Experiments', () => { }); }); - describe('Runs page', () => { + describe('Runs page for active experiment', () => { const activeExperimentsTable = experimentsTabs.getActiveExperimentsTable(); const [mockExperiment] = mockExperiments; @@ -240,6 +245,67 @@ describe('Experiments', () => { }); }); +describe('Runs page for archived experiment', () => { + const archivedExperimentsTable = experimentsTabs.getArchivedExperimentsTable(); + const mockExperiment = { ...mockExperiments[0], storage_state: StorageStateKF.ARCHIVED }; + + beforeEach(() => { + initIntercepts(); + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/experiments/:experimentId', + { + path: { + namespace: projectName, + serviceName: 'dspa', + experimentId: mockExperiment.experiment_id, + }, + }, + mockExperiment, + ); + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/runs', + { + path: { namespace: projectName, serviceName: 'dspa' }, + }, + { runs: [buildMockRunKF({ storage_state: StorageStateKF.ARCHIVED })] }, + ); + + cy.interceptOdh( + 'GET /api/service/pipelines/:namespace/:serviceName/apis/v2beta1/recurringruns', + { + path: { namespace: projectName, serviceName: 'dspa' }, + }, + { recurringRuns: [buildMockJobKF({ status: RecurringRunStatus.DISABLED })] }, + ); + experimentsTabs.mockGetExperiments(projectName, [], mockExperiments); + experimentsTabs.visit(projectName); + experimentsTabs.findArchivedTab().click(); + archivedExperimentsTable.getRowByName(mockExperiment.display_name).find().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 empty state on active runs tab', () => { + pipelineRunsGlobal.findActiveRunsTab().click(); + cy.findByTestId('experiment-archived-empty-state').should('be.visible'); + }); + + it('has restore button disabled on archived runs tab', () => { + pipelineRunsGlobal.findArchivedRunsTab().click(); + archivedRunsTable.getRowByName('Test run').findCheckbox().click(); + pipelineRunsGlobal.findRestoreRunButton().should('have.class', 'pf-m-aria-disabled'); + }); + + it('has create schedule button disabled on schedules tab', () => { + pipelineRunsGlobal.findSchedulesTab().click(); + pipelineRunJobTable.getRowByName('Test job').findCheckbox().click(); + pipelineRunsGlobal.findScheduleRunButton().should('have.class', 'pf-m-aria-disabled'); + }); +}); + const initIntercepts = () => { cy.interceptOdh('GET /api/config', mockDashboardConfig({ disablePipelineExperiments: false })); cy.interceptK8sList( diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts index 68dc713fb7..ff685a51cc 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelines.cy.ts @@ -959,7 +959,7 @@ describe('Pipelines', () => { pipelineRow.findExpandButton().click(); pipelineRow .getPipelineVersionRowById(initialMockPipelineVersion.pipeline_version_id) - .findKebabAction('Schedule run') + .findKebabAction('Create schedule') .click(); verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create?runType=scheduled`); @@ -1176,7 +1176,7 @@ export const runScheduleRunPageNavTest = (visitPipelineProjects: () => void): vo pipelinesTable.find(); pipelinesTable .getRowById(initialMockPipeline.pipeline_id) - .findKebabAction('Schedule run') + .findKebabAction('Create schedule') .click(); verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create?runType=scheduled`); @@ -1196,10 +1196,10 @@ export const viewPipelineServerDetailsTest = (visitPipelineProjects: () => void) }), ); visitPipelineProjects(); - viewPipelinelineDetails(); + viewPipelineDetails(); }; -const viewPipelinelineDetails = ( +const viewPipelineDetails = ( accessKey = 'sdsd', secretKey = 'sdsd', endpoint = 'https://s3.amazonaws.com', diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts index eaa7d8a3c4..cd7c324691 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/pipelines/pipelinesTopology.cy.ts @@ -183,7 +183,7 @@ describe('Pipeline topology', () => { }); it('navigates to "Schedule run" page on "Schedule run" click', () => { - pipelineDetails.selectActionDropdownItem('Schedule run'); + pipelineDetails.selectActionDropdownItem('Create schedule'); verifyRelativeURL(`/pipelineRuns/${projectId}/pipelineRun/create?runType=scheduled`); }); diff --git a/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx b/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx index 23eae93b29..bacab2c7db 100644 --- a/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx @@ -57,6 +57,7 @@ const RunForm: React.FC = ({ data, runType, onValueChange }) => { projectName={getProjectDisplayName(data.project)} value={data.experiment} onChange={(experiment) => onValueChange('experiment', experiment)} + isSchedule={isSchedule} /> void; + isSchedule: boolean; }; -const ProjectAndExperimentSection: React.FC = ({ +const ProjectAndExperimentSection: React.FC = ({ projectName, value, onChange, + isSchedule, }) => { const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; @@ -39,7 +44,11 @@ const ProjectAndExperimentSection: React.FC = ({ - + {isSchedule ? ( + + ) : ( + + )} { - if (!formData.experiment && experiment) { + // on create run page, we always check the experiment archived state + // no matter it's duplicated or carried from the create schedules pages + if (formData.runType.type === RunTypeOption.ONE_TRIGGER) { + if (formData.experiment) { + if (formData.experiment.storage_state === StorageStateKF.ARCHIVED) { + setFormValue('experiment', null); + } + } else if (experiment) { + if (experiment.storage_state === StorageStateKF.ARCHIVED) { + setFormValue('experiment', null); + } else { + setFormValue('experiment', experiment); + } + } + } else if (!formData.experiment && experiment) { + // else, on create schedules page, we do what we did before setFormValue('experiment', experiment); } - }, [formData.experiment, setFormValue, experiment]); + }, [formData.experiment, setFormValue, experiment, formData.runType.type]); }; const useUpdatePipelineFormData = ( diff --git a/frontend/src/concepts/pipelines/content/experiment/ExperimentSelector.tsx b/frontend/src/concepts/pipelines/content/experiment/ExperimentSelector.tsx index 332888e1b0..74dbb3d3b9 100644 --- a/frontend/src/concepts/pipelines/content/experiment/ExperimentSelector.tsx +++ b/frontend/src/concepts/pipelines/content/experiment/ExperimentSelector.tsx @@ -20,7 +20,10 @@ import { TableBase, getTableColumnSort } from '~/components/table'; import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes'; import PipelineViewMoreFooterRow from '~/concepts/pipelines/content/tables/PipelineViewMoreFooterRow'; import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; -import { useExperimentSelector } from '~/concepts/pipelines/content/pipelineSelector/useCreateSelectors'; +import { + useActiveExperimentSelector, + useAllExperimentSelector, +} from '~/concepts/pipelines/content/pipelineSelector/useCreateSelectors'; import { experimentSelectorColumns } from '~/concepts/pipelines/content/experiment/columns'; type ExperimentSelectorProps = { @@ -28,21 +31,23 @@ type ExperimentSelectorProps = { onSelect: (experiment: ExperimentKFv2) => void; }; -const ExperimentSelector: React.FC = ({ selection, onSelect }) => { +const InnerExperimentSelector: React.FC< + ReturnType & ExperimentSelectorProps +> = ({ + fetchedSize, + totalSize, + searchProps, + onSearchClear, + onLoadMore, + sortProps, + loaded, + initialLoaded, + data: experiments, + selection, + onSelect, +}) => { const [isOpen, setOpen] = React.useState(false); - const { - fetchedSize, - totalSize, - searchProps, - onSearchClear, - onLoadMore, - sortProps, - loaded, - initialLoaded, - data: experiments, - } = useExperimentSelector(); - const toggleRef = React.useRef(null); const menuRef = React.useRef(null); @@ -140,4 +145,12 @@ const ExperimentSelector: React.FC = ({ selection, onSe ); }; -export default ExperimentSelector; +export const AllExperimentSelector: React.FC = (props) => { + const selectorProps = useAllExperimentSelector(); + return ; +}; + +export const ActiveExperimentSelector: React.FC = (props) => { + const selectorProps = useActiveExperimentSelector(); + return ; +}; diff --git a/frontend/src/concepts/pipelines/content/pipelineSelector/useCreateSelectors.ts b/frontend/src/concepts/pipelines/content/pipelineSelector/useCreateSelectors.ts index cffc62b88b..1cc03c153a 100644 --- a/frontend/src/concepts/pipelines/content/pipelineSelector/useCreateSelectors.ts +++ b/frontend/src/concepts/pipelines/content/pipelineSelector/useCreateSelectors.ts @@ -1,6 +1,8 @@ import * as React from 'react'; import { useSelectorSearch } from '~/concepts/pipelines/content/pipelineSelector/utils'; -import useExperimentTable from '~/concepts/pipelines/content/tables/experiment/useExperimentTable'; +import useExperimentTable, { + useActiveExperimentTable, +} from '~/concepts/pipelines/content/tables/experiment/useExperimentTable'; import usePipelinesTable from '~/concepts/pipelines/content/tables/pipeline/usePipelinesTable'; import usePipelineVersionsTable from '~/concepts/pipelines/content/tables/pipelineVersion/usePipelineVersionsTable'; import { @@ -38,14 +40,19 @@ type UsePipelineSelectorData = { }; }; -export const useExperimentSelector = (): UsePipelineSelectorData => { - const experimentsTable = useExperimentTable(); - const [[{ items: initialData, nextPageToken: initialPageToken }, loaded]] = experimentsTable; - return useCreateSelector( - experimentsTable, - useExperimentLoadMore({ initialData, initialPageToken, loaded }), - ); -}; +export const getExperimentSelector = + (useTable: typeof useExperimentTable) => (): UsePipelineSelectorData => { + const experimentsTable = useTable(); + const [[{ items: initialData, nextPageToken: initialPageToken }, loaded]] = experimentsTable; + return useCreateSelector( + experimentsTable, + useExperimentLoadMore({ initialData, initialPageToken, loaded }), + ); + }; + +export const useAllExperimentSelector = getExperimentSelector(useExperimentTable); + +export const useActiveExperimentSelector = getExperimentSelector(useActiveExperimentTable); export const usePipelineSelector = (): UsePipelineSelectorData => { const pipelinesTable = usePipelinesTable(); diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx index d89ce847d3..b23ae8806d 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx @@ -76,7 +76,7 @@ const PipelineDetailsActions: React.FC = ({ ) } > - Schedule run + Create schedule , , = ({ }, }, { - title: 'Schedule run', + title: 'Create schedule', onClick: () => { navigate( { diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx index d67a36101d..57bf1e6f7f 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx @@ -20,6 +20,7 @@ import { RestoreRunModal } from '~/pages/pipelines/global/runs/RestoreRunModal'; import { useSetVersionFilter } from '~/concepts/pipelines/content/tables/useSetVersionFilter'; import { createRunRoute, experimentsCompareRunsRoute } from '~/routes'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { useContextExperimentArchived } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; type PipelineRunTableProps = { runs: PipelineRunKFv2[]; @@ -74,17 +75,30 @@ const PipelineRunTable: React.FC = ({ return acc; }, []); + const restoreButtonTooltipRef = React.useRef(null); + const isExperimentArchived = useContextExperimentArchived(); + const primaryToolbarAction = React.useMemo(() => { if (runType === PipelineRunType.ARCHIVED) { return ( - + <> + {isExperimentArchived && ( + + )} + + ); } @@ -100,10 +114,18 @@ const PipelineRunTable: React.FC = ({ Create run ); - }, [runType, selectedIds.length, navigate, isExperimentsAvailable, experimentId, namespace]); + }, [ + runType, + selectedIds.length, + navigate, + isExperimentsAvailable, + experimentId, + namespace, + isExperimentArchived, + ]); const compareRunsAction = - isExperimentsAvailable && experimentId ? ( + isExperimentsAvailable && experimentId && !isExperimentArchived ? ( + {dropdownActions} diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineVersion/PipelineVersionTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineVersion/PipelineVersionTableRow.tsx index 0d4a6ce9db..45a3a141a2 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineVersion/PipelineVersionTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineVersion/PipelineVersionTableRow.tsx @@ -76,7 +76,7 @@ const PipelineVersionTableRow: React.FC = ({ }, }, { - title: 'Schedule run', + title: 'Create schedule', onClick: () => { navigate( { diff --git a/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx b/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx index eb627290f5..efb929c5af 100644 --- a/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx +++ b/frontend/src/concepts/pipelines/content/tables/renderUtils.tsx @@ -29,6 +29,7 @@ import { import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { computeRunStatus } from '~/concepts/pipelines/content/utils'; import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/PipelinesTableRowTime'; +import { useContextExperimentArchived } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; export const NoRunContent = (): React.JSX.Element => <>-; @@ -147,6 +148,7 @@ export const RunJobStatus: RunJobUtil<{ onToggle: (value: boolean) => Promise { const [error, setError] = React.useState(null); const [isChangingFlag, setIsChangingFlag] = React.useState(false); + const isExperimentArchived = useContextExperimentArchived(); const isEnabled = job.mode === RecurringRunMode.ENABLE; React.useEffect(() => { @@ -160,7 +162,7 @@ export const RunJobStatus: RunJobUtil<{ onToggle: (value: boolean) => Promise { setIsChangingFlag(true); setError(null); diff --git a/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx b/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx index 135993b28d..27765915c6 100644 --- a/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx +++ b/frontend/src/pages/pipelines/GlobalPipelineExperimentsRoutes.tsx @@ -13,6 +13,7 @@ import PipelineRunJobDetails from '~/concepts/pipelines/content/pipelinesDetails import { experimentsBaseRoute } from '~/routes'; import CreateRunPage from '~/concepts/pipelines/content/createRun/CreateRunPage'; import CloneRunPage from '~/concepts/pipelines/content/createRun/CloneRunPage'; +import ExperimentRunsContextProvider from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; import { GlobalExperimentDetails } from './global/GlobalPipelineCoreDetails'; import GlobalPipelineRuns from './global/runs/GlobalPipelineRuns'; import { ExperimentRunsListBreadcrumb } from './global/experiments/ExperimentRunsListBreadcrumb'; @@ -35,52 +36,59 @@ const GlobalPipelineExperimentsRoutes: React.FC = () => ( } /> } /> } /> - } - description="Manage your experiment runs and schedules." - getRedirectPath={experimentsBaseRoute} + }> + } + description="Manage your experiment runs and schedules." + getRedirectPath={experimentsBaseRoute} + /> + } + /> + } + /> + + } + /> + } + /> + + } + /> + } + /> + } + /> + }> + } /> - } - /> - } - /> - - } - /> - } - /> - } - /> - } - /> - } - /> - }> + } + path="compareRuns/add" + element={} /> - } - /> } /> diff --git a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx index ee38646e8f..1b92deb875 100644 --- a/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx +++ b/frontend/src/pages/pipelines/global/GlobalPipelineCoreDetails.tsx @@ -7,7 +7,7 @@ import { PipelineCoreDetailsPageComponent } from '~/concepts/pipelines/content/t import EnsureAPIAvailability from '~/concepts/pipelines/EnsureAPIAvailability'; import { experimentRunsRoute, experimentSchedulesRoute, experimentsBaseRoute } from '~/routes'; import EnsureCompatiblePipelineServer from '~/concepts/pipelines/EnsureCompatiblePipelineServer'; -import { useExperimentByParams } from './experiments/useExperimentByParams'; +import { ExperimentRunsContext } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; type GlobalPipelineCoreDetailsProps = { pageName: string; @@ -48,7 +48,7 @@ export const GlobalExperimentDetails: React.FC< isSchedule?: boolean; } > = ({ BreadcrumbDetailsComponent, isSchedule }) => { - const experiment = useExperimentByParams(); + const { experiment } = React.useContext(ExperimentRunsContext); const experimentId = experiment?.experiment_id; const { namespace, project } = usePipelinesAPI(); diff --git a/frontend/src/pages/pipelines/global/experiments/ExperimentRunsContext.tsx b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsContext.tsx new file mode 100644 index 0000000000..00d71c582f --- /dev/null +++ b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsContext.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Bullseye, Spinner } from '@patternfly/react-core'; +import { Outlet } from 'react-router-dom'; +import { ExperimentKFv2, StorageStateKF } from '~/concepts/pipelines/kfTypes'; +import { useExperimentByParams } from '~/pages/pipelines/global/experiments/useExperimentByParams'; + +type ExperimentRunsContextState = { + experiment: ExperimentKFv2 | null; +}; + +export const ExperimentRunsContext = React.createContext({ + experiment: null, +}); + +const ExperimentRunsContextProvider: React.FC = () => { + const { experiment, isExperimentLoaded } = useExperimentByParams(); + + const contextValue = React.useMemo(() => ({ experiment }), [experiment]); + + if (!isExperimentLoaded) { + return ( + + + + ); + } + + return ( + + + + ); +}; + +export const useContextExperimentArchived = (): boolean => { + const { experiment } = React.useContext(ExperimentRunsContext); + return experiment?.storage_state === StorageStateKF.ARCHIVED; +}; + +export default ExperimentRunsContextProvider; diff --git a/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx index 115f099539..2e4df9f1bf 100644 --- a/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx +++ b/frontend/src/pages/pipelines/global/experiments/ExperimentRunsListBreadcrumb.tsx @@ -1,12 +1,13 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { Breadcrumb, BreadcrumbItem, Truncate } from '@patternfly/react-core'; +import { Breadcrumb, BreadcrumbItem, Label, Truncate } from '@patternfly/react-core'; import { experimentsRootPath } from '~/routes'; -import { useExperimentByParams } from './useExperimentByParams'; +import { StorageStateKF } from '~/concepts/pipelines/kfTypes'; +import { ExperimentRunsContext } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; export const ExperimentRunsListBreadcrumb: React.FC = () => { - const experiment = useExperimentByParams(); + const { experiment } = React.useContext(ExperimentRunsContext); return ( @@ -16,6 +17,7 @@ export const ExperimentRunsListBreadcrumb: React.FC = () => { + {experiment?.storage_state === StorageStateKF.ARCHIVED && } ); diff --git a/frontend/src/pages/pipelines/global/experiments/compareRuns/ManageRunsPage.tsx b/frontend/src/pages/pipelines/global/experiments/compareRuns/ManageRunsPage.tsx index eff27f2b47..2b9398d297 100644 --- a/frontend/src/pages/pipelines/global/experiments/compareRuns/ManageRunsPage.tsx +++ b/frontend/src/pages/pipelines/global/experiments/compareRuns/ManageRunsPage.tsx @@ -25,7 +25,6 @@ import { experimentsCreateRunRoute, } from '~/routes'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import { useExperimentByParams } from '~/pages/pipelines/global/experiments/useExperimentByParams'; import { getProjectDisplayName } from '~/concepts/projects/utils'; import { usePipelinesAPI } from '~/concepts/pipelines/context'; import usePipelineFilter, { @@ -34,6 +33,7 @@ import usePipelineFilter, { import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes'; import { PipelineRunTabTitle, PipelineRunType } from '~/pages/pipelines/global/runs'; import PipelineRunVersionsContextProvider from '~/pages/pipelines/global/runs/PipelineRunVersionsContext'; +import { ExperimentRunsContext } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; import { ManageRunsTable } from './ManageRunsTable'; interface ManageRunsPageInternalProps { @@ -165,6 +165,6 @@ export const ManageRunsPageInternal: React.FC = ({ }; export const ManageRunsPage: React.FC = () => { - const experiment = useExperimentByParams(); + const { experiment } = React.useContext(ExperimentRunsContext); return experiment ? : null; }; diff --git a/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts b/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts index 367c6e75bb..e908e38332 100644 --- a/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts +++ b/frontend/src/pages/pipelines/global/experiments/useExperimentByParams.ts @@ -4,7 +4,10 @@ import { experimentsRootPath } from '~/routes'; import useExperimentById from '~/concepts/pipelines/apiHooks/useExperimentById'; import { ExperimentKFv2 } from '~/concepts/pipelines/kfTypes'; -export const useExperimentByParams = (): ExperimentKFv2 | null => { +export const useExperimentByParams = (): { + experiment: ExperimentKFv2 | null; + isExperimentLoaded: boolean; +} => { const navigate = useNavigate(); const { experimentId } = useParams(); const [experiment, isExperimentLoaded, experimentError] = useExperimentById(experimentId); @@ -16,5 +19,5 @@ export const useExperimentByParams = (): ExperimentKFv2 | null => { } }, [experimentError, isExperimentLoaded, navigate]); - return experiment; + return { experiment, isExperimentLoaded }; }; diff --git a/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx b/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx index 72f2ba4135..84fd3bcd3f 100644 --- a/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/ActiveRuns.tsx @@ -12,13 +12,14 @@ import { EmptyStateActions, Button, } from '@patternfly/react-core'; -import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import { CubesIcon, 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 { createRunRoute } from '~/routes'; import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { useContextExperimentArchived } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; import { PipelineRunTabTitle, PipelineRunType } from './types'; export const ActiveRuns: React.FC = () => { @@ -27,6 +28,25 @@ export const ActiveRuns: React.FC = () => { const [[{ items: runs, totalSize }, loaded, error], { initialLoaded, ...tableProps }] = usePipelineActiveRunsTable({ experimentId }); const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; + const isExperimentArchived = useContextExperimentArchived(); + + if (isExperimentArchived) { + return ( + + + } + headingLevel="h2" + /> + + When an experiment is archived, its runs are moved to the {PipelineRunTabTitle.ARCHIVED}{' '} + tab. + + + + ); + } if (error) { return ( diff --git a/frontend/src/pages/pipelines/global/runs/CreateScheduleButton.tsx b/frontend/src/pages/pipelines/global/runs/CreateScheduleButton.tsx new file mode 100644 index 0000000000..26fbe6dff6 --- /dev/null +++ b/frontend/src/pages/pipelines/global/runs/CreateScheduleButton.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Button, Tooltip } from '@patternfly/react-core'; +import { useNavigate, useParams } from 'react-router-dom'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; +import { PipelineRunType } from '~/pages/pipelines/global/runs/types'; +import { scheduleRunRoute } from '~/routes'; +import { useContextExperimentArchived } from '~/pages/pipelines/global/experiments/ExperimentRunsContext'; + +const CreateScheduleButton: React.FC = () => { + const navigate = useNavigate(); + const { namespace, experimentId } = useParams(); + const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; + const isExperimentArchived = useContextExperimentArchived(); + const tooltipRef = React.useRef(null); + + return ( + <> + {isExperimentArchived && ( + + )} + + + ); +}; + +export default CreateScheduleButton; diff --git a/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx b/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx index 58c552e706..8a2999c5bf 100644 --- a/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx +++ b/frontend/src/pages/pipelines/global/runs/ScheduledRuns.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { Bullseye, @@ -8,7 +8,6 @@ import { EmptyStateIcon, Spinner, EmptyStateHeader, - Button, EmptyStateActions, EmptyStateFooter, } from '@patternfly/react-core'; @@ -16,17 +15,12 @@ import { ExclamationCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; import PipelineRunJobTable from '~/concepts/pipelines/content/tables/pipelineRunJob/PipelineRunJobTable'; import { usePipelineScheduledRunsTable } from '~/concepts/pipelines/content/tables/pipelineRunJob/usePipelineRunJobTable'; -import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; -import { scheduleRunRoute } from '~/routes'; -import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; -import { PipelineRunType } from './types'; +import CreateScheduleButton from '~/pages/pipelines/global/runs/CreateScheduleButton'; const ScheduledRuns: React.FC = () => { - const navigate = useNavigate(); - const { namespace, experimentId } = useParams(); + const { experimentId } = useParams(); const [[{ items: jobs, totalSize }, loaded, error], { initialLoaded, ...tableProps }] = usePipelineScheduledRunsTable({ experimentId }); - const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; if (error) { return ( @@ -67,21 +61,7 @@ const ScheduledRuns: React.FC = () => { - +