feat(frontend): Support cloning recurringRun in KFP v2 (#8652)
* Added Recurring run control component in the NewRunV2. * Added recurring run details in apiRun. * Added recurringRun creation logic. * Remove unused comment. * temporarily add small tag v2 followed by "start a new run" for differenciate the diff between v1 and v2 * Add log to check the information in the run / job details. * Get IR from apiJob if the run is a run of recurring run. * Add comments. * Remove console.log() * Add isClone back. * Format * Remove setIsClone * Add unit tests. * Remove unused comment. * 1. Remove unused comment 2. Set default value (as 10) for max concurrent runs 3. Add pre-check for max concurrent runs field 4. Update unit tests * 1. Change job to recurring run in comments 2. Redirect to details page rather than list page if id is existing. * Change naming from job to recurring run. * Fix pre-check message. * Fix pre-check message. * Fix pre-check message. * Update unit tests' snapshots. * 1. Enable to retrieve recurring details for cloning. 2. Hide pipeline section for cloning recurring run 3. Disable the run type switcher for cloning. * Enable cloning runtime config. * Enable cloning run trigger. * 1. Change the error message text. 2. Remove the prop isMaxConcurrentRunValid 3. Update the snapshot 4. Remove react.fragment for simplification * 1. Added negative integer condition for max concurrent run pre-check. 2. Added unit tests. * Remove unused import. * Updated a unit test. * 1. Use trigger prop in existing recurring run as the initial prop of useState(). 2. Add unit tests for both UI-created and SDK-created recurring run. * Remove comments. * remove react.fragment * Remove unused const in unit tests. * Remove unused import item. * Format. * 1. Add pre-check for initial trigger and max concurrent runs. 2. Simplify the logic to determine the source of pipeline in NewRunSwitcher. 3. Wrap PipelineUrlLabel expression as a helper function. 4. Add new error if apiRun and apiRecurringRun exist at the same time. * Define a helper type cloneOrigin to integrate apiRun and apiRecurring run to simplify the logic in the NewRunV2. * Improve the logic in getPipelineDetailsUrl function. * Remove unnecessary useState. Simplify the variable type in getCloneOrigin() and NewRunV2 interface. * Add recurring run object assign logic back.
This commit is contained in:
parent
834d966033
commit
80c0dc50db
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ApiJob, Error>(
|
||||
['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, Error>(
|
||||
['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 (
|
||||
<NewRunV2
|
||||
{...props}
|
||||
namespace={namespace}
|
||||
existingRunId={existingRunId}
|
||||
apiRun={apiRun}
|
||||
originalRecurringRunId={originalRecurringRunId}
|
||||
apiRecurringRun={apiRecurringRun}
|
||||
existingPipeline={apiPipeline}
|
||||
handlePipelineIdChange={setPipelineId}
|
||||
existingPipelineVersion={apiPipelineVersion}
|
||||
|
|
@ -126,6 +158,7 @@ function NewRunSwitcher(props: PageProps) {
|
|||
// TODO(jlyaoyuli): set v2 as default once v1 is deprecated.
|
||||
if (
|
||||
runIsFetching ||
|
||||
recurringRunIsFetching ||
|
||||
pipelineIsFetching ||
|
||||
pipelineVersionIsFetching ||
|
||||
pipelineTemplateStrIsFetching
|
||||
|
|
|
|||
|
|
@ -90,6 +90,22 @@ describe('NewRunV2', () => {
|
|||
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(
|
||||
<CommonTestWrapper>
|
||||
|
|
@ -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(
|
||||
<CommonTestWrapper>
|
||||
<NewRunV2
|
||||
{...generatePropsClonedRun()}
|
||||
existingRunId={null}
|
||||
apiRun={undefined}
|
||||
originalRecurringRunId={TEST_RECURRING_RUN_ID}
|
||||
apiRecurringRun={API_UI_CREATED_NEW_RECURRING_RUN_DETAILS}
|
||||
existingPipeline={undefined}
|
||||
handlePipelineIdChange={jest.fn()}
|
||||
existingPipelineVersion={undefined}
|
||||
handlePipelineVersionIdChange={jest.fn()}
|
||||
templateString={v2YamlTemplateString}
|
||||
chosenExperiment={undefined}
|
||||
/>
|
||||
</CommonTestWrapper>,
|
||||
);
|
||||
|
||||
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(
|
||||
<CommonTestWrapper>
|
||||
<NewRunV2
|
||||
{...generatePropsClonedRun()}
|
||||
existingRunId={null}
|
||||
apiRun={undefined}
|
||||
originalRecurringRunId={TEST_RECURRING_RUN_ID}
|
||||
apiRecurringRun={API_SDK_CREATED_NEW_RECURRING_RUN_DETAILS}
|
||||
existingPipeline={undefined}
|
||||
handlePipelineIdChange={jest.fn()}
|
||||
existingPipelineVersion={undefined}
|
||||
handlePipelineVersionIdChange={jest.fn()}
|
||||
templateString={v2YamlTemplateString}
|
||||
chosenExperiment={undefined}
|
||||
/>
|
||||
</CommonTestWrapper>,
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<CustomRendererProps<string>> = 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<ApiTrigger>();
|
||||
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<PipelineSpecRuntimeConfig>({});
|
||||
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) {
|
|||
<div className={commonCss.scrollContainer}>
|
||||
<div className={commonCss.header}>Run details</div>
|
||||
|
||||
{apiRun && (
|
||||
{cloneOrigin.isClone && (
|
||||
<div>
|
||||
<div>
|
||||
<span>{usePipelineFromRunLabel}</span>
|
||||
</div>
|
||||
<div className={classes(padding(10, 't'))}>
|
||||
{/* TODO(jlyaoyuli): View pipelineDetails from existing recurring run*/}
|
||||
{apiRun && (
|
||||
<Link className={classes(commonCss.link)} to={pipelineDetailsUrl}>
|
||||
<Link
|
||||
className={classes(commonCss.link)}
|
||||
to={getPipelineDetailsUrl(
|
||||
props,
|
||||
cloneOrigin.isRecurring,
|
||||
existingRunId,
|
||||
originalRecurringRunId,
|
||||
)}
|
||||
>
|
||||
[View pipeline]
|
||||
</Link>
|
||||
)}
|
||||
|
|
@ -386,7 +448,7 @@ function NewRunV2(props: NewRunV2Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!apiRun && (
|
||||
{!cloneOrigin.isClone && (
|
||||
<div>
|
||||
{/* Pipeline selection */}
|
||||
<PipelineSelector
|
||||
|
|
@ -508,22 +570,25 @@ function NewRunV2(props: NewRunV2Props) {
|
|||
{/* One-off/Recurring Run Type */}
|
||||
{/* TODO(zijianjoy): Support Recurring Run */}
|
||||
<div className={commonCss.header}>Run Type</div>
|
||||
<>
|
||||
<FormControlLabel
|
||||
id='oneOffToggle'
|
||||
label='One-off'
|
||||
control={<Radio color='primary' />}
|
||||
onChange={() => setIsRecurringRun(false)}
|
||||
checked={!isRecurringRun}
|
||||
/>
|
||||
<FormControlLabel
|
||||
id='recurringToggle'
|
||||
label='Recurring'
|
||||
control={<Radio color='primary' />}
|
||||
onChange={() => setIsRecurringRun(true)}
|
||||
checked={isRecurringRun}
|
||||
/>
|
||||
</>
|
||||
{cloneOrigin.isClone === true && <span>{isRecurringRun ? 'Recurring' : 'One-off'}</span>}
|
||||
{cloneOrigin.isClone === false && (
|
||||
<>
|
||||
<FormControlLabel
|
||||
id='oneOffToggle'
|
||||
label='One-off'
|
||||
control={<Radio color='primary' />}
|
||||
onChange={() => setIsRecurringRun(false)}
|
||||
checked={!isRecurringRun}
|
||||
/>
|
||||
<FormControlLabel
|
||||
id='recurringToggle'
|
||||
label='Recurring'
|
||||
control={<Radio color='primary' />}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in New Issue