diff --git a/frontend/src/components/NewRunParametersV2.tsx b/frontend/src/components/NewRunParametersV2.tsx index 72430562ad..849a3a1784 100644 --- a/frontend/src/components/NewRunParametersV2.tsx +++ b/frontend/src/components/NewRunParametersV2.tsx @@ -54,7 +54,7 @@ interface NewRunParametersProps { pipelineRoot?: string; // ComponentInputsSpec_ParameterSpec specParameters: SpecParameters; - clonedRuntimeConfig: PipelineSpecRuntimeConfig; + clonedRuntimeConfig?: PipelineSpecRuntimeConfig; handlePipelineRootChange?: (pipelineRoot: string) => void; handleParameterChange?: (parameters: RuntimeParameters) => void; setIsValidInput?: (isValid: boolean) => void; @@ -164,7 +164,7 @@ function NewRunParametersV2(props: NewRunParametersProps) { const [updatedParameters, setUpdatedParameters] = useState({}); useEffect(() => { - if (clonedRuntimeConfig.parameters) { + if (clonedRuntimeConfig && clonedRuntimeConfig.parameters) { const clonedRuntimeParametersStr: RuntimeParameters = {}; // Convert cloned parameter to string type first to avoid error from convertInput Object.entries(clonedRuntimeConfig.parameters).forEach(entry => { @@ -181,7 +181,7 @@ function NewRunParametersV2(props: NewRunParametersProps) { setIsValidInput(true); } - if (handleParameterChange) { + if (clonedRuntimeConfig && handleParameterChange) { handleParameterChange(clonedRuntimeConfig.parameters); } return; diff --git a/frontend/src/pages/NewRunSwitcher.tsx b/frontend/src/pages/NewRunSwitcher.tsx index 2371d441b9..b5f0b14343 100644 --- a/frontend/src/pages/NewRunSwitcher.tsx +++ b/frontend/src/pages/NewRunSwitcher.tsx @@ -12,6 +12,7 @@ import { isTemplateV2 } from 'src/lib/v2/WorkflowUtils'; import { ApiPipeline, ApiPipelineVersion } from 'src/apis/pipeline'; import { ApiRunDetail } from 'src/apis/run'; import { ApiExperiment } from 'src/apis/experiment'; +import { ApiJob } from 'src/apis/job'; function NewRunSwitcher(props: PageProps) { const namespace = React.useContext(NamespaceContext); @@ -23,6 +24,7 @@ function NewRunSwitcher(props: PageProps) { // runID query by cloneFromRun will be deprecated once v1 is deprecated. const originalRunId = urlParser.get(QUERY_PARAMS.cloneFromRun); const embeddedRunId = urlParser.get(QUERY_PARAMS.fromRunId); + const originalRecurringRunId = urlParser.get(QUERY_PARAMS.cloneFromRecurringRun); const [pipelineId, setPipelineId] = useState(urlParser.get(QUERY_PARAMS.pipelineId)); const experimentId = urlParser.get(QUERY_PARAMS.experimentId); const [pipelineVersionIdParam, setPipelineVersionIdParam] = useState( @@ -30,7 +32,8 @@ function NewRunSwitcher(props: PageProps) { ); const existingRunId = originalRunId ? originalRunId : embeddedRunId; - const { isSuccess: runIsSuccess, isFetching: runIsFetching, data: apiRun } = useQuery< + // Retrieve run details + const { isSuccess: getRunSuccess, isFetching: runIsFetching, data: apiRun } = useQuery< ApiRunDetail, Error >( @@ -43,7 +46,32 @@ function NewRunSwitcher(props: PageProps) { }, { enabled: !!existingRunId, staleTime: Infinity }, ); - const templateStrFromRunId = apiRun ? apiRun.run?.pipeline_spec?.pipeline_manifest : ''; + + // Retrieve recurring run details + const { + isSuccess: getRecurringRunSuccess, + isFetching: recurringRunIsFetching, + data: apiRecurringRun, + } = useQuery( + ['ApiRecurringRun', originalRecurringRunId], + () => { + if (!originalRecurringRunId) { + throw new Error('Recurring Run ID is missing'); + } + return Apis.jobServiceApi.getJob(originalRecurringRunId); + }, + { enabled: !!originalRecurringRunId, staleTime: Infinity }, + ); + + if (apiRun !== undefined && apiRecurringRun !== undefined) { + throw new Error('The existence of run and recurring run should be exclusive.'); + } + + // template string from cloned object + let pipelineManifest = apiRun?.run?.pipeline_spec?.pipeline_manifest; + if (getRecurringRunSuccess && apiRecurringRun) { + pipelineManifest = apiRecurringRun.pipeline_spec?.pipeline_manifest; + } const { isFetching: pipelineIsFetching, data: apiPipeline } = useQuery( ['ApiPipeline', pipelineId], @@ -99,17 +127,21 @@ function NewRunSwitcher(props: PageProps) { { enabled: !!experimentId, staleTime: Infinity }, ); - const templateString = - templateStrFromRunId === '' ? templateStrFromPipelineId : templateStrFromRunId; + const templateString = pipelineManifest ? pipelineManifest : templateStrFromPipelineId; if (isFeatureEnabled(FeatureKey.V2_ALPHA)) { - if ((runIsSuccess || isTemplatePullSuccessFromPipeline) && isTemplateV2(templateString || '')) { + if ( + (getRunSuccess || getRecurringRunSuccess || isTemplatePullSuccessFromPipeline) && + isTemplateV2(templateString || '') + ) { return ( { name: NEW_TEST_PIPELINE_VERSION_NAME, description: '', }; + const TEST_RESOURCE_REFERENCE = [ + { + key: { + id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', + type: ApiResourceType.EXPERIMENT, + }, + relationship: ApiRelationship.OWNER, + }, + { + key: { + id: ORIGINAL_TEST_PIPELINE_VERSION_ID, + type: ApiResourceType.PIPELINEVERSION, + }, + relationship: ApiRelationship.CREATOR, + }, + ]; // Reponse from BE while POST a run for creating New UI-Run const API_UI_CREATED_NEW_RUN_DETAILS: ApiRunDetail = { @@ -106,37 +122,12 @@ describe('NewRunV2', () => { pipeline_manifest: v2YamlTemplateString, runtime_config: { parameters: { intParam: 123 } }, }, - resource_references: [ - { - key: { - id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', - type: ApiResourceType.EXPERIMENT, - }, - relationship: ApiRelationship.OWNER, - }, - { - key: { - id: ORIGINAL_TEST_PIPELINE_VERSION_ID, - type: ApiResourceType.PIPELINEVERSION, - }, - relationship: ApiRelationship.CREATOR, - }, - ], + resource_references: TEST_RESOURCE_REFERENCE, scheduled_at: new Date('2021-05-17T20:58:23.000Z'), status: 'Succeeded', }, }; - const API_UI_CREATED_NEW_RECURRING_RUN_DETAILS: ApiJob = { - created_at: new Date('2021-05-17T20:58:23.000Z'), - description: 'V2 xgboost', - id: TEST_RECURRING_RUN_ID, - name: 'Run of v2-xgboost-ilbo', - pipeline_spec: { - pipeline_manifest: v2YamlTemplateString, - }, - }; - // Reponse from BE while POST a run for cloning UI-Run const API_UI_CREATED_CLONING_RUN_DETAILS: ApiRunDetail = { pipeline_runtime: { @@ -152,22 +143,7 @@ describe('NewRunV2', () => { pipeline_manifest: v2YamlTemplateString, runtime_config: { parameters: { intParam: 123 } }, }, - resource_references: [ - { - key: { - id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', - type: ApiResourceType.EXPERIMENT, - }, - relationship: ApiRelationship.OWNER, - }, - { - key: { - id: ORIGINAL_TEST_PIPELINE_VERSION_ID, - type: ApiResourceType.PIPELINEVERSION, - }, - relationship: ApiRelationship.CREATOR, - }, - ], + resource_references: TEST_RESOURCE_REFERENCE, scheduled_at: new Date('2022-08-12T20:58:23.000Z'), status: 'Succeeded', }, @@ -231,6 +207,86 @@ describe('NewRunV2', () => { }, }; + const API_UI_CREATED_NEW_RECURRING_RUN_DETAILS: ApiJob = { + created_at: new Date('2021-05-17T20:58:23.000Z'), + description: 'V2 xgboost', + id: TEST_RECURRING_RUN_ID, + name: 'Run of v2-xgboost-ilbo', + pipeline_spec: { + pipeline_manifest: v2YamlTemplateString, + runtime_config: { parameters: { intParam: 123 } }, + }, + resource_references: TEST_RESOURCE_REFERENCE, + trigger: { + periodic_schedule: { interval_second: '3600' }, + }, + max_concurrency: '10', + }; + + const API_UI_CREATED_CLONING_RECURRING_RUN_DETAILS: ApiJob = { + created_at: new Date('2023-01-04T20:58:23.000Z'), + description: 'V2 xgboost', + id: 'test-clone-ui-recurring-run-id', + name: 'Clone of Run of v2-xgboost-ilbo', + pipeline_spec: { + pipeline_manifest: v2YamlTemplateString, + runtime_config: { parameters: { intParam: 123 } }, + }, + resource_references: TEST_RESOURCE_REFERENCE, + trigger: { + periodic_schedule: { interval_second: '3600' }, + }, + max_concurrency: '10', + }; + + const API_SDK_CREATED_NEW_RECURRING_RUN_DETAILS: ApiJob = { + created_at: new Date('2021-05-17T20:58:23.000Z'), + description: 'V2 xgboost', + id: TEST_RECURRING_RUN_ID, + name: 'Run of v2-xgboost-ilbo', + pipeline_spec: { + pipeline_manifest: v2YamlTemplateString, + runtime_config: { parameters: { intParam: 123 } }, + }, + resource_references: [ + { + key: { + id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', + type: ApiResourceType.EXPERIMENT, + }, + relationship: ApiRelationship.OWNER, + }, + ], + trigger: { + periodic_schedule: { interval_second: '3600' }, + }, + max_concurrency: '10', + }; + + const API_SDK_CREATED_CLONING_RECURRING_RUN_DETAILS: ApiJob = { + created_at: new Date('2023-01-04T20:58:23.000Z'), + description: 'V2 xgboost', + id: 'test-clone-ui-recurring-run-id', + name: 'Clone of Run of v2-xgboost-ilbo', + pipeline_spec: { + pipeline_manifest: v2YamlTemplateString, + runtime_config: { parameters: { intParam: 123 } }, + }, + resource_references: [ + { + key: { + id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', + type: ApiResourceType.EXPERIMENT, + }, + relationship: ApiRelationship.OWNER, + }, + ], + trigger: { + periodic_schedule: { interval_second: '3600' }, + }, + max_concurrency: '10', + }; + const DEFAULT_EXPERIMENT: ApiExperiment = { created_at: new Date('2022-07-14T21:26:58Z'), id: '796eb126-dd76-44de-a21f-d70010c6a029', @@ -302,6 +358,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId='e0115ac1-0479-4194-a22d-01e65e09a32b' apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -337,6 +395,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId='e0115ac1-0479-4194-a22d-01e65e09a32b' apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -370,6 +430,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId='e0115ac1-0479-4194-a22d-01e65e09a32b' apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -406,6 +468,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -447,6 +511,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -502,6 +568,8 @@ describe('NewRunV2', () => { namespace='test-ns' existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -546,6 +614,8 @@ describe('NewRunV2', () => { namespace='test-ns' existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -590,6 +660,8 @@ describe('NewRunV2', () => { namespace='test-ns' existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -641,6 +713,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -668,7 +742,7 @@ describe('NewRunV2', () => { }); describe('creating a recurring run', () => { - it('displays run trigger section', async () => { + it('submits a new recurring run', async () => { const createJobSpy = jest.spyOn(Apis.jobServiceApi, 'createJob'); createJobSpy.mockResolvedValue(API_UI_CREATED_NEW_RECURRING_RUN_DETAILS); @@ -678,6 +752,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -730,6 +806,8 @@ describe('NewRunV2', () => { {...generatePropsNewRun()} existingRunId={null} apiRun={undefined} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={ORIGINAL_TEST_PIPELINE} handlePipelineIdChange={jest.fn()} existingPipelineVersion={ORIGINAL_TEST_PIPELINE_VERSION} @@ -831,7 +909,7 @@ describe('NewRunV2', () => { }); }); - describe('cloning a existing run', () => { + describe('cloning an existing run', () => { it('only shows clone run name from original run', () => { render( @@ -839,6 +917,8 @@ describe('NewRunV2', () => { {...generatePropsClonedRun()} existingRunId='e0115ac1-0479-4194-a22d-01e65e09a32b' apiRun={API_UI_CREATED_NEW_RUN_DETAILS} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={undefined} handlePipelineIdChange={jest.fn()} existingPipelineVersion={undefined} @@ -861,6 +941,8 @@ describe('NewRunV2', () => { {...generatePropsClonedRun()} existingRunId={TEST_RUN_ID} apiRun={API_UI_CREATED_NEW_RUN_DETAILS} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={undefined} handlePipelineIdChange={jest.fn()} existingPipelineVersion={undefined} @@ -887,22 +969,7 @@ describe('NewRunV2', () => { pipeline_manifest: undefined, runtime_config: { parameters: { intParam: 123 } }, }, - resource_references: [ - { - key: { - id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', - type: ApiResourceType.EXPERIMENT, - }, - relationship: ApiRelationship.OWNER, - }, - { - key: { - id: ORIGINAL_TEST_PIPELINE_VERSION_ID, - type: ApiResourceType.PIPELINEVERSION, - }, - relationship: ApiRelationship.CREATOR, - }, - ], + resource_references: TEST_RESOURCE_REFERENCE, service_account: '', }), ); @@ -919,6 +986,8 @@ describe('NewRunV2', () => { {...generatePropsClonedRun()} existingRunId={TEST_RUN_ID} apiRun={API_SDK_CREATED_NEW_RUN_DETAILS} + originalRecurringRunId={null} + apiRecurringRun={undefined} existingPipeline={undefined} handlePipelineIdChange={jest.fn()} existingPipelineVersion={undefined} @@ -960,4 +1029,124 @@ describe('NewRunV2', () => { }); }); }); + + describe('clone an existing recurring run', () => { + it('submits a recurring run with same runtimeConfig and trigger from clone UI-created recurring run', async () => { + const createJobSpy = jest.spyOn(Apis.jobServiceApi, 'createJob'); + createJobSpy.mockResolvedValue(API_UI_CREATED_CLONING_RECURRING_RUN_DETAILS); + + render( + + + , + ); + + const startButton = await screen.findByText('Start'); + // Because start button is set false by default + await waitFor(() => { + expect(startButton.closest('button')?.disabled).toEqual(false); + }); + fireEvent.click(startButton); + + await waitFor(() => { + expect(createJobSpy).toHaveBeenCalledWith( + expect.objectContaining({ + description: '', + name: 'Clone of Run of v2-xgboost-ilbo', + pipeline_spec: { + pipeline_manifest: undefined, + runtime_config: { parameters: { intParam: 123 } }, + }, + resource_references: TEST_RESOURCE_REFERENCE, + trigger: { + periodic_schedule: { interval_second: '3600' }, + }, + max_concurrency: '10', + }), + ); + }); + + await waitFor(() => { + expect(updateSnackbarSpy).toHaveBeenLastCalledWith({ + message: 'Successfully started new recurring Run: Clone of Run of v2-xgboost-ilbo', + open: true, + }); + }); + }); + + it('submits a recurring run with same runtimeConfig and trigger from clone SDK-created recurring run', async () => { + const createJobSpy = jest.spyOn(Apis.jobServiceApi, 'createJob'); + createJobSpy.mockResolvedValue(API_SDK_CREATED_CLONING_RECURRING_RUN_DETAILS); + + render( + + + , + ); + + const startButton = await screen.findByText('Start'); + // Because start button is set false by default + await waitFor(() => { + expect(startButton.closest('button')?.disabled).toEqual(false); + }); + fireEvent.click(startButton); + + await waitFor(() => { + expect(createJobSpy).toHaveBeenCalledWith( + expect.objectContaining({ + description: '', + name: 'Clone of Run of v2-xgboost-ilbo', + pipeline_spec: { + pipeline_manifest: v2YamlTemplateString, + runtime_config: { parameters: { intParam: 123 } }, + }, + resource_references: [ + { + key: { + id: '275ea11d-ac63-4ce3-bc33-ec81981ed56b', + type: ApiResourceType.EXPERIMENT, + }, + relationship: ApiRelationship.OWNER, + }, + ], + trigger: { + periodic_schedule: { interval_second: '3600' }, + }, + max_concurrency: '10', + }), + ); + }); + + await waitFor(() => { + expect(updateSnackbarSpy).toHaveBeenLastCalledWith({ + message: 'Successfully started new recurring Run: Clone of Run of v2-xgboost-ilbo', + open: true, + }); + }); + }); + }); }); diff --git a/frontend/src/pages/NewRunV2.tsx b/frontend/src/pages/NewRunV2.tsx index 6f5eb540ee..f86d08e102 100644 --- a/frontend/src/pages/NewRunV2.tsx +++ b/frontend/src/pages/NewRunV2.tsx @@ -28,7 +28,7 @@ import { useMutation } from 'react-query'; import { Link } from 'react-router-dom'; import { ApiExperiment, ApiExperimentStorageState } from 'src/apis/experiment'; import { ApiFilter, PredicateOp } from 'src/apis/filter'; -import { ApiJob, ApiTrigger } from 'src/apis/job'; +import { ApiJob } from 'src/apis/job'; import { ApiPipeline, ApiPipelineVersion } from 'src/apis/pipeline'; import { ApiRelationship, @@ -36,7 +36,6 @@ import { ApiResourceType, ApiRun, ApiRunDetail, - PipelineSpecRuntimeConfig, } from 'src/apis/run'; import BusyButton from 'src/atoms/BusyButton'; import { ExternalLink } from 'src/atoms/ExternalLink'; @@ -77,13 +76,15 @@ const descriptionCustomRenderer: React.FC> = props = interface RunV2Props { namespace?: string; existingRunId: string | null; - apiRun: ApiRunDetail | undefined; - existingPipeline: ApiPipeline | undefined; + apiRun?: ApiRunDetail; + originalRecurringRunId: string | null; + apiRecurringRun?: ApiJob; + existingPipeline?: ApiPipeline; handlePipelineIdChange: (pipelineId: string) => void; - existingPipelineVersion: ApiPipelineVersion | undefined; + existingPipelineVersion?: ApiPipelineVersion; handlePipelineVersionIdChange: (pipelineVersionId: string) => void; - templateString: string | undefined; - chosenExperiment: ApiExperiment | undefined; + templateString?: string; + chosenExperiment?: ApiExperiment; } type NewRunV2Props = RunV2Props & PageProps; @@ -91,24 +92,71 @@ type NewRunV2Props = RunV2Props & PageProps; export type SpecParameters = { [key: string]: ComponentInputsSpec_ParameterSpec }; export type RuntimeParameters = { [key: string]: any }; -function hasVersionID(apiRun: ApiRunDetail | undefined): boolean { - if (!apiRun) { +type CloneOrigin = { + isClone: boolean; + isRecurring: boolean; + run?: ApiRunDetail; + recurringRun?: ApiJob; +}; + +function getCloneOrigin(apiRun?: ApiRunDetail, apiRecurringRun?: ApiJob) { + let cloneOrigin: CloneOrigin = { + isClone: apiRun !== undefined || apiRecurringRun !== undefined, + isRecurring: apiRecurringRun !== undefined, + run: apiRun, + recurringRun: apiRecurringRun, + }; + return cloneOrigin; +} + +function hasVersionID(cloneOrigin: CloneOrigin): boolean { + if (!cloneOrigin.isClone) { return true; } let hasVersionType: boolean = false; - if (apiRun.run?.resource_references) { - apiRun.run.resource_references.forEach(value => { + const existResourceRef = cloneOrigin.isRecurring + ? cloneOrigin.recurringRun?.resource_references + : cloneOrigin.run?.run?.resource_references; + if (existResourceRef) { + existResourceRef.forEach(value => { hasVersionType = hasVersionType || value.key?.type === ApiResourceType.PIPELINEVERSION; }); } return hasVersionType; } +function getPipelineDetailsUrl( + props: NewRunV2Props, + isRecurring: boolean, + existingRunId: string | null, + originalRecurringRunId: string | null, +): string { + const urlParser = new URLParser(props); + + const pipelineDetailsUrlfromRun = existingRunId + ? RoutePage.PIPELINE_DETAILS.replace( + ':' + RouteParams.pipelineId + '/version/:' + RouteParams.pipelineVersionId + '?', + '', + ) + urlParser.build({ [QUERY_PARAMS.fromRunId]: existingRunId }) + : ''; + + const pipelineDetailsUrlfromRecurringRun = originalRecurringRunId + ? RoutePage.PIPELINE_DETAILS.replace( + ':' + RouteParams.pipelineId + '/version/:' + RouteParams.pipelineVersionId + '?', + '', + ) + urlParser.build({ [QUERY_PARAMS.cloneFromRecurringRun]: originalRecurringRunId }) + : ''; + + return isRecurring ? pipelineDetailsUrlfromRecurringRun : pipelineDetailsUrlfromRun; +} + function NewRunV2(props: NewRunV2Props) { // List of elements we need to create Pipeline Run. const { existingRunId, apiRun, + originalRecurringRunId, + apiRecurringRun, existingPipeline, handlePipelineIdChange, existingPipelineVersion, @@ -116,6 +164,7 @@ function NewRunV2(props: NewRunV2Props) { templateString, chosenExperiment, } = props; + const cloneOrigin = getCloneOrigin(apiRun, apiRecurringRun); const [runName, setRunName] = useState(''); const [runDescription, setRunDescription] = useState(''); const [pipelineName, setPipelineName] = useState(''); @@ -131,30 +180,37 @@ function NewRunV2(props: NewRunV2Props) { const [isStartingNewRun, setIsStartingNewRun] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [isParameterValid, setIsParameterValid] = useState(false); - const [isRecurringRun, setIsRecurringRun] = useState(false); - const [trigger, setTrigger] = useState(); - const [maxConcurrentRuns, setMaxConcurrentRuns] = useState('10'); + const [isRecurringRun, setIsRecurringRun] = useState(cloneOrigin.isRecurring); + const initialTrigger = cloneOrigin.recurringRun?.trigger + ? cloneOrigin.recurringRun.trigger + : undefined; + const [trigger, setTrigger] = useState(initialTrigger); + const initialMaxConCurrentRuns = + cloneOrigin.recurringRun?.max_concurrency !== undefined + ? cloneOrigin.recurringRun.max_concurrency + : '10'; + const [maxConcurrentRuns, setMaxConcurrentRuns] = useState(initialMaxConCurrentRuns); const [isMaxConcurrentRunValid, setIsMaxConcurrentRunValid] = useState(true); - const [catchup, setCatchup] = useState(true); - const [clonedRuntimeConfig, setClonedRuntimeConfig] = useState({}); + const initialCatchup = + cloneOrigin.recurringRun?.no_catchup !== undefined + ? !cloneOrigin.recurringRun.no_catchup + : true; + const [needCatchup, setNeedCatchup] = useState(initialCatchup); + const clonedRuntimeConfig = cloneOrigin.isRecurring + ? cloneOrigin.recurringRun?.pipeline_spec?.runtime_config + : cloneOrigin.run?.run?.pipeline_spec?.runtime_config; const urlParser = new URLParser(props); - const usePipelineFromRunLabel = 'Using pipeline from existing run.'; - const pipelineDetailsUrl = existingRunId - ? RoutePage.PIPELINE_DETAILS.replace( - ':' + RouteParams.pipelineId + '/version/:' + RouteParams.pipelineVersionId + '?', - '', - ) + urlParser.build({ [QUERY_PARAMS.fromRunId]: existingRunId }) - : ''; + const labelTextAdjective = isRecurringRun ? 'recurring ' : ''; + const usePipelineFromRunLabel = `Using pipeline from existing ${labelTextAdjective} run.`; const isTemplatePullSuccess = templateString ? true : false; - const apiResourceRefFromRun = apiRun?.run?.resource_references - ? apiRun.run?.resource_references - : undefined; + const existResourceRef = cloneOrigin.isRecurring + ? cloneOrigin.recurringRun?.resource_references + : cloneOrigin.run?.run?.resource_references; - // TODO(jlyaoyuli): support cloning recurring run with query parameter from isRecurring. - const titleVerb = existingRunId ? 'Clone' : 'Start'; - const titleAdjective = existingRunId ? '' : 'new'; + const titleVerb = cloneOrigin.isClone ? 'Clone' : 'Start'; + const titleAdjective = cloneOrigin.isClone ? '' : 'new'; // Title and list of actions on the top of page. useEffect(() => { @@ -165,7 +221,7 @@ function NewRunV2(props: NewRunV2Props) { : `${titleVerb} a ${titleAdjective} run`, }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [isRecurringRun]); // Pre-fill names for pipeline, pipeline version and experiment. useEffect(() => { @@ -188,12 +244,15 @@ function NewRunV2(props: NewRunV2Props) { if (apiRun?.run?.name) { const cloneRunName = 'Clone of ' + apiRun.run.name; setRunName(cloneRunName); + } else if (apiRecurringRun?.name) { + const cloneRecurringName = 'Clone of ' + apiRecurringRun.name; + setRunName(cloneRecurringName); } else if (existingPipelineVersion?.name) { const initRunName = 'Run of ' + existingPipelineVersion.name + ' (' + generateRandomString(5) + ')'; setRunName(initRunName); } - }, [apiRun, existingPipelineVersion]); + }, [apiRun, apiRecurringRun, existingPipelineVersion]); // Set pipeline spec, pipeline root and parameters fields on UI based on returned template. useEffect(() => { @@ -225,12 +284,6 @@ function NewRunV2(props: NewRunV2Props) { } }, [templateString, errorMessage, isParameterValid, isMaxConcurrentRunValid]); - useEffect(() => { - if (apiRun?.run?.pipeline_spec?.runtime_config) { - setClonedRuntimeConfig(apiRun?.run?.pipeline_spec?.runtime_config); - } - }, [apiRun]); - // Whenever any input value changes, validate and show error if needed. // TODO(zijianjoy): Validate run name for now, we need to validate others first. useEffect(() => { @@ -264,7 +317,7 @@ function NewRunV2(props: NewRunV2Props) { relationship: ApiRelationship.OWNER, }); } - if (existingPipelineVersion && hasVersionID(apiRun)) { + if (existingPipelineVersion && hasVersionID(cloneOrigin)) { references.push({ key: { id: existingPipelineVersion.id, @@ -279,7 +332,7 @@ function NewRunV2(props: NewRunV2Props) { name: runName, pipeline_spec: { // FE can only provide either pipeline_manifest or pipeline version - pipeline_manifest: hasVersionID(apiRun) ? undefined : templateString, + pipeline_manifest: hasVersionID(cloneOrigin) ? undefined : templateString, runtime_config: { // TODO(zijianjoy): determine whether to provide pipeline root. pipeline_root: undefined, // pipelineRoot, @@ -287,7 +340,7 @@ function NewRunV2(props: NewRunV2Props) { }, }, //TODO(jlyaoyuli): deprecate the resource reference and use pipeline / workflow manifest - resource_references: apiResourceRefFromRun ? apiResourceRefFromRun : references, + resource_references: existResourceRef ? existResourceRef : references, service_account: serviceAccount, }; @@ -297,7 +350,7 @@ function NewRunV2(props: NewRunV2Props) { ? { enabled: true, max_concurrency: maxConcurrentRuns || '1', - no_catchup: !catchup, + no_catchup: !needCatchup, trigger: trigger, } : { @@ -371,14 +424,23 @@ function NewRunV2(props: NewRunV2Props) {
Run details
- {apiRun && ( + {cloneOrigin.isClone && (
{usePipelineFromRunLabel}
+ {/* TODO(jlyaoyuli): View pipelineDetails from existing recurring run*/} {apiRun && ( - + [View pipeline] )} @@ -386,7 +448,7 @@ function NewRunV2(props: NewRunV2Props) {
)} - {!apiRun && ( + {!cloneOrigin.isClone && (
{/* Pipeline selection */} Run Type
- <> - } - onChange={() => setIsRecurringRun(false)} - checked={!isRecurringRun} - /> - } - onChange={() => setIsRecurringRun(true)} - checked={isRecurringRun} - /> - + {cloneOrigin.isClone === true && {isRecurringRun ? 'Recurring' : 'One-off'}} + {cloneOrigin.isClone === false && ( + <> + } + onChange={() => setIsRecurringRun(false)} + checked={!isRecurringRun} + /> + } + onChange={() => setIsRecurringRun(true)} + checked={isRecurringRun} + /> + + )} {/* Recurring run controls */} {isRecurringRun && ( @@ -535,7 +600,7 @@ function NewRunV2(props: NewRunV2Props) { initialProps={{ trigger: trigger, maxConcurrentRuns: maxConcurrentRuns, - catchup: catchup, + catchup: needCatchup, }} onChange={({ trigger, maxConcurrentRuns, catchup }) => { setTrigger(trigger); @@ -543,7 +608,7 @@ function NewRunV2(props: NewRunV2Props) { setIsMaxConcurrentRunValid( Number.isInteger(Number(maxConcurrentRuns)) && Number(maxConcurrentRuns) > 0, ); - setCatchup(catchup); + setNeedCatchup(catchup); }} />