diff --git a/frontend/src/lib/RunUtils.ts b/frontend/src/lib/RunUtils.ts index 5b0343fa363..6d4ee0a10e2 100644 --- a/frontend/src/lib/RunUtils.ts +++ b/frontend/src/lib/RunUtils.ts @@ -88,6 +88,19 @@ function extractMetricMetadata(runs: ApiRun[]): MetricMetadata[] { return orderBy(metrics, ['count', 'name'], ['desc', 'asc']); } +function getRecurringRunId(run?: ApiRun): string { + if (!run) { + return ''; + } + + for (const ref of run.resource_references || []) { + if (ref.key && ref.key.type === ApiResourceType.JOB) { + return ref.key.id || ''; + } + } + return ''; +} + export default { extractMetricMetadata, getAllExperimentReferences, @@ -95,5 +108,6 @@ export default { getFirstExperimentReferenceId, getPipelineId, getPipelineSpec, + getRecurringRunId, runsToMetricMetadataMap, }; diff --git a/frontend/src/pages/RunList.test.tsx b/frontend/src/pages/RunList.test.tsx index 4e76fdd4036..1bdc2bd9f82 100644 --- a/frontend/src/pages/RunList.test.tsx +++ b/frontend/src/pages/RunList.test.tsx @@ -319,6 +319,21 @@ describe('RunList', () => { expect(tree).toMatchSnapshot(); }); + it('shows link to recurring run config', async () => { + mockNRuns(1, { + run: { + resource_references: [{ + key: { id: 'test-recurring-run-id', type: ApiResourceType.JOB } + }] + } + }); + const props = generateProps(); + tree = shallow(); + await (tree.instance() as RunListTest)._loadRuns({}); + expect(props.onError).not.toHaveBeenCalled(); + expect(tree).toMatchSnapshot(); + }); + it('shows experiment name', async () => { mockNRuns(1, { run: { @@ -372,6 +387,10 @@ describe('RunList', () => { expect(getMountedInstance()._pipelineCustomRenderer({ value: { /* no displayName */ showLink: true }, id: 'run-id' })).toMatchSnapshot(); }); + it('renders pipeline name as link to its details page', () => { + expect(getMountedInstance()._recurringRunCustomRenderer({ value: { id: 'recurring-run-id', }, id: 'run-id' })).toMatchSnapshot(); + }); + it('renders experiment name as link to its details page', () => { expect(getMountedInstance()._experimentCustomRenderer({ value: { displayName: 'test experiment', id: 'experiment-id' }, id: 'run-id' })).toMatchSnapshot(); }); diff --git a/frontend/src/pages/RunList.tsx b/frontend/src/pages/RunList.tsx index f872360c3d7..711a02cb664 100644 --- a/frontend/src/pages/RunList.tsx +++ b/frontend/src/pages/RunList.tsx @@ -41,8 +41,14 @@ interface PipelineInfo { showLink: boolean; } +interface RecurringRunInfo { + displayName?: string; + id?: string; +} + interface DisplayRun { experiment?: ExperimentInfo; + recurringRun?: RecurringRunInfo; run: ApiRun; pipeline?: PipelineInfo; error?: string; @@ -90,13 +96,14 @@ class RunList extends React.PureComponent { const columns: Column[] = [ { customRenderer: this._nameCustomRenderer, - flex: 2, + flex: 1.5, label: 'Run name', sortKey: RunSortKeys.NAME, }, { customRenderer: this._statusCustomRenderer, flex: 0.5, label: 'Status' }, { label: 'Duration', flex: 0.5 }, { customRenderer: this._pipelineCustomRenderer, label: 'Pipeline', flex: 1 }, + { customRenderer: this._recurringRunCustomRenderer, label: 'Recurring Run', flex: 0.5 }, { label: 'Start time', flex: 1, sortKey: RunSortKeys.CREATED_AT }, ]; @@ -116,7 +123,7 @@ class RunList extends React.PureComponent { columns.push(...metricMetadata.map((metadata) => { return { customRenderer: this._metricCustomRenderer, - flex: 1, + flex: 0.5, label: metadata.name! }; })); @@ -141,6 +148,7 @@ class RunList extends React.PureComponent { r.run.status || '-', getRunDuration(r.run), r.pipeline, + r.recurringRun, formatDateString(r.run.created_at), ] as any, }; @@ -198,6 +206,20 @@ class RunList extends React.PureComponent { ); } + public _recurringRunCustomRenderer: React.FC> = (props: CustomRendererProps) => { + // If the getJob call failed or a run has no job, we display a placeholder. + if (!props.value || !props.value.id) { + return
-
; + } + const url = RoutePage.RECURRING_RUN.replace(':' + RouteParams.runId, props.value.id || ''); + return ( + e.stopPropagation()} + to={url}> + {props.value.displayName || '[View config]'} + + ); + } + public _experimentCustomRenderer: React.FC> = (props: CustomRendererProps) => { // If the getExperiment call failed or a run has no experiment, we display a placeholder. if (!props.value || !props.value.id) { @@ -277,10 +299,7 @@ class RunList extends React.PureComponent { } } - await this._getAndSetPipelineNames(displayRuns); - if (!this.props.hideExperimentColumn) { - await this._getAndSetExperimentNames(displayRuns); - } + await this._setColumns(displayRuns); this.setState({ metrics: RunUtils.extractMetricMetadata(displayRuns.map(r => r.run)), @@ -289,6 +308,30 @@ class RunList extends React.PureComponent { return nextPageToken; } + private async _setColumns(displayRuns: DisplayRun[]): Promise { + return Promise.all( + displayRuns.map(async (displayRun) => { + this._setRecurringRun(displayRun); + + await this._getAndSetPipelineNames(displayRun); + + if (!this.props.hideExperimentColumn) { + await this._getAndSetExperimentNames(displayRun); + } + return displayRun; + }) + ); + } + + private _setRecurringRun(displayRun: DisplayRun): void { + const recurringRunId = RunUtils.getRecurringRunId(displayRun.run); + if (recurringRunId) { + // TODO: It would be better to use name here, but that will require another n API calls at + // this time. + displayRun.recurringRun = { id: recurringRunId, displayName: recurringRunId }; + } + } + /** * For each run ID, fetch its corresponding run, and set it in DisplayRuns */ @@ -306,55 +349,43 @@ class RunList extends React.PureComponent { } /** - * For each DisplayRun, get its ApiRun and retrieve that ApiRun's Pipeline ID if it has one, then - * use that Pipeline ID to fetch its associated Pipeline and attach that Pipeline's name to the - * DisplayRun. If the ApiRun has no Pipeline ID, then the corresponding DisplayRun will show '-'. + * For the given DisplayRun, get its ApiRun and retrieve that ApiRun's Pipeline ID if it has one, + * then use that Pipeline ID to fetch its associated Pipeline and attach that Pipeline's name to + * the DisplayRun. If the ApiRun has no Pipeline ID, then the corresponding DisplayRun will show + * '-'. */ - private _getAndSetPipelineNames(displayRuns: DisplayRun[]): Promise { - return Promise.all( - displayRuns.map(async (displayRun) => { - const pipelineId = RunUtils.getPipelineId(displayRun.run); - if (pipelineId) { - try { - const pipeline = await Apis.pipelineServiceApi.getPipeline(pipelineId); - displayRun.pipeline = { displayName: pipeline.name || '', id: pipelineId, showLink: false }; - } catch (err) { - // This could be an API exception, or a JSON parse exception. - displayRun.error = 'Failed to get associated pipeline: ' + await errorToMessage(err); - } - } else if (!!RunUtils.getPipelineSpec(displayRun.run)) { - displayRun.pipeline = { showLink: true }; - } - return displayRun; - }) - ); + private async _getAndSetPipelineNames(displayRun: DisplayRun): Promise { + const pipelineId = RunUtils.getPipelineId(displayRun.run); + if (pipelineId) { + try { + const pipeline = await Apis.pipelineServiceApi.getPipeline(pipelineId); + displayRun.pipeline = { displayName: pipeline.name || '', id: pipelineId, showLink: false }; + } catch (err) { + // This could be an API exception, or a JSON parse exception. + displayRun.error = 'Failed to get associated pipeline: ' + await errorToMessage(err); + } + } else if (!!RunUtils.getPipelineSpec(displayRun.run)) { + displayRun.pipeline = { showLink: true }; + } } /** - * For each DisplayRun, get its ApiRun and retrieve that ApiRun's Experiment ID if it has one, - * then use that Experiment ID to fetch its associated Experiment and attach that Experiment's - * name to the DisplayRun. If the ApiRun has no Experiment ID, then the corresponding DisplayRun - * will show '-'. + * For the given DisplayRun, get its ApiRun and retrieve that ApiRun's Experiment ID if it has + * one, then use that Experiment ID to fetch its associated Experiment and attach that + * Experiment's name to the DisplayRun. If the ApiRun has no Experiment ID, then the corresponding + * DisplayRun will show '-'. */ - private _getAndSetExperimentNames(displayRuns: DisplayRun[]): Promise { - return Promise.all( - displayRuns.map(async (displayRun) => { - const experimentId = RunUtils.getFirstExperimentReferenceId(displayRun.run); - if (experimentId) { - try { - // TODO: Experiment could be an optional field in state since whenever the RunList is - // created from the ExperimentDetails page, we already have the experiment (and will) - // be fetching the same one over and over here. - const experiment = await Apis.experimentServiceApi.getExperiment(experimentId); - displayRun.experiment = { displayName: experiment.name || '', id: experimentId }; - } catch (err) { - // This could be an API exception, or a JSON parse exception. - displayRun.error = 'Failed to get associated experiment: ' + await errorToMessage(err); - } - } - return displayRun; - }) - ); + private async _getAndSetExperimentNames(displayRun: DisplayRun): Promise { + const experimentId = RunUtils.getFirstExperimentReferenceId(displayRun.run); + if (experimentId) { + try { + const experiment = await Apis.experimentServiceApi.getExperiment(experimentId); + displayRun.experiment = { displayName: experiment.name || '', id: experimentId }; + } catch (err) { + // This could be an API exception, or a JSON parse exception. + displayRun.error = 'Failed to get associated experiment: ' + await errorToMessage(err); + } + } } } diff --git a/frontend/src/pages/__snapshots__/RunList.test.tsx.snap b/frontend/src/pages/__snapshots__/RunList.test.tsx.snap index deac5d0cf80..074546ab29a 100644 --- a/frontend/src/pages/__snapshots__/RunList.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RunList.test.tsx.snap @@ -7,7 +7,7 @@ exports[`RunList adds metrics columns 1`] = ` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -30,6 +30,11 @@ exports[`RunList adds metrics columns 1`] = ` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -42,12 +47,12 @@ exports[`RunList adds metrics columns 1`] = ` }, Object { "customRenderer": [Function], - "flex": 1, + "flex": 0.5, "label": "metric1", }, Object { "customRenderer": [Function], - "flex": 1, + "flex": 0.5, "label": "metric2", }, ] @@ -67,6 +72,7 @@ exports[`RunList adds metrics columns 1`] = ` "-", undefined, undefined, + undefined, "-", "", Object { @@ -104,6 +110,7 @@ exports[`RunList adds metrics columns 1`] = ` "-", undefined, undefined, + undefined, "-", "", Object { @@ -145,7 +152,7 @@ exports[`RunList displays error in run row if experiment could not be fetched 1` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -168,6 +175,11 @@ exports[`RunList displays error in run row if experiment could not be fetched 1` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -190,6 +202,7 @@ exports[`RunList displays error in run row if experiment could not be fetched 1` "-", undefined, undefined, + undefined, "-", ], }, @@ -206,7 +219,7 @@ exports[`RunList displays error in run row if it failed to parse (run list mask) Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -229,6 +242,11 @@ exports[`RunList displays error in run row if it failed to parse (run list mask) "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -251,6 +269,7 @@ exports[`RunList displays error in run row if it failed to parse (run list mask) "-", undefined, undefined, + undefined, "-", ], }, @@ -263,6 +282,7 @@ exports[`RunList displays error in run row if it failed to parse (run list mask) "-", undefined, undefined, + undefined, "-", ], }, @@ -279,7 +299,7 @@ exports[`RunList displays error in run row if pipeline could not be fetched 1`] Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -302,6 +322,11 @@ exports[`RunList displays error in run row if pipeline could not be fetched 1`] "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -324,6 +349,7 @@ exports[`RunList displays error in run row if pipeline could not be fetched 1`] "-", undefined, undefined, + undefined, "-", ], }, @@ -357,7 +383,7 @@ exports[`RunList hides experiment name if instructed 1`] = ` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -375,6 +401,11 @@ exports[`RunList hides experiment name if instructed 1`] = ` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -396,6 +427,7 @@ exports[`RunList hides experiment name if instructed 1`] = ` "-", "-", undefined, + undefined, "-", ], }, @@ -412,7 +444,7 @@ exports[`RunList in archived state renders the empty experience 1`] = ` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -435,6 +467,11 @@ exports[`RunList in archived state renders the empty experience 1`] = ` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -458,7 +495,7 @@ exports[`RunList loads multiple runs 1`] = ` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -481,6 +518,11 @@ exports[`RunList loads multiple runs 1`] = ` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -503,6 +545,7 @@ exports[`RunList loads multiple runs 1`] = ` "-", undefined, undefined, + undefined, "-", ], }, @@ -515,6 +558,7 @@ exports[`RunList loads multiple runs 1`] = ` "-", undefined, undefined, + undefined, "-", ], }, @@ -527,6 +571,7 @@ exports[`RunList loads multiple runs 1`] = ` "-", undefined, undefined, + undefined, "-", ], }, @@ -539,6 +584,7 @@ exports[`RunList loads multiple runs 1`] = ` "-", undefined, undefined, + undefined, "-", ], }, @@ -551,6 +597,7 @@ exports[`RunList loads multiple runs 1`] = ` "-", undefined, undefined, + undefined, "-", ], }, @@ -567,7 +614,7 @@ exports[`RunList loads one run 1`] = ` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -590,6 +637,11 @@ exports[`RunList loads one run 1`] = ` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -612,6 +664,7 @@ exports[`RunList loads one run 1`] = ` "-", undefined, undefined, + undefined, "-", ], }, @@ -638,7 +691,7 @@ exports[`RunList reloads the run when refresh is called 1`] = ` Array [ Object { "customRenderer": [Function], - "flex": 2, + "flex": 1.5, "label": "Run name", "sortKey": "name", }, @@ -661,6 +714,11 @@ exports[`RunList reloads the run when refresh is called 1`] = ` "flex": 1, "label": "Pipeline", }, + Object { + "customRenderer": [Function], + "flex": 0.5, + "label": "Recurring Run", + }, Object { "flex": 1, "label": "Start time", @@ -1950,7 +2008,7 @@ exports[`RunList reloads the run when refresh is called 1`] = ` key="0" style={ Object { - "width": "33.33333333333333%", + "width": "25%", } } > @@ -4871,13 +4929,13 @@ exports[`RunList reloads the run when refresh is called 1`] = ` key="5" style={ Object { - "width": "16.666666666666664%", + "width": "8.333333333333332%", } } > - Start time + Recurring Run @@ -5422,12 +5479,12 @@ exports[`RunList reloads the run when refresh is called 1`] = ` - Start time + Recurring Run - -
- No available runs found. -
-
-
- - Rows per page: - - - - -
- - } - value={10} - > - - } - value={10} - > - `; +exports[`RunList renders pipeline name as link to its details page 2`] = ` + + [View config] + +`; + exports[`RunList renders raw metric 1`] = ` +
+`; + +exports[`RunList shows link to recurring run config 1`] = ` +
+