diff --git a/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx b/frontend/src/concepts/pipelines/content/createRun/RunForm.tsx index 5546a92887..a1aa5a6597 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 42262931fe..d107e88d3d 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/pipeline/PipelineDetailsActions.tsx @@ -56,6 +56,22 @@ const PipelineDetailsActions: React.FC = ({ > Create run , + + navigate( + { + pathname: `/pipelineRuns/${namespace}/pipelineRun/create`, + search: `?${PipelineRunSearchParam.RunType}=${PipelineRunType.Scheduled}`, + }, + { + state: { lastPipeline: pipeline, lastVersion: pipelineVersion }, + }, + ) + } + > + Schedule run + , @@ -72,6 +88,22 @@ 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 334df4c866..a3c0ab19fa 100644 --- a/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx +++ b/frontend/src/concepts/pipelines/content/tables/pipeline/PipelinesTableRow.tsx @@ -10,6 +10,8 @@ import PipelineVersionUploadModal from '~/concepts/pipelines/content/import/Pipe import PipelinesTableRowTime from '~/concepts/pipelines/content/tables/PipelinesTableRowTime'; import usePipelineTableRowData from '~/concepts/pipelines/content/tables/pipeline/usePipelineTableRowData'; import { PipelineAndVersionContext } from '~/concepts/pipelines/content/PipelineAndVersionContext'; +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'; @@ -111,6 +113,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 a9e2e9a799..458461c20e 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,14 +14,13 @@ 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 { useSetVersionFilter } from '~/concepts/pipelines/content/tables/useSetVersionFilter'; type PipelineRunTableProps = { runs: PipelineRunKFv2[]; @@ -51,11 +50,9 @@ const PipelineRunTable: React.FC = ({ setFilter, ...tableProps }) => { - const { state } = useLocation(); const navigate = useNavigate(); const { namespace, refreshAllAPI, getJobInformation } = usePipelinesAPI(); const filterToolbarProps = usePipelineFilter(setFilter); - const lastLocationPipelineVersion: PipelineVersionKFv2 | undefined = state?.lastVersion; const { selections: selectedIds, tableProps: checkboxTableProps, @@ -76,23 +73,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 ( @@ -116,6 +96,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 }, + }, + ); + }, + }, { title: 'View runs', onClick: () => { @@ -88,6 +102,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]); +};