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:
Joe Li 2023-01-14 07:35:20 +00:00 committed by GitHub
parent 834d966033
commit 80c0dc50db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 417 additions and 130 deletions

View File

@ -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;

View File

@ -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

View File

@ -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,
});
});
});
});
});

View File

@ -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);
}}
/>
</>