From c55695faa678efcd3c67ba2160c90e6adb76cea1 Mon Sep 17 00:00:00 2001 From: Jeff Puzzo Date: Fri, 8 Mar 2024 11:32:16 -0500 Subject: [PATCH] [RHOAIENG-3887] Run Schedules - UX cleanup --- .../pipelines/content/createRun/RunForm.tsx | 45 +++++++----- .../pipelines/content/createRun/const.ts | 8 ++- .../contentSections/RunTypeSection.tsx | 69 +++++++++++++++++++ .../pipeline/PipelineDetailsActions.tsx | 37 +++++++++- .../__tests__/useSetVersionFilter.spec.tsx | 42 +++++++++++ .../tables/pipeline/PipelinesTableRow.tsx | 16 +++++ .../tables/pipelineRun/PipelineRunTable.tsx | 30 ++------ .../pipelineRunJob/PipelineRunJobTable.tsx | 3 + .../PipelineVersionTableRow.tsx | 31 +++++++++ .../content/tables/useSetVersionFilter.ts | 32 +++++++++ 10 files changed, 268 insertions(+), 45 deletions(-) create mode 100644 frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx create mode 100644 frontend/src/concepts/pipelines/content/tables/__tests__/useSetVersionFilter.spec.tsx create mode 100644 frontend/src/concepts/pipelines/content/tables/useSetVersionFilter.ts diff --git a/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx b/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx index ead284f4ad..a53ecd09a1 100644 --- a/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx +++ b/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Form, FormGroup, FormSection, Text } from '@patternfly/react-core'; +import { Form, FormSection, Text } from '@patternfly/react-core'; import NameDescriptionField from '~/concepts/k8s/NameDescriptionField'; import { RunFormData, @@ -18,6 +18,7 @@ import ExperimentSection from '~/concepts/pipelines/content/createRun/contentSec import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; import PipelineSection from './contentSections/PipelineSection'; import { CreateRunPageSections, runPageSectionTitles } from './const'; +import { RunTypeSection } from './contentSections/RunTypeSection'; type RunFormProps = { data: RunFormData; @@ -29,7 +30,6 @@ const RunForm: React.FC = ({ data, runType, onValueChange }) => { const [latestVersion] = useLatestPipelineVersion(data.pipeline?.pipeline_id); const selectedVersion = data.version || latestVersion; const paramsRef = React.useRef(data.params); - const isExperimentsAvailable = useIsAreaAvailable(SupportedArea.PIPELINE_EXPERIMENTS).status; const updateInputParams = React.useCallback( @@ -55,17 +55,16 @@ const RunForm: React.FC = ({ data, runType, onValueChange }) => { }, [latestVersion, onValueChange, updateInputParams]); return ( -
{ - e.preventDefault(); - }} - > - - - {getProjectDisplayName(data.project)} - + e.preventDefault()} maxWidth="50%"> + + + + {getProjectDisplayName(data.project)} + = ({ data, runType, onValueChange }) => { setData={(nameDesc) => onValueChange('nameDesc', nameDesc)} /> + {isExperimentsAvailable && ( onValueChange('experiment', experiment)} /> )} + { @@ -90,6 +91,7 @@ const RunForm: React.FC = ({ data, runType, onValueChange }) => { onValueChange('version', undefined); }} /> + = ({ data, runType, onValueChange }) => { updateInputParams(version); }} /> + {runType === PipelineRunType.Scheduled && ( - - onValueChange('runType', { type: RunTypeOption.SCHEDULED, data: scheduleData }) - } - /> + + + onValueChange('runType', { type: RunTypeOption.SCHEDULED, data: scheduleData }) + } + /> + )} + = { + [CreateRunPageSections.RUN_TYPE]: 'Run type', + [CreateRunPageSections.PROJECT]: 'Project', [CreateRunPageSections.NAME_DESC]: 'Name and description', [CreateRunPageSections.EXPERIMENT]: 'Experiment', [CreateRunPageSections.PIPELINE]: 'Pipeline', [CreateRunPageSections.PIPELINE_VERSION]: 'Pipeline version', - [CreateRunPageSections.PARAMS]: 'Pipeline input parameters', + [CreateRunPageSections.SCHEDULE_SETTINGS]: 'Schedule settings', + [CreateRunPageSections.PARAMS]: 'Parameters', }; diff --git a/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx b/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx new file mode 100644 index 0000000000..75ec982566 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/createRun/contentSections/RunTypeSection.tsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import { Alert, AlertActionCloseButton, Button, FormSection } from '@patternfly/react-core'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { PipelineRunTabTitle, PipelineRunType } from '~/pages/pipelines/global/runs'; +import { + CreateRunPageSections, + runPageSectionTitles, +} from '~/concepts/pipelines/content/createRun/const'; +import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; + +interface RunTypeSectionProps { + runType: PipelineRunType; +} + +export const RunTypeSection: React.FC = ({ runType }) => { + const navigate = useNavigate(); + const { namespace } = useParams(); + const [isAlertOpen, setIsAlertOpen] = React.useState(true); + + let runTypeValue = 'Run once immediately after creation'; + let alertProps = { + title: 'Go to Schedules to create schedules that execute recurring runs', + label: `Go to ${PipelineRunTabTitle.Schedules}`, + navSearch: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }; + + if (runType === PipelineRunType.Scheduled) { + 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}`, + navSearch: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Active}`, + }; + } + + return ( + + {runTypeValue} + + {isAlertOpen && ( + + navigate({ + pathname: `/pipelineRuns/${namespace}`, + search: alertProps.navSearch, + }) + } + > + {alertProps.label} + + } + actionClose={ setIsAlertOpen(false)} />} + /> + )} + + ); +}; diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx index e6fe0f4228..c183419ac1 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx @@ -51,7 +51,7 @@ const PipelineDetailsActions: React.FC = ({ setIsVersionImportModalOpen(true)}> Upload new version , - , + , @@ -62,6 +62,23 @@ const PipelineDetailsActions: React.FC = ({ > Create run , + + navigate( + { + pathname: `/pipelineRuns/${namespace}/pipelineRun/create`, + search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }, + { + state: { lastPipeline: pipeline, lastVersion: pipelineVersion }, + }, + ) + } + > + Schedule run + , + , @@ -78,7 +95,23 @@ const PipelineDetailsActions: React.FC = ({ > View runs , - , + + navigate( + { + pathname: `/pipelineRuns/${namespace}`, + search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }, + { + state: { lastVersion: pipelineVersion }, + }, + ) + } + > + View schedules + , + , onDelete()}> Delete pipeline version , diff --git a/frontend/src/concepts/pipelines/content/tables/__tests__/useSetVersionFilter.spec.tsx b/frontend/src/concepts/pipelines/content/tables/__tests__/useSetVersionFilter.spec.tsx new file mode 100644 index 0000000000..08b0a2ad34 --- /dev/null +++ b/frontend/src/concepts/pipelines/content/tables/__tests__/useSetVersionFilter.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { BrowserRouter } from 'react-router-dom'; + +import { renderHook } from '~/__tests__/unit/testUtils/hooks'; +import { useSetVersionFilter } from '~/concepts/pipelines/content/tables/useSetVersionFilter'; +import { PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; + +let mockUseLocationState: { lastVersion: Partial } | undefined; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(() => ({ + state: mockUseLocationState, + })), +})); + +describe('useSetVersionFilter', () => { + const onFilterUpdate = jest.fn(); + + it('does not call onFilterUpdate when there is no router state version', async () => { + renderHook(() => useSetVersionFilter(onFilterUpdate), { + wrapper: ({ children }) => {children}, + }); + + expect(onFilterUpdate).not.toHaveBeenCalled(); + }); + + it('calls onFilterUpdate when router state has a version', () => { + mockUseLocationState = { + // eslint-disable-next-line camelcase + lastVersion: { display_name: 'Some version', pipeline_version_id: 'some-id' }, + }; + renderHook(() => useSetVersionFilter(onFilterUpdate), { + wrapper: ({ children }) => {children}, + }); + + expect(onFilterUpdate).toBeCalledTimes(1); + expect(onFilterUpdate).toHaveBeenCalledWith('pipeline_version', { + label: 'Some version', + value: 'some-id', + }); + }); +}); diff --git a/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx b/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx index f3fbbf067a..928f833075 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx @@ -11,6 +11,8 @@ import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/Pipelines import usePipelineTableRowData from '~/concepts/pipelines/content/tables/pipeline/usePipelineTableRowData'; import { PipelineAndVersionContext } from '~/concepts/pipelines/content/PipelineAndVersionContext'; import { routePipelineRunCreateNamespacePipelinesPage } from '~/routes'; +import { PipelineRunSearchParam } from '~/concepts/pipelines/content/types'; +import { PipelineRunType } from '~/pages/pipelines/global/runs'; const DISABLE_TOOLTIP = 'All child pipeline versions must be deleted before deleting the parent pipeline'; @@ -112,6 +114,20 @@ const PipelinesTableRow: React.FC = ({ }); }, }, + { + title: 'Schedule run', + onClick: () => { + navigate( + { + pathname: `/pipelines/${namespace}/pipelineRun/create`, + search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }, + { + state: { lastPipeline: pipeline }, + }, + ); + }, + }, { isSeparator: true, }, diff --git a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx index c650a4951a..270ae9d62f 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipelineRun/PipelineRunTable.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { Button } from '@patternfly/react-core'; import { TableVariant } from '@patternfly/react-table'; import { TableBase, getTableColumnSort, useCheckboxTable } from '~/components/table'; -import { PipelineRunKFv2, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; +import { PipelineRunKFv2 } from '~/concepts/pipelines/kfTypes'; import { pipelineRunColumns } from '~/concepts/pipelines/content/tables/columns'; import PipelineRunTableRow from '~/concepts/pipelines/content/tables/pipelineRun/PipelineRunTableRow'; import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; @@ -14,15 +14,14 @@ import DeletePipelineRunsModal from '~/concepts/pipelines/content/DeletePipeline import { usePipelinesAPI } from '~/concepts/pipelines/context'; import { PipelineRunType } from '~/pages/pipelines/global/runs/types'; import { PipelinesFilter } from '~/concepts/pipelines/types'; -import usePipelineFilter, { - FilterOptions, -} from '~/concepts/pipelines/content/tables/usePipelineFilter'; +import usePipelineFilter from '~/concepts/pipelines/content/tables/usePipelineFilter'; import SimpleMenuActions from '~/components/SimpleMenuActions'; import { BulkArchiveRunModal } from '~/pages/pipelines/global/runs/BulkArchiveRunModal'; 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'; type PipelineRunTableProps = { runs: PipelineRunKFv2[]; @@ -52,11 +51,9 @@ const PipelineRunTable: React.FC = ({ setFilter, ...tableProps }) => { - const { state } = useLocation(); const navigate = useNavigate(); const { namespace, refreshAllAPI } = usePipelinesAPI(); const filterToolbarProps = usePipelineFilter(setFilter); - const lastLocationPipelineVersion: PipelineVersionKFv2 | undefined = state?.lastVersion; const { selections: selectedIds, tableProps: checkboxTableProps, @@ -77,23 +74,6 @@ const PipelineRunTable: React.FC = ({ return acc; }, []); - // Update filter on initial render with the last location-stored pipeline version. - React.useEffect(() => { - if (lastLocationPipelineVersion) { - filterToolbarProps.onFilterUpdate(FilterOptions.PIPELINE_VERSION, { - label: lastLocationPipelineVersion.display_name, - value: lastLocationPipelineVersion.pipeline_version_id, - }); - } - - return () => { - // Reset the location-stored pipeline version to avoid re-creating - // a filter that might otherwise have been removed/changed by the user. - window.history.replaceState({ ...state, lastVersion: undefined }, ''); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const primaryToolbarAction = React.useMemo(() => { if (runType === PipelineRunType.Archived) { return ( @@ -117,6 +97,8 @@ const PipelineRunTable: React.FC = ({ ); }, [runType, selectedIds.length, navigate, namespace]); + useSetVersionFilter(filterToolbarProps.onFilterUpdate); + return ( <> = ({ } = useCheckboxTable(jobs.map(({ recurring_run_id }) => recurring_run_id)); const [deleteResources, setDeleteResources] = React.useState([]); + useSetVersionFilter(filterToolbarProps.onFilterUpdate); + return ( <> = ({ }); }, }, + { + title: 'Schedule run', + onClick: () => { + navigate( + { + pathname: `/pipelines/${namespace}/pipelineRun/create`, + search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }, + { + state: { lastPipeline: pipeline, lastVersion: version }, + }, + ); + }, + }, + { + isSeparator: true, + }, { title: 'View runs', onClick: () => { @@ -89,6 +106,20 @@ const PipelineVersionTableRow: React.FC = ({ ); }, }, + { + title: 'View schedules', + onClick: () => { + navigate( + { + pathname: `/pipelineRuns/${namespace}`, + search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }, + { + state: { lastVersion: version }, + }, + ); + }, + }, { isSeparator: true, }, diff --git a/frontend/src/concepts/pipelines/content/tables/useSetVersionFilter.ts b/frontend/src/concepts/pipelines/content/tables/useSetVersionFilter.ts new file mode 100644 index 0000000000..c226882bab --- /dev/null +++ b/frontend/src/concepts/pipelines/content/tables/useSetVersionFilter.ts @@ -0,0 +1,32 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; + +import { PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes'; +import { FilterProps } from './PipelineFilterBar'; +import { FilterOptions } from './usePipelineFilter'; + +/** + * Update filter with the last location-stored pipeline version. + * @param onFilterUpdate + */ +export const useSetVersionFilter = (onFilterUpdate: FilterProps['onFilterUpdate']): void => { + const { state } = useLocation(); + const [versionToFilter, setVersionToFilter] = React.useState( + state?.lastVersion, + ); + + React.useEffect(() => { + if (versionToFilter) { + onFilterUpdate(FilterOptions.PIPELINE_VERSION, { + label: versionToFilter.display_name, + value: versionToFilter.pipeline_version_id, + }); + } + + return () => { + // Reset the location-stored pipeline version to avoid re-creating + // a filter that might otherwise have been removed/changed by the user. + setVersionToFilter(undefined); + }; + }, [onFilterUpdate, versionToFilter]); +};