{
+ constructor(props: any) {
+ super(props);
+ }
+
+ public render(): JSX.Element | null {
+ const { handleParamChange, initialParams, titleMessage } = this.props;
+
+ return (
+
+
Run parameters
+
{titleMessage}
+ {!!initialParams.length && (
+
+ {initialParams.map((param, i) =>
+ handleParamChange(i, ev.target.value || '')}
+ style={{ maxWidth: 600 }} className={commonCss.textField}/>)}
+
+ )}
+
+ );
+ }
+}
+
+export default NewRunParameters;
diff --git a/frontend/src/components/Router.tsx b/frontend/src/components/Router.tsx
index 6ef0514b88e..d6c54ed3a73 100644
--- a/frontend/src/components/Router.tsx
+++ b/frontend/src/components/Router.tsx
@@ -47,6 +47,7 @@ const css = stylesheet({
export enum QUERY_PARAMS {
cloneFromRun = 'cloneFromRun',
+ cloneFromRecurringRun = 'cloneFromRecurringRun',
experimentId = 'experimentId',
isRecurring = 'recurring',
firstRunInExperiment = 'firstRunInExperiment',
diff --git a/frontend/src/components/__snapshots__/NewRunParameters.test.tsx.snap b/frontend/src/components/__snapshots__/NewRunParameters.test.tsx.snap
new file mode 100644
index 00000000000..6f6323224fa
--- /dev/null
+++ b/frontend/src/components/__snapshots__/NewRunParameters.test.tsx.snap
@@ -0,0 +1,45 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NewRunParameters does not display any text fields if there are parameters 1`] = `
+
+
+ Run parameters
+
+
+ This pipeline has no parameters
+
+
+`;
+
+exports[`NewRunParameters shows parameters 1`] = `
+
+
+ Run parameters
+
+
+ Specify parameters required by the pipeline
+
+
+
+
+
+`;
diff --git a/frontend/src/lib/Buttons.ts b/frontend/src/lib/Buttons.ts
index 6956d5b54bf..134360e6609 100644
--- a/frontend/src/lib/Buttons.ts
+++ b/frontend/src/lib/Buttons.ts
@@ -59,6 +59,17 @@ export default class Buttons {
};
}
+ public cloneRecurringRun(getSelectedIds: () => string[], useCurrentResource: boolean): ToolbarActionConfig {
+ return {
+ action: () => this._cloneRun(getSelectedIds(), true),
+ disabled: !useCurrentResource,
+ disabledTitle: useCurrentResource ? undefined : 'Select a recurring run to clone',
+ id: 'cloneBtn',
+ title: 'Clone recurring run',
+ tooltip: 'Create a copy from this run\s initial state',
+ };
+ }
+
public collapseSections(action: () => void): ToolbarActionConfig {
return {
action,
@@ -221,10 +232,19 @@ export default class Buttons {
};
}
- private _cloneRun(selectedIds: string[]): void {
+ private _cloneRun(selectedIds: string[], isRecurring?: boolean): void {
if (selectedIds.length === 1) {
const runId = selectedIds[0];
- const searchString = this._urlParser.build({ [QUERY_PARAMS.cloneFromRun]: runId || '' });
+ let searchTerms;
+ if (isRecurring) {
+ searchTerms = {
+ [QUERY_PARAMS.cloneFromRecurringRun]: runId || '',
+ [QUERY_PARAMS.isRecurring]: '1'
+ };
+ } else {
+ searchTerms = { [QUERY_PARAMS.cloneFromRun]: runId || '' };
+ }
+ const searchString = this._urlParser.build(searchTerms);
this._props.history.push(RoutePage.NEW_RUN + searchString);
}
}
diff --git a/frontend/src/lib/RunUtils.ts b/frontend/src/lib/RunUtils.ts
index 6d4ee0a10e2..b7c26ccef15 100644
--- a/frontend/src/lib/RunUtils.ts
+++ b/frontend/src/lib/RunUtils.ts
@@ -15,8 +15,12 @@
*/
import { ApiJob } from '../apis/job';
-import { ApiRun, ApiResourceType, ApiResourceReference } from '../apis/run';
+import { ApiRun, ApiResourceType, ApiResourceReference, ApiRunDetail, ApiPipelineRuntime } from '../apis/run';
import { orderBy } from 'lodash';
+import { ApiParameter } from 'src/apis/pipeline';
+import { Workflow } from 'third_party/argo-ui/argo_template';
+import WorkflowParser from './WorkflowParser';
+import { logger } from './Utils';
export interface MetricMetadata {
count: number;
@@ -25,6 +29,24 @@ export interface MetricMetadata {
name: string;
}
+function getParametersFromRun(run: ApiRunDetail): ApiParameter[] {
+ return getParametersFromRuntime(run.pipeline_runtime);
+}
+
+function getParametersFromRuntime(runtime?: ApiPipelineRuntime): ApiParameter[] {
+ if (!runtime) {
+ return [];
+ }
+
+ try {
+ const workflow = JSON.parse(runtime.workflow_manifest!) as Workflow;
+ return WorkflowParser.getParameters(workflow);
+ } catch (err) {
+ logger.error('Failed to parse runtime workflow manifest', err);
+ return [];
+ }
+}
+
function getPipelineId(run?: ApiRun | ApiJob): string | null {
return (run && run.pipeline_spec && run.pipeline_spec.pipeline_id) || null;
}
@@ -106,6 +128,8 @@ export default {
getAllExperimentReferences,
getFirstExperimentReference,
getFirstExperimentReferenceId,
+ getParametersFromRun,
+ getParametersFromRuntime,
getPipelineId,
getPipelineSpec,
getRecurringRunId,
diff --git a/frontend/src/pages/NewRun.test.tsx b/frontend/src/pages/NewRun.test.tsx
index 4c4df1bb706..db7068e8904 100644
--- a/frontend/src/pages/NewRun.test.tsx
+++ b/frontend/src/pages/NewRun.test.tsx
@@ -29,6 +29,7 @@ class TestNewRun extends NewRun {
public _experimentSelectorClosed = super._experimentSelectorClosed;
public _pipelineSelectorClosed = super._pipelineSelectorClosed;
public _updateRecurringRunState = super._updateRecurringRunState;
+ public _handleParamChange = super._handleParamChange;
}
describe('NewRun', () => {
@@ -63,8 +64,8 @@ describe('NewRun', () => {
function newMockPipeline(): ApiPipeline {
return {
- id: 'some-mock-pipeline-id',
- name: 'some mock pipeline name',
+ id: 'original-run-pipeline-id',
+ name: 'original mock pipeline name',
parameters: [],
};
}
@@ -78,7 +79,8 @@ describe('NewRun', () => {
id: 'some-mock-run-id',
name: 'some mock run name',
pipeline_spec: {
- pipeline_id: 'original-run-pipeline-id'
+ pipeline_id: 'original-run-pipeline-id',
+ workflow_manifest: '{}',
},
},
};
@@ -87,7 +89,7 @@ describe('NewRun', () => {
function newMockRunWithEmbeddedPipeline(): ApiRunDetail {
const runDetail = newMockRunDetail();
delete runDetail.run!.pipeline_spec!.pipeline_id;
- runDetail.run!.pipeline_spec!.workflow_manifest = '{"parameters": []}';
+ runDetail.run!.pipeline_spec!.workflow_manifest = '{"metadata": {"name": "embedded"}, "parameters": []}';
return runDetail;
}
@@ -149,7 +151,7 @@ describe('NewRun', () => {
expect(updateToolbarSpy).toHaveBeenLastCalledWith({
actions: [],
breadcrumbs: [{ displayName: 'Experiments', href: RoutePage.EXPERIMENTS }],
- pageTitle: 'Start a new run',
+ pageTitle: 'Start a run',
});
});
@@ -262,7 +264,7 @@ describe('NewRun', () => {
href: RoutePage.EXPERIMENT_DETAILS.replace(':' + RouteParams.experimentId, MOCK_EXPERIMENT.id!),
},
],
- pageTitle: 'Start a new run',
+ pageTitle: 'Start a run',
});
});
@@ -632,7 +634,7 @@ describe('NewRun', () => {
await TestUtils.flushPromises();
expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({
- message: 'Could not find the cloned run\'s pipeline definition.',
+ message: 'Error: failed to read the clone run\'s pipeline definition. Click Details for more information.',
mode: 'error',
}));
});
@@ -680,53 +682,13 @@ describe('NewRun', () => {
await TestUtils.flushPromises();
expect(updateBannerSpy).toHaveBeenCalledTimes(1);
- expect(tree.state('pipelineFromRun')).toEqual({ parameters: [] });
- expect(tree.state('usePipelineFromRun')).toBe(true);
- });
-
- it('shows switching controls when run has embedded pipeline, selects that pipeline by default,' +
- ' and hides pipeline selector', async () => {
- const props = generateProps();
- props.location.search = `?${QUERY_PARAMS.cloneFromRun}=${MOCK_RUN_WITH_EMBEDDED_PIPELINE.run!.id}`;
-
- getRunSpy.mockImplementation(() => MOCK_RUN_WITH_EMBEDDED_PIPELINE);
-
- tree = shallow( );
- await TestUtils.flushPromises();
- expect(tree).toMatchSnapshot();
- });
-
- it('shows pipeline selector when switching from embedded pipeline to select pipeline', async () => {
- const props = generateProps();
- props.location.search = `?${QUERY_PARAMS.cloneFromRun}=${MOCK_RUN_WITH_EMBEDDED_PIPELINE.run!.id}`;
-
- getRunSpy.mockImplementation(() => MOCK_RUN_WITH_EMBEDDED_PIPELINE);
-
- tree = shallow( );
- await TestUtils.flushPromises();
- tree.find('WithStyles(WithFormControlContext(FormControlLabel))').at(1).simulate('change');
- expect(tree).toMatchSnapshot();
- });
-
- it('resets selected pipeline from embedded when switching to select from pipeline list, and back', async () => {
- const props = generateProps();
- props.location.search = `?${QUERY_PARAMS.cloneFromRun}=${MOCK_RUN_WITH_EMBEDDED_PIPELINE.run!.id}`;
-
- getRunSpy.mockImplementation(() => MOCK_RUN_WITH_EMBEDDED_PIPELINE);
-
- tree = shallow( );
- await TestUtils.flushPromises();
- expect(tree.state('pipeline')).toEqual({ parameters: [] });
- tree.find('WithStyles(WithFormControlContext(FormControlLabel))').at(1).simulate('change');
- expect(tree.state('pipeline')).toBeUndefined();
-
- tree.find('WithStyles(WithFormControlContext(FormControlLabel))').at(0).simulate('change');
- expect(tree.state('pipeline')).toEqual({ parameters: [] });
+ expect(tree.state('workflowFromRun')).toEqual({ metadata: { name: 'embedded' }, parameters: [] });
+ expect(tree.state('useWorkflowFromRun')).toBe(true);
});
it('shows a page error if the original run\'s workflow_manifest is undefined', async () => {
const runDetail = newMockRunDetail();
- runDetail.pipeline_runtime!.workflow_manifest = undefined;
+ runDetail.run!.pipeline_spec!.workflow_manifest = undefined;
const props = generateProps();
props.location.search = `?${QUERY_PARAMS.cloneFromRun}=${runDetail.run!.id}`;
@@ -742,8 +704,8 @@ describe('NewRun', () => {
});
it('shows a page error if the original run\'s workflow_manifest is invalid JSON', async () => {
- const runDetail = newMockRunDetail();
- runDetail.pipeline_runtime!.workflow_manifest = 'not json';
+ const runDetail = newMockRunWithEmbeddedPipeline();
+ runDetail.run!.pipeline_spec!.workflow_manifest = 'not json';
const props = generateProps();
props.location.search = `?${QUERY_PARAMS.cloneFromRun}=${runDetail.run!.id}`;
@@ -753,7 +715,7 @@ describe('NewRun', () => {
await TestUtils.flushPromises();
expect(updateBannerSpy).toHaveBeenLastCalledWith(expect.objectContaining({
- message: 'Error: failed to parse the original run\'s runtime. Click Details for more information.',
+ message: 'Error: failed to read the clone run\'s pipeline definition. Click Details for more information.',
mode: 'error',
}));
});
@@ -778,7 +740,7 @@ describe('NewRun', () => {
tree = shallow( );
await TestUtils.flushPromises();
- expect(tree.state('pipeline')).toHaveProperty('parameters', originalRunPipelineParams);
+ expect(tree.state('parameters')).toEqual(originalRunPipelineParams);
});
@@ -812,8 +774,8 @@ describe('NewRun', () => {
tree = shallow( );
await TestUtils.flushPromises();
- expect(tree.state('usePipelineFromRun')).toBe(true);
- expect(tree.state('usePipelineFromRunLabel')).toBe('Use pipeline from previous step');
+ expect(tree.state('useWorkflowFromRun')).toBe(true);
+ expect(tree.state('usePipelineFromRunLabel')).toBe('Using pipeline from previous page');
expect(tree).toMatchSnapshot();
});
@@ -824,15 +786,15 @@ describe('NewRun', () => {
expect(getRunSpy).toHaveBeenLastCalledWith(MOCK_RUN_WITH_EMBEDDED_PIPELINE.run!.id);
});
- it('parses the embedded pipeline and stores it in state', async () => {
+ it('parses the embedded workflow and stores it in state', async () => {
MOCK_RUN_WITH_EMBEDDED_PIPELINE.run!.pipeline_spec!.workflow_manifest = JSON.stringify(MOCK_PIPELINE);
tree = shallow( );
await TestUtils.flushPromises();
- expect(tree.state('pipeline')).toEqual(MOCK_PIPELINE);
- expect(tree.state('pipelineFromRun')).toEqual(MOCK_PIPELINE);
- expect(tree.state('pipelineName')).toEqual(MOCK_PIPELINE.name);
+ expect(tree.state('workflowFromRun')).toEqual(MOCK_PIPELINE);
+ expect(tree.state('parameters')).toEqual(MOCK_PIPELINE.parameters);
+ expect(tree.state('useWorkflowFromRun')).toBe(true);
});
it('displays a page error if it fails to parse the embedded pipeline', async () => {
@@ -909,7 +871,7 @@ describe('NewRun', () => {
expect(tree.find('#startNewRunBtn').props()).toHaveProperty('disabled', true);
});
- it('sends a request to start a new run when \'Start\' is clicked', async () => {
+ it('sends a request to Start a run when \'Start\' is clicked', async () => {
const props = generateProps();
props.location.search =
`?${QUERY_PARAMS.experimentId}=${MOCK_EXPERIMENT.id}`
@@ -942,7 +904,7 @@ describe('NewRun', () => {
});
});
- it('updates the pipeline in state when a user fills in its params', async () => {
+ it('updates the parameters in state on handleParamChange', async () => {
const props = generateProps();
const pipeline = newMockPipeline();
pipeline.parameters = [
@@ -957,8 +919,7 @@ describe('NewRun', () => {
await TestUtils.flushPromises();
(tree.instance() as TestNewRun).handleChange('runName')({ target: { value: 'test run name' } });
// Fill in the first pipeline parameter
- tree.find('#newRunPipelineParam0').simulate('change', { target: { value: 'test param value' } });
- await TestUtils.flushPromises();
+ (tree.instance() as TestNewRun)._handleParamChange(0, 'test param value');
tree.find('#startNewRunBtn').simulate('click');
// The start APIs are called in a callback triggered by clicking 'Start', so we wait again
@@ -991,13 +952,16 @@ describe('NewRun', () => {
await TestUtils.flushPromises();
expect(startRunSpy).toHaveBeenCalledTimes(1);
- expect(startRunSpy).toHaveBeenLastCalledWith(expect.objectContaining({
+ expect(startRunSpy).toHaveBeenLastCalledWith({
+ description: '',
+ name: 'Clone of ' + MOCK_RUN_WITH_EMBEDDED_PIPELINE.run!.name,
pipeline_spec: {
parameters: [],
pipeline_id: undefined,
- workflow_manifest: '{"parameters":[]}',
+ workflow_manifest: '{"metadata":{"name":"embedded"},"parameters":[]}',
},
- }));
+ resource_references: [],
+ });
expect(tree).toMatchSnapshot();
});
diff --git a/frontend/src/pages/NewRun.tsx b/frontend/src/pages/NewRun.tsx
index 56f96134238..5d2f061fdfa 100644
--- a/frontend/src/pages/NewRun.tsx
+++ b/frontend/src/pages/NewRun.tsx
@@ -23,15 +23,15 @@ import DialogContent from '@material-ui/core/DialogContent';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Input from '../atoms/Input';
import InputAdornment from '@material-ui/core/InputAdornment';
+import NewRunParameters from '../components/NewRunParameters';
import Radio from '@material-ui/core/Radio';
-import RunUtils from '../lib/RunUtils';
import ResourceSelector from './ResourceSelector';
-import TextField, { TextFieldProps } from '@material-ui/core/TextField';
+import RunUtils from '../lib/RunUtils';
+import { TextFieldProps } from '@material-ui/core/TextField';
import Trigger from '../components/Trigger';
-import WorkflowParser from '../lib/WorkflowParser';
import { ApiExperiment } from '../apis/experiment';
-import { ApiPipeline } from '../apis/pipeline';
-import { ApiRun, ApiResourceReference, ApiRelationship, ApiResourceType, ApiRunDetail } from '../apis/run';
+import { ApiPipeline, ApiParameter } from '../apis/pipeline';
+import { ApiRun, ApiResourceReference, ApiRelationship, ApiResourceType, ApiRunDetail, ApiPipelineRuntime } from '../apis/run';
import { ApiTrigger, ApiJob } from '../apis/job';
import { Apis, PipelineSortKeys, ExperimentSortKeys } from '../lib/Apis';
import { Link } from 'react-router-dom';
@@ -51,15 +51,15 @@ interface NewRunState {
experimentName: string;
experimentSelectorOpen: boolean;
isBeingStarted: boolean;
+ isClone: boolean;
isFirstRunInExperiment: boolean;
isRecurringRun: boolean;
maxConcurrentRuns?: string;
+ parameters: ApiParameter[];
pipeline?: ApiPipeline;
// This represents a pipeline from a run that is being cloned, or if a user is creating a run from
// a pipeline that was not uploaded to the system (as in the case of runs created from notebooks).
- // By storing this here instead of in the 'pipeline' field, we won't lose it if the user selects a
- // different pipeline.
- pipelineFromRun?: ApiPipeline;
+ workflowFromRun?: Workflow;
// TODO: this is only here to properly display the name in the text field.
// There is definitely a way to do this that doesn't necessitate this being in state.
// Note: this cannot be undefined/optional or the label animation for the input field will not
@@ -70,7 +70,7 @@ interface NewRunState {
trigger?: ApiTrigger;
unconfirmedSelectedExperiment?: ApiExperiment;
unconfirmedSelectedPipeline?: ApiPipeline;
- usePipelineFromRun: boolean;
+ useWorkflowFromRun: boolean;
usePipelineFromRunLabel: string;
}
@@ -106,13 +106,15 @@ class NewRun extends Page<{}, NewRunState> {
experimentName: '',
experimentSelectorOpen: false,
isBeingStarted: false,
+ isClone: false,
isFirstRunInExperiment: false,
isRecurringRun: false,
+ parameters: [],
pipelineName: '',
pipelineSelectorOpen: false,
runName: '',
- usePipelineFromRun: false,
- usePipelineFromRunLabel: 'Use pipeline from cloned run',
+ usePipelineFromRunLabel: 'Using pipeline from cloned run',
+ useWorkflowFromRun: false,
};
}
@@ -126,27 +128,29 @@ class NewRun extends Page<{}, NewRunState> {
public render(): JSX.Element {
const {
- pipelineFromRun,
+ workflowFromRun,
description,
errorMessage,
experimentName,
experimentSelectorOpen,
- isRecurringRun,
+ isClone,
isFirstRunInExperiment,
- pipeline,
+ isRecurringRun,
+ parameters,
pipelineName,
pipelineSelectorOpen,
runName,
unconfirmedSelectedExperiment,
unconfirmedSelectedPipeline,
- usePipelineFromRun,
usePipelineFromRunLabel,
+ useWorkflowFromRun,
} = this.state;
- const originalRunId = new URLParser(this.props).get(QUERY_PARAMS.cloneFromRun);
+ const urlParser = new URLParser(this.props);
+ const originalRunId = urlParser.get(QUERY_PARAMS.cloneFromRun) || urlParser.get(QUERY_PARAMS.fromRunId);
const pipelineDetailsUrl = originalRunId
? RoutePage.PIPELINE_DETAILS.replace(':' + RouteParams.pipelineId + '?', '') +
- new URLParser(this.props).build({ [QUERY_PARAMS.fromRunId]: originalRunId })
+ urlParser.build({ [QUERY_PARAMS.fromRunId]: originalRunId })
: '';
return (
@@ -156,16 +160,13 @@ class NewRun extends Page<{}, NewRunState> {
Run details
{/* Pipeline selection */}
- {!!pipelineFromRun && (
- }
- onChange={() => this.setStateSafe({ pipeline: pipelineFromRun, usePipelineFromRun: true })}
- checked={usePipelineFromRun} />
- {!!originalRunId && [View pipeline]}
- }
- onChange={() => this.setStateSafe({ pipeline: undefined, usePipelineFromRun: false })}
- checked={!usePipelineFromRun} />
- )}
- {!usePipelineFromRun && (
+ {!!workflowFromRun && (
+
+ {usePipelineFromRunLabel}
+ {!!originalRunId && [View pipeline]}
+
+ )}
+ {!useWorkflowFromRun && (
{
}}
/>
- {/* One-off/Recurring Toggle */}
+ {/* One-off/Recurring Run Type */}
Run Type
- }
- onChange={() => this._updateRecurringRunState(false)}
- checked={!isRecurringRun} />
- }
- onChange={() => this._updateRecurringRunState(true)}
- checked={isRecurringRun} />
+ {isClone && (
+ {isRecurringRun ? 'Recurring' : 'One-off'}
+ )}
+ {!isClone && (
+
+ }
+ onChange={() => this._updateRecurringRunState(false)}
+ checked={!isRecurringRun} />
+ }
+ onChange={() => this._updateRecurringRunState(true)}
+ checked={isRecurringRun} />
+
+ )}
{/* Recurring run controls */}
{isRecurringRun && (
@@ -295,17 +303,11 @@ class NewRun extends Page<{}, NewRunState> {
)}
{/* Run parameters form */}
- Run parameters
- {this._runParametersMessage(pipeline)}
- {pipeline && Array.isArray(pipeline.parameters) && !!pipeline.parameters.length && (
-
- {pipeline.parameters.map((param, i) =>
- this._handleParamChange(i, ev.target.value || '')}
- style={{ maxWidth: 600 }} className={commonCss.textField}/>)}
-
- )}
+
{/* Create/Cancel buttons */}
@@ -344,21 +346,42 @@ class NewRun extends Page<{}, NewRunState> {
// Get clone run id from querystring if any
const originalRunId = urlParser.get(QUERY_PARAMS.cloneFromRun);
- // If we are not cloning, we may have an embedded pipeline from a run from a Notebook
+ const originalRecurringRunId = urlParser.get(QUERY_PARAMS.cloneFromRecurringRun);
+ // If we are not cloning from an existing run, we may have an embedded pipeline from a run from
+ // a notebook. This is a somewhat hidden path that can be reached via the following steps:
+ // 1. Create a pipeline and run it from a notebook
+ // 2. Click [View Pipeline] for this run from one of the list pages
+ // (Now you will be viewing a pipeline details page for a pipeline that hasn't been uploaded)
+ // 3. Click Create run
const embeddedPipelineRunId = urlParser.get(QUERY_PARAMS.fromRunId);
if (originalRunId) {
+ // If we are cloning a run, fetch the original
+ try {
+ const originalRun = await Apis.runServiceApi.getRun(originalRunId);
+ await this._prepareFormFromClone(originalRun.run, originalRun.pipeline_runtime);
+ // If the querystring did not contain an experiment ID, try to get one from the run.
+ if (!experimentId) {
+ experimentId = RunUtils.getFirstExperimentReferenceId(originalRun.run);
+ }
+ } catch (err) {
+ await this.showPageError(`Error: failed to retrieve original run: ${originalRunId}.`, err);
+ logger.error(`Failed to retrieve original run: ${originalRunId}`, err);
+ }
+
+
+ } else if (originalRecurringRunId) {
+ // If we are cloning a recurring run, fetch the original
try {
- const originalRun = await Apis.runServiceApi.getRun(originalRunId);
+ const originalRun = await Apis.jobServiceApi.getJob(originalRecurringRunId);
await this._prepareFormFromClone(originalRun);
- // If the querystring did not contain an experiment ID, try to get one from the run.
if (!experimentId) {
- experimentId = RunUtils.getFirstExperimentReferenceId(originalRun.run);
+ experimentId = RunUtils.getFirstExperimentReferenceId(originalRun);
}
} catch (err) {
- await this.showPageError(`Error: failed to retrieve original run: ${originalRunId}.`, err);
- logger.error(`Failed to retrieve original run: ${originalRunId}`, err);
+ await this.showPageError(`Error: failed to retrieve original recurring run: ${originalRunId}.`, err);
+ logger.error(`Failed to retrieve original recurring run: ${originalRunId}`, err);
}
- } else if(embeddedPipelineRunId) {
+ } else if (embeddedPipelineRunId) {
this._prepareFormFromEmbeddedPipeline(embeddedPipelineRunId);
} else {
// Get pipeline id from querystring if any
@@ -366,7 +389,11 @@ class NewRun extends Page<{}, NewRunState> {
if (possiblePipelineId) {
try {
const pipeline = await Apis.pipelineServiceApi.getPipeline(possiblePipelineId);
- this.setStateSafe({ pipeline, pipelineName: (pipeline && pipeline.name) || '' });
+ this.setStateSafe({
+ parameters: pipeline.parameters || [],
+ pipeline,
+ pipelineName: (pipeline && pipeline.name) || ''
+ });
} catch (err) {
urlParser.clear(QUERY_PARAMS.pipelineId);
await this.showPageError(
@@ -395,11 +422,10 @@ class NewRun extends Page<{}, NewRunState> {
}
const isRecurringRun = urlParser.get(QUERY_PARAMS.isRecurring) === '1';
- this.props.updateToolbar({
- actions: this.props.toolbarProps.actions,
- breadcrumbs,
- pageTitle: isRecurringRun ? 'Start a recurring run' : 'Start a new run',
- });
+ const titleVerb = originalRunId ? 'Clone' : 'Start';
+ const pageTitle = isRecurringRun ? `${titleVerb} a recurring run` : `${titleVerb} a run`;
+
+ this.props.updateToolbar({ actions: this.props.toolbarProps.actions, breadcrumbs, pageTitle });
this.setStateSafe({
experiment,
@@ -430,12 +456,14 @@ class NewRun extends Page<{}, NewRunState> {
}
protected async _pipelineSelectorClosed(confirmed: boolean): Promise {
- let { pipeline } = this.state;
+ let { parameters, pipeline } = this.state;
if (confirmed && this.state.unconfirmedSelectedPipeline) {
pipeline = this.state.unconfirmedSelectedPipeline;
+ parameters = pipeline.parameters || [];
}
this.setStateSafe({
+ parameters,
pipeline,
pipelineName: (pipeline && pipeline.name) || '',
pipelineSelectorOpen: false
@@ -449,11 +477,18 @@ class NewRun extends Page<{}, NewRunState> {
this.setStateSafe({ isRecurringRun });
}
+ protected _handleParamChange(index: number, value: string): void {
+ const { parameters } = this.state;
+ parameters[index].value = value;
+ this.setStateSafe({ parameters });
+ }
+
private async _prepareFormFromEmbeddedPipeline(embeddedPipelineRunId: string): Promise {
let embeddedPipelineSpec: string | null;
+ let runWithEmbeddedPipeline: ApiRunDetail;
try {
- const runWithEmbeddedPipeline = await Apis.runServiceApi.getRun(embeddedPipelineRunId);
+ runWithEmbeddedPipeline = await Apis.runServiceApi.getRun(embeddedPipelineRunId);
embeddedPipelineSpec = RunUtils.getPipelineSpec(runWithEmbeddedPipeline.run);
} catch (err) {
await this.showPageError(
@@ -469,13 +504,13 @@ class NewRun extends Page<{}, NewRunState> {
}
try {
- const pipeline = JSON.parse(embeddedPipelineSpec);
+ const workflow: Workflow = JSON.parse(embeddedPipelineSpec);
+ const parameters = RunUtils.getParametersFromRun(runWithEmbeddedPipeline);
this.setStateSafe({
- pipeline,
- pipelineFromRun: pipeline,
- pipelineName: (pipeline && pipeline.name) || '',
- usePipelineFromRun: true,
- usePipelineFromRunLabel: 'Use pipeline from previous step',
+ parameters,
+ usePipelineFromRunLabel: 'Using pipeline from previous page',
+ useWorkflowFromRun: true,
+ workflowFromRun: workflow,
});
} catch (err) {
await this.showPageError(
@@ -487,78 +522,75 @@ class NewRun extends Page<{}, NewRunState> {
this._validate();
}
- private async _prepareFormFromClone(originalRun: ApiRunDetail): Promise {
- if (!originalRun.run) {
+ private async _prepareFormFromClone(originalRun?: ApiRun | ApiJob, runtime?: ApiPipelineRuntime): Promise {
+ if (!originalRun) {
logger.error('Could not get cloned run details');
return;
}
- let pipeline: ApiPipeline;
- let workflow: Workflow;
- let pipelineFromRun: ApiPipeline;
- let usePipelineFromRun = false;
+ let pipeline: ApiPipeline | undefined;
+ let workflowFromRun: Workflow | undefined;
+ let useWorkflowFromRun = false;
let usePipelineFromRunLabel = '';
+ let name = '';
// This corresponds to a run using a pipeline that has been uploaded
- const referencePipelineId = RunUtils.getPipelineId(originalRun.run);
+ const referencePipelineId = RunUtils.getPipelineId(originalRun);
// This corresponds to a run where the pipeline has not been uploaded, such as runs started from
// the CLI or notebooks
- const embeddedPipelineSpec = RunUtils.getPipelineSpec(originalRun.run);
+ const embeddedPipelineSpec = RunUtils.getPipelineSpec(originalRun);
if (referencePipelineId) {
try {
pipeline = await Apis.pipelineServiceApi.getPipeline(referencePipelineId);
+ name = pipeline.name || '';
} catch (err) {
await this.showPageError(
'Error: failed to find a pipeline corresponding to that of the original run:'
- + ` ${originalRun.run.id}.`, err);
+ + ` ${originalRun.id}.`, err);
return;
}
} else if (embeddedPipelineSpec) {
try {
- pipeline = JSON.parse(embeddedPipelineSpec);
- pipelineFromRun = pipeline;
+ workflowFromRun = JSON.parse(embeddedPipelineSpec);
+ name = workflowFromRun!.metadata.name || '';
} catch (err) {
await this.showPageError('Error: failed to read the clone run\'s pipeline definition.', err);
return;
}
- usePipelineFromRun = true;
- usePipelineFromRunLabel = 'Use pipeline from cloned run';
+ useWorkflowFromRun = true;
+ usePipelineFromRunLabel = 'Using pipeline from cloned run';
} else {
await this.showPageError('Could not find the cloned run\'s pipeline definition.');
return;
}
- if (originalRun.pipeline_runtime!.workflow_manifest === undefined) {
- await this.showPageError(`Error: run ${originalRun.run.id} had no workflow manifest`);
- logger.error(originalRun.pipeline_runtime!.workflow_manifest);
- return;
- }
- try {
- workflow = JSON.parse(originalRun.pipeline_runtime!.workflow_manifest!) as Workflow;
- } catch (err) {
- await this.showPageError('Error: failed to parse the original run\'s runtime.', err);
- logger.error(originalRun.pipeline_runtime!.workflow_manifest);
+ if (!originalRun.pipeline_spec || !originalRun.pipeline_spec.workflow_manifest) {
+ await this.showPageError(`Error: run ${originalRun.id} had no workflow manifest`);
return;
}
- // Set pipeline parameter values from run's workflow
- pipeline.parameters = WorkflowParser.getParameters(workflow);
+ const parameters = runtime
+ ? await RunUtils.getParametersFromRuntime(runtime) // cloned from run
+ : originalRun.pipeline_spec.parameters || []; // cloned from recurring run
this.setStateSafe({
+ isClone: true,
+ parameters,
pipeline,
- pipelineFromRun: pipelineFromRun!,
- pipelineName: (pipeline && pipeline.name) || '',
- runName: this._getCloneName(originalRun.run.name!),
- usePipelineFromRun,
+ pipelineName: name,
+ runName: this._getCloneName(originalRun.name!),
usePipelineFromRunLabel,
+ useWorkflowFromRun,
+ workflowFromRun,
});
this._validate();
}
- private _runParametersMessage(selectedPipeline: ApiPipeline | undefined): string {
- if (selectedPipeline) {
- if (selectedPipeline.parameters && selectedPipeline.parameters.length) {
+
+ private _runParametersMessage(): string {
+ if (this.state.pipeline || this.state.workflowFromRun) {
+ if (this.state.parameters.length) {
return 'Specify parameters required by the pipeline';
} else {
return 'This pipeline has no parameters';
@@ -568,9 +600,7 @@ class NewRun extends Page<{}, NewRunState> {
}
private _start(): void {
- // TODO: This cannot currently be reached because _validate() is called everywhere and blocks
- // the button from being clicked without first having a pipeline.
- if (!this.state.pipeline) {
+ if (!this.state.pipeline && !this.state.workflowFromRun) {
this.showErrorDialog('Run creation failed', 'Cannot start run without pipeline');
logger.error('Cannot start run without pipeline');
return;
@@ -590,12 +620,12 @@ class NewRun extends Page<{}, NewRunState> {
description: this.state.description,
name: this.state.runName,
pipeline_spec: {
- parameters: (this.state.pipeline.parameters || []).map(p => {
+ parameters: (this.state.parameters || []).map(p => {
p.value = (p.value || '').trim(); return p;
}),
- pipeline_id: this.state.usePipelineFromRun ? undefined : this.state.pipeline.id,
- workflow_manifest: this.state.usePipelineFromRun
- ? JSON.stringify(this.state.pipelineFromRun)
+ pipeline_id: this.state.pipeline ? this.state.pipeline.id : undefined,
+ workflow_manifest: this.state.useWorkflowFromRun
+ ? JSON.stringify(this.state.workflowFromRun)
: undefined,
},
resource_references: references,
@@ -639,15 +669,6 @@ class NewRun extends Page<{}, NewRunState> {
});
}
- private _handleParamChange(index: number, value: string): void {
- const { pipeline } = this.state;
- if (!pipeline || !pipeline.parameters) {
- return;
- }
- pipeline.parameters[index].value = value;
- this.setStateSafe({ pipeline });
- }
-
private _getCloneName(oldName: string): string {
const numberRegex = /Clone(?: \(([0-9]*)\))? of (.*)/;
const match = oldName.match(numberRegex);
@@ -661,9 +682,9 @@ class NewRun extends Page<{}, NewRunState> {
private _validate(): void {
// Validate state
- const { pipeline, maxConcurrentRuns, runName, trigger } = this.state;
+ const { pipeline, workflowFromRun, maxConcurrentRuns, runName, trigger } = this.state;
try {
- if (!pipeline) {
+ if (!pipeline && !workflowFromRun) {
throw new Error('A pipeline must be selected');
}
if (!runName) {
diff --git a/frontend/src/pages/RecurringRunDetails.test.tsx b/frontend/src/pages/RecurringRunDetails.test.tsx
index 2c4c651afaf..4408416dc99 100644
--- a/frontend/src/pages/RecurringRunDetails.test.tsx
+++ b/frontend/src/pages/RecurringRunDetails.test.tsx
@@ -20,7 +20,7 @@ import TestUtils from '../TestUtils';
import { ApiJob, ApiResourceType } from '../apis/job';
import { Apis } from '../lib/Apis';
import { PageProps } from './Page';
-import { RouteParams, RoutePage } from '../components/Router';
+import { RouteParams, RoutePage, QUERY_PARAMS } from '../components/Router';
import { ToolbarActionConfig } from '../components/Toolbar';
import { shallow, ReactWrapper, ShallowWrapper } from 'enzyme';
@@ -185,6 +185,22 @@ describe('RecurringRunDetails', () => {
expect(getJobSpy).toHaveBeenCalledTimes(2);
});
+
+ it('has a clone button, clicking it navigates to new run page', async () => {
+ tree = shallow( );
+ await TestUtils.flushPromises();
+ const instance = tree.instance() as RecurringRunDetails;
+ const cloneBtn = instance.getInitialToolbarState().actions.find(
+ b => b.title === 'Clone recurring run');
+ expect(cloneBtn).toBeDefined();
+ await cloneBtn!.action();
+ expect(historyPushSpy).toHaveBeenCalledTimes(1);
+ expect(historyPushSpy).toHaveBeenLastCalledWith(
+ RoutePage.NEW_RUN
+ + `?${QUERY_PARAMS.cloneFromRecurringRun}=${fullTestJob!.id}`
+ + `&${QUERY_PARAMS.isRecurring}=1`);
+ });
+
it('shows enabled Disable, and disabled Enable buttons if the run is enabled', async () => {
tree = shallow( );
await TestUtils.flushPromises();
diff --git a/frontend/src/pages/RecurringRunDetails.tsx b/frontend/src/pages/RecurringRunDetails.tsx
index 184e6e58c7b..b02008a2625 100644
--- a/frontend/src/pages/RecurringRunDetails.tsx
+++ b/frontend/src/pages/RecurringRunDetails.tsx
@@ -47,6 +47,7 @@ class RecurringRunDetails extends Page<{}, RecurringRunConfigState> {
const buttons = new Buttons(this.props, this.refresh.bind(this));
return {
actions: [
+ buttons.cloneRecurringRun(() => this.state.run ? [this.state.run.id!] : [], true),
buttons.refresh(this.refresh.bind(this)),
buttons.enableRecurringRun(() => this.state.run ? this.state.run.id! : ''),
buttons.disableRecurringRun(() => this.state.run ? this.state.run.id! : ''),
@@ -170,8 +171,8 @@ class RecurringRunDetails extends Page<{}, RecurringRunConfigState> {
const pageTitle = run ? run.name! : runId;
const toolbarActions = [...this.props.toolbarProps.actions];
- toolbarActions[1].disabled = !!run.enabled;
- toolbarActions[2].disabled = !run.enabled;
+ toolbarActions[2].disabled = !!run.enabled;
+ toolbarActions[3].disabled = !run.enabled;
this.props.updateToolbar({ actions: toolbarActions, breadcrumbs, pageTitle });
diff --git a/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap b/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap
index e313498ec79..8a344756df2 100644
--- a/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap
+++ b/frontend/src/pages/__snapshots__/NewRun.test.tsx.snap
@@ -12,26 +12,18 @@ exports[`NewRun arriving from pipeline details page indicates that a pipeline is
>
Run details
-
- }
- label="Use pipeline from previous step"
- onChange={[Function]}
- />
-
- }
- label="Select a pipeline from list"
- onChange={[Function]}
- />
+
+
+ Using pipeline from previous page
+
+
+
+ [View pipeline]
+
+
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- This pipeline has no parameters
-
+
@@ -548,7 +537,7 @@ exports[`NewRun changes the exit button's text if query params indicate this is
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -678,7 +667,7 @@ exports[`NewRun changes the exit button's text if query params indicate this is
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -775,20 +764,17 @@ exports[`NewRun changes the exit button's text if query params indicate this is
control={
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
+
@@ -968,7 +954,7 @@ exports[`NewRun changes title and form if the new run will recur, based on the r
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
Array [
@@ -1046,887 +1032,7 @@ exports[`NewRun changes title and form if the new run will recur, based on the r
location={
Object {
"pathname": "/runs/new",
- "search": "?experimentId=some-mock-experiment-id",
- }
- }
- match=""
- selectionChanged={[Function]}
- title="Choose an experiment"
- toolbarProps={
- Object {
- "actions": Array [],
- "breadcrumbs": Array [
- Object {
- "displayName": "Experiments",
- "href": "/experiments",
- },
- ],
- "pageTitle": "Start a new run",
- }
- }
- updateBanner={
- [MockFunction] {
- "calls": Array [
- Array [
- Object {},
- ],
- ],
- }
- }
- updateDialog={[MockFunction]}
- updateSnackbar={[MockFunction]}
- updateToolbar={
- [MockFunction] {
- "calls": Array [
- Array [
- Object {
- "actions": Array [],
- "breadcrumbs": Array [
- Object {
- "displayName": "Experiments",
- "href": "/experiments",
- },
- ],
- "pageTitle": "Start a new run",
- },
- ],
- Array [
- Object {
- "actions": Array [],
- "breadcrumbs": Array [
- Object {
- "displayName": "Experiments",
- "href": "/experiments",
- },
- Object {
- "displayName": "some mock experiment name",
- "href": "/experiments/details/some-mock-experiment-id",
- },
- ],
- "pageTitle": "Start a new run",
- },
- ],
- Array [
- Object {
- "pageTitle": "Start a recurring run",
- },
- ],
- ],
- }
- }
- />
-
-
-
- Cancel
-
-
- Use this experiment
-
-
-
-
-
-
- This run will be associated with the following experiment
-
-
-
- Choose
-
- ,
- "readOnly": true,
- }
- }
- disabled={true}
- label="Experiment"
- required={true}
- value="some mock experiment name"
- variant="outlined"
- />
-
- Run Type
-
-
- }
- id="oneOffToggle"
- label="One-off"
- onChange={[Function]}
- />
-
- }
- label="Recurring"
- onChange={[Function]}
- />
-
- Run trigger
-
-
- Choose a method by which new runs will be triggered
-
-
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
-
-
-
- Cancel
-
-
- A pipeline must be selected
-
-
-
-
-`;
-
-exports[`NewRun changes title and form to default state if the new run is a one-off, based on the radio buttons 1`] = `
-
-
-
- Run details
-
-
-
- Choose
-
- ,
- "readOnly": true,
- }
- }
- disabled={true}
- label="Pipeline"
- required={true}
- value=""
- variant="outlined"
- />
-
-
-
-
-
-
- Cancel
-
-
- Use this pipeline
-
-
-
-
-
-
-
-
-
- Cancel
-
-
- Use this experiment
-
-
-
-
-
-
- This run will be associated with the following experiment
-
-
-
- Choose
-
- ,
- "readOnly": true,
- }
- }
- disabled={true}
- label="Experiment"
- required={true}
- value=""
- variant="outlined"
- />
-
- Run Type
-
-
- }
- id="oneOffToggle"
- label="One-off"
- onChange={[Function]}
- />
-
- }
- label="Recurring"
- onChange={[Function]}
- />
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
-
-
-
- Cancel
-
-
- A pipeline must be selected
-
-
-
-
-`;
-
-exports[`NewRun cloning from a run shows pipeline selector when switching from embedded pipeline to select pipeline 1`] = `
-
-
-
- Run details
-
-
- }
- label="Use pipeline from cloned run"
- onChange={[Function]}
- />
-
- [View pipeline]
-
-
- }
- label="Select a pipeline from list"
- onChange={[Function]}
- />
-
-
- Choose
-
- ,
- "readOnly": true,
- }
- }
- disabled={true}
- label="Pipeline"
- required={true}
- value=""
- variant="outlined"
- />
-
-
-
-
-
-
- Cancel
-
-
- Use this pipeline
-
-
-
-
-
-
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
- Run parameters
+ Run trigger
- Parameters will appear after you select a pipeline
+ Choose a method by which new runs will be triggered
+
+
+ >
+ A pipeline must be selected
+
`;
-exports[`NewRun cloning from a run shows switching controls when run has embedded pipeline, selects that pipeline by default, and hides pipeline selector 1`] = `
+exports[`NewRun changes title and form to default state if the new run is a one-off, based on the radio buttons 1`] = `
@@ -2131,31 +1256,37 @@ exports[`NewRun cloning from a run shows switching controls when run has embedde
>
Run details
-
- }
- label="Use pipeline from cloned run"
- onChange={[Function]}
- />
-
- [View pipeline]
-
-
+
+
+ Choose
+
+ ,
+ "readOnly": true,
+ }
}
- label="Select a pipeline from list"
- onChange={[Function]}
+ disabled={true}
+ label="Pipeline"
+ required={true}
+ value=""
+ variant="outlined"
/>
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- This pipeline has no parameters
-
+
+ >
+ A pipeline must be selected
+
@@ -2564,7 +1704,7 @@ exports[`NewRun fetches the associated pipeline if one is present in the query p
disabled={true}
label="Pipeline"
required={true}
- value="some mock pipeline name"
+ value="original mock pipeline name"
variant="outlined"
/>
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- This pipeline has no parameters
-
+
@@ -3083,7 +2220,7 @@ exports[`NewRun renders the new run page 1`] = `
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -3213,7 +2350,7 @@ exports[`NewRun renders the new run page 1`] = `
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -3310,20 +2447,17 @@ exports[`NewRun renders the new run page 1`] = `
control={
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
+
@@ -3722,9 +2856,9 @@ exports[`NewRun starting a new recurring run includes additional trigger input f
control={
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
@@ -3739,14 +2873,11 @@ exports[`NewRun starting a new recurring run includes additional trigger input f
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
+
@@ -3790,32 +2921,18 @@ exports[`NewRun starting a new run copies pipeline from run in the start API cal
>
Run details
-
- }
- label="Use pipeline from cloned run"
- onChange={[Function]}
- />
-
- [View pipeline]
-
-
- }
- label="Select a pipeline from list"
- onChange={[Function]}
- />
+
+
+ Using pipeline from cloned run
+
+
+
+ [View pipeline]
+
+
Run Type
-
- }
- id="oneOffToggle"
- label="One-off"
- onChange={[Function]}
- />
-
- }
- label="Recurring"
- onChange={[Function]}
+
+ One-off
+
+
-
- Run parameters
-
-
- This pipeline has no parameters
-
@@ -4216,7 +3311,7 @@ exports[`NewRun starting a new run copies pipeline from run in the start API cal
`;
-exports[`NewRun starting a new run updates the pipeline in state when a user fills in its params 1`] = `
+exports[`NewRun starting a new run updates the parameters in state on handleParamChange 1`] = `
@@ -4257,7 +3352,7 @@ exports[`NewRun starting a new run updates the pipeline in state when a user fil
disabled={true}
label="Pipeline"
required={true}
- value="some mock pipeline name"
+ value="original mock pipeline name"
variant="outlined"
/>
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- Specify parameters required by the pipeline
-
-
-
-
-
+ "name": "param-2",
+ "value": "prefilled value",
+ },
+ ]
+ }
+ titleMessage="Specify parameters required by the pipeline"
+ />
@@ -4842,7 +3911,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -4972,7 +4041,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -5069,20 +4138,17 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d
control={
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
+
@@ -5155,7 +4221,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d
disabled={true}
label="Pipeline"
required={true}
- value="some mock pipeline name"
+ value="original mock pipeline name"
variant="outlined"
/>
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- Specify parameters required by the pipeline
-
-
-
-
-
+ "name": "param-2",
+ "value": "prefilled value 2",
+ },
+ ]
+ }
+ titleMessage="Specify parameters required by the pipeline"
+ />
@@ -5609,7 +4649,7 @@ exports[`NewRun starting a new run updates the pipeline params as user selects d
disabled={true}
label="Pipeline"
required={true}
- value="some mock pipeline name"
+ value="original mock pipeline name"
variant="outlined"
/>
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- This pipeline has no parameters
-
+
@@ -6136,7 +5173,7 @@ exports[`NewRun updates the run's state with the associated experiment if one is
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -6266,7 +5303,7 @@ exports[`NewRun updates the run's state with the associated experiment if one is
"href": "/experiments/details/some-mock-experiment-id",
},
],
- "pageTitle": "Start a new run",
+ "pageTitle": "Start a run",
},
],
],
@@ -6363,20 +5400,17 @@ exports[`NewRun updates the run's state with the associated experiment if one is
control={
}
+ id="recurringToggle"
label="Recurring"
onChange={[Function]}
/>
-
- Run parameters
-
-
- Parameters will appear after you select a pipeline
-
+