diff --git a/frontend/src/components/DetailsTable.test.tsx b/frontend/src/components/DetailsTable.test.tsx index cb91e1de763..a0ba935ca4e 100644 --- a/frontend/src/components/DetailsTable.test.tsx +++ b/frontend/src/components/DetailsTable.test.tsx @@ -106,4 +106,10 @@ describe('DetailsTable', () => { expect(tree).toMatchSnapshot(); }); + it('does render values with the provided valueComponent', () => { + const valueComponent: React.FC = ({key}) => {key}; + const tree = shallow(); + expect(tree).toMatchSnapshot(); + }); + }); diff --git a/frontend/src/components/DetailsTable.tsx b/frontend/src/components/DetailsTable.tsx index 3f43b955c5b..a4461a1fa20 100644 --- a/frontend/src/components/DetailsTable.tsx +++ b/frontend/src/components/DetailsTable.tsx @@ -17,11 +17,13 @@ import * as React from 'react'; import { stylesheet } from 'typestyle'; import { color, spacing, commonCss } from '../Css'; +import { KeyValue } from '../lib/StaticGraphParser'; import Editor from './Editor'; import 'brace'; import 'brace/ext/language_tools'; import 'brace/mode/json'; import 'brace/theme/github'; +import { S3Artifact } from 'third_party/argo-ui/argo_template'; export const css = stylesheet({ key: { @@ -40,12 +42,20 @@ export const css = stylesheet({ }, valueText: { maxWidth: 400, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', }, }); interface DetailsTableProps { - fields: string[][]; + fields: Array>; title?: string; + valueComponent?: React.FC; +} + +function isString(x: any): x is string { + return typeof x === 'string'; } export default (props: DetailsTableProps) => { @@ -53,30 +63,39 @@ export default (props: DetailsTableProps) => { {!!props.title &&
{props.title}
}
{props.fields.map((f, i) => { - try { - const parsedJson = JSON.parse(f[1]); - // Nulls, booleans, strings, and numbers can all be parsed as JSON, but we don't care - // about rendering. Note that `typeOf null` returns 'object' - if (parsedJson === null || typeof parsedJson !== 'object') { - throw new Error('Parsed JSON was neither an array nor an object. Using default renderer'); + const [key, value] = f; + + // only try to parse json if value is a string + if (isString(value)) { + try { + const parsedJson = JSON.parse(value); + // Nulls, booleans, strings, and numbers can all be parsed as JSON, but we don't care + // about rendering. Note that `typeOf null` returns 'object' + if (parsedJson === null || typeof parsedJson !== 'object') { + throw new Error('Parsed JSON was neither an array nor an object. Using default renderer'); + } + return ( +
+ {key} + +
+ ); + } catch (err) { + // do nothing } - return ( -
- {f[0]} - -
- ); - } catch (err) { - // If the value isn't a JSON object, just display it as is - return ( -
- {f[0]} - {f[1]} -
- ); } + // If the value isn't a JSON object, just display it as is + return ( +
+ {key} + + {props.valueComponent && !!value && !isString(value) ? props.valueComponent(value) : value} + +
+ ); + })}
diff --git a/frontend/src/components/MinioArtifactLink.test.tsx b/frontend/src/components/MinioArtifactLink.test.tsx new file mode 100644 index 00000000000..2318cba26be --- /dev/null +++ b/frontend/src/components/MinioArtifactLink.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MinioArtifactLink from './MinioArtifactLink'; + +describe('MinioArtifactLink', () => { + + it('handles undefined artifact', () => { + expect(MinioArtifactLink(undefined as any)).toMatchSnapshot(); + }); + + it('handles null artifact', () => { + expect(MinioArtifactLink(null as any)).toMatchSnapshot(); + }); + + it('handles empty artifact', () => { + expect(MinioArtifactLink({} as any)).toMatchSnapshot(); + }); + + it('handles invalid artifact: no bucket', () => { + const s3artifact = { + accessKeySecret: {key: 'accesskey', optional: false, name: 'minio'}, + bucket: '', + endpoint: 'minio.kubeflow', + key: 'bar', + secretKeySecret: {key: 'secretkey', optional: false, name: 'minio'}, + }; + expect(MinioArtifactLink(s3artifact)).toMatchSnapshot(); + }); + + it('handles invalid artifact: no key', () => { + const s3artifact = { + accessKeySecret: {key: 'accesskey', optional: false, name: 'minio'}, + bucket: 'foo', + endpoint: 'minio.kubeflow', + key: '', + secretKeySecret: {key: 'secretkey', optional: false, name: 'minio'}, + }; + expect(MinioArtifactLink(s3artifact)).toMatchSnapshot(); + }); + + it('handles s3 artifact', () => { + const s3artifact = { + accessKeySecret: {key: 'accesskey', optional: false, name: 'minio'}, + bucket: 'foo', + endpoint: 's3.amazonaws.com', + key: 'bar', + secretKeySecret: {key: 'secretkey', optional: false, name: 'minio'}, + }; + expect(MinioArtifactLink(s3artifact)).toMatchSnapshot(); + }); + + it('handles minio artifact', () => { + const minioartifact = { + accessKeySecret: {key: 'accesskey', optional: false, name: 'minio'}, + bucket: 'foo', + endpoint: 'minio.kubeflow', + key: 'bar', + secretKeySecret: {key: 'secretkey', optional: false, name: 'minio'}, + }; + expect(MinioArtifactLink(minioartifact)).toMatchSnapshot(); + }); + +}); \ No newline at end of file diff --git a/frontend/src/components/MinioArtifactLink.tsx b/frontend/src/components/MinioArtifactLink.tsx new file mode 100644 index 00000000000..1f15848d64b --- /dev/null +++ b/frontend/src/components/MinioArtifactLink.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { StoragePath, StorageService } from '../lib/WorkflowParser'; +import { S3Artifact } from '../../third_party/argo-ui/argo_template'; + +const artifactApiUri = ({ source, bucket, key }: StoragePath) => + 'artifacts/get' + + `?source=${source}&bucket=${bucket}&key=${encodeURIComponent(key)}`; + +/** + * A component that renders an artifact link. + */ +const MinioArtifactLink: React.FC = ( + s3artifact +) => { + if (!s3artifact || !s3artifact.key || !s3artifact.bucket) { + return null; + } + + const { key, bucket, endpoint } = s3artifact; + const source = endpoint === 's3.amazonaws.com' ? StorageService.S3 : StorageService.MINIO; + const linkText = `${source.toString()}://${bucket}/${key}`; + // Opens in new window safely + return ( + + {linkText} + + ); + +}; + +export default MinioArtifactLink; \ No newline at end of file diff --git a/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap b/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap index fd6e8640512..725d83649e4 100644 --- a/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap +++ b/frontend/src/components/__snapshots__/DetailsTable.test.tsx.snap @@ -215,6 +215,30 @@ exports[`DetailsTable does render arrays as JSON 2`] = ` `; +exports[`DetailsTable does render values with the provided valueComponent 1`] = ` + +
+
+ + key + + + + foobar + + +
+
+
+`; + exports[`DetailsTable shows a row with a title 1`] = `
+ minio://foo/bar + +`; + +exports[`MinioArtifactLink handles null artifact 1`] = `null`; + +exports[`MinioArtifactLink handles s3 artifact 1`] = ` + + s3://foo/bar + +`; + +exports[`MinioArtifactLink handles undefined artifact 1`] = `null`; diff --git a/frontend/src/lib/StaticGraphParser.ts b/frontend/src/lib/StaticGraphParser.ts index 3330f15c20f..5cfb3db187f 100644 --- a/frontend/src/lib/StaticGraphParser.ts +++ b/frontend/src/lib/StaticGraphParser.ts @@ -22,16 +22,21 @@ import { logger } from './Utils'; export type nodeType = 'container' | 'resource' | 'dag' | 'unknown'; +export interface KeyValue extends Array { + 0?: string; + 1?: T; +} + export class SelectedNodeInfo { public args: string[]; public command: string[]; public condition: string; public image: string; - public inputs: string[][]; + public inputs: Array>; public nodeType: nodeType; - public outputs: string[][]; - public volumeMounts: string[][]; - public resource: string[][]; + public outputs: Array>; + public volumeMounts: Array>; + public resource: Array>; constructor() { this.args = []; diff --git a/frontend/src/lib/WorkflowParser.test.ts b/frontend/src/lib/WorkflowParser.test.ts index cdb68f58c82..ba5536eed50 100644 --- a/frontend/src/lib/WorkflowParser.test.ts +++ b/frontend/src/lib/WorkflowParser.test.ts @@ -337,37 +337,38 @@ describe('WorkflowParser', () => { }); describe('getNodeInputOutputParams', () => { + const emptyParams = {inputParams: [], outputParams: []}; it('handles undefined workflow', () => { - expect(WorkflowParser.getNodeInputOutputParams(undefined as any, '')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams(undefined as any, '')).toEqual(emptyParams); }); it('handles empty workflow, without status', () => { - expect(WorkflowParser.getNodeInputOutputParams({} as any, '')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams({} as any, '')).toEqual(emptyParams); }); it('handles workflow without nodes', () => { const workflow = { status: {} }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, '')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, '')).toEqual(emptyParams); }); it('handles node not existing in graph', () => { const workflow = { status: { nodes: { node1: {} } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node2')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node2')).toEqual(emptyParams); }); it('handles an empty node', () => { const workflow = { status: { nodes: { node1: {} } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual(emptyParams); }); it('handles a node with inputs but no parameters', () => { const workflow = { status: { nodes: { node1: { inputs: {} } } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual(emptyParams); }); it('handles a node with inputs and empty parameters', () => { const workflow = { status: { nodes: { node1: { inputs: { parameters: [] } } } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([[], []]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual(emptyParams); }); it('handles a node with one input parameter', () => { @@ -385,11 +386,10 @@ describe('WorkflowParser', () => { } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([ - [ - ['input param1', 'input param1 value'] - ], [] - ]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual({ + inputParams: [['input param1', 'input param1 value']], + outputParams: [], + }); }); it('handles a node with one input parameter that has no value', () => { @@ -406,11 +406,10 @@ describe('WorkflowParser', () => { } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([ - [ - ['input param1', ''] - ], [] - ]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual({ + inputParams: [['input param1', '']], + outputParams: [], + }); }); it('handles a node with one input parameter that is not the first node', () => { @@ -436,11 +435,10 @@ describe('WorkflowParser', () => { } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node2')).toEqual([ - [ - ['node2 input param1', 'node2 input param1 value'] - ], [] - ]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node2')).toEqual({ + inputParams: [['node2 input param1', 'node2 input param1 value']], + outputParams: [], + }); }); it('handles a node with one output parameter', () => { @@ -458,12 +456,10 @@ describe('WorkflowParser', () => { } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([ - [], - [ - ['output param1', 'output param1 value'] - ], - ]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual({ + inputParams: [], + outputParams: [['output param1', 'output param1 value']], + }); }); it('handles a node with one input and one output parameter', () => { @@ -487,14 +483,10 @@ describe('WorkflowParser', () => { } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([ - [ - ['input param1', 'input param1 value'] - ], - [ - ['output param1', 'output param1 value'] - ], - ]); + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual({ + inputParams: [['input param1', 'input param1 value']], + outputParams: [['output param1', 'output param1 value']], + }); }); it('handles a node with multiple input and output parameter', () => { @@ -527,17 +519,222 @@ describe('WorkflowParser', () => { } } }; - expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual([ - [ + expect(WorkflowParser.getNodeInputOutputParams(workflow as any, 'node1')).toEqual({ + inputParams: [ ['input param1', 'input param1 value'], ['input param2', 'input param2 value'], ['input param3', 'input param3 value'], ], - [ + outputParams: [ ['output param1', 'output param1 value'], ['output param2', 'output param2 value'], ], - ]); + }); + }); + }); + + describe('getNodeInputOutputArtifacts', () => { + const emptyArtifacts = {inputArtifacts: [], outputArtifacts: []}; + const s3 = { + accessKeySecret: {key: 'accesskey', optional: false, name: 'minio'}, + bucket: 'foo', + endpoint: 'minio.kubeflow', + key: 'bar', + secretKeySecret: {key: 'secretkey', optional: false, name: 'minio'}, + }; + + it('handles undefined workflow', () => { + expect(WorkflowParser.getNodeInputOutputArtifacts(undefined as any, '')).toEqual(emptyArtifacts); + }); + + it('handles empty workflow, without status', () => { + expect(WorkflowParser.getNodeInputOutputArtifacts({} as any, '')).toEqual(emptyArtifacts); + }); + + it('handles workflow without nodes', () => { + const workflow = { status: {} }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, '')).toEqual(emptyArtifacts); + }); + + it('handles node not existing in graph', () => { + const workflow = { status: { nodes: { node1: {} } } }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node2')).toEqual(emptyArtifacts); + }); + + it('handles an empty node', () => { + const workflow = { status: { nodes: { node1: {} } } }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual(emptyArtifacts); + }); + + it('handles a node with inputs but no artifact', () => { + const workflow = { status: { nodes: { node1: { inputs: {} } } } }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual(emptyArtifacts); + }); + + it('handles a node with inputs and empty artifact', () => { + const workflow = { status: { nodes: { node1: { inputs: { artifacts: [] } } } } }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual(emptyArtifacts); + }); + + it('handles a node with one input artifact', () => { + const workflow = { + status: { + nodes: { + node1: { + inputs: { + artifacts: [{ + name: 'input art1', + s3 + }] + } + } + } + } + }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual({ + inputArtifacts: [['input art1', s3]], + outputArtifacts: [], + }); + }); + + it('handles a node with one input artifact that has no s3 artifact config', () => { + const workflow = { + status: { + nodes: { + node1: { + inputs: { + artifacts: [{ + name: 'input art1', + }] + } + } + } + } + }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual({ + inputArtifacts: [['input art1', undefined]], + outputArtifacts: [], + }); + }); + + it('handles a node with one input artifact that is not the first node', () => { + const workflow = { + status: { + nodes: { + node1: { + inputs: { + artifacts: [{ + name: 'input art1', + s3: {...s3, key: 'in1'} + }] + } + }, + node2: { + inputs: { + artifacts: [{ + name: 'node2 input art1', + s3 + }] + } + } + } + } + }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node2')).toEqual({ + inputArtifacts: [['node2 input art1', s3]], + outputArtifacts: [], + }); + }); + + it('handles a node with one output artifact', () => { + const workflow = { + status: { + nodes: { + node1: { + outputs: { + artifacts: [{ + name: 'output art1', + s3 + }] + } + } + } + } + }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual({ + inputArtifacts: [], + outputArtifacts: [['output art1', s3]], + }); + }); + + it('handles a node with one input and one output artifacts', () => { + const workflow = { + status: { + nodes: { + node1: { + inputs: { + artifacts: [{ + name: 'input art1', + s3: {...s3, key: 'in1'} + }] + }, + outputs: { + artifacts: [{ + name: 'output art1', + s3: {...s3, key: 'out1'} + }] + }, + } + } + } + }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual({ + inputArtifacts: [['input art1', {...s3, key: 'in1'}]], + outputArtifacts: [['output art1', {...s3, key: 'out1'}]], + }); + }); + + it('handles a node with multiple input and output artifacts', () => { + const workflow = { + status: { + nodes: { + node1: { + inputs: { + artifacts: [{ + name: 'input art1', + s3: {...s3, key: 'in1'} + }, { + name: 'input art2', + s3: {...s3, key: 'in2'} + }, { + name: 'input art3', + s3: {...s3, key: 'in3'} + }], + }, + outputs: { + artifacts: [{ + name: 'output art1', + s3: {...s3, key: 'out1'} + }, { + name: 'output art2', + s3: {...s3, key: 'out2'} + }], + }, + } + } + } + }; + expect(WorkflowParser.getNodeInputOutputArtifacts(workflow as any, 'node1')).toEqual({ + inputArtifacts: [ + ['input art1', {...s3, key: 'in1'}], + ['input art2', {...s3, key: 'in2'}], + ['input art3', {...s3, key: 'in3'}], + ], + outputArtifacts: [ + ['output art1', {...s3, key: 'out1'}], + ['output art2', {...s3, key: 'out2'}], + ], + }); }); }); diff --git a/frontend/src/lib/WorkflowParser.ts b/frontend/src/lib/WorkflowParser.ts index fa060f811af..06e138d013b 100644 --- a/frontend/src/lib/WorkflowParser.ts +++ b/frontend/src/lib/WorkflowParser.ts @@ -17,11 +17,12 @@ import * as dagre from 'dagre'; import IconWithTooltip from '../atoms/IconWithTooltip'; import MoreIcon from '@material-ui/icons/MoreHoriz'; -import { Workflow, NodeStatus, Parameter } from '../../third_party/argo-ui/argo_template'; +import { Workflow, NodeStatus, Parameter, S3Artifact } from '../../third_party/argo-ui/argo_template'; import { statusToIcon } from '../pages/Status'; import { color } from '../Css'; import { Constants } from './Constants'; import { NodePhase, statusToBgColor, hasFinished } from './StatusUtils'; +import { KeyValue } from './StaticGraphParser'; export enum StorageService { GCS = 'gcs', @@ -168,33 +169,55 @@ export default class WorkflowParser { // Makes sure the workflow object contains the node and returns its // inputs/outputs if any, while looking out for any missing link in the chain to // the node's inputs/outputs. - public static getNodeInputOutputParams(workflow?: Workflow, nodeId?: string): [string[][], string[][]] { - type paramList = string[][]; + public static getNodeInputOutputParams(workflow?: Workflow, nodeId?: string): Record<'inputParams' | 'outputParams', Array>> { + type ParamList = Array>; + let inputParams: ParamList = []; + let outputParams: ParamList = []; if (!nodeId || !workflow || !workflow.status || !workflow.status.nodes || !workflow.status.nodes[nodeId]) { - return [[], []]; + return {inputParams, outputParams}; } - const node = workflow.status.nodes[nodeId]; - const inputsOutputs: [paramList, paramList] = [[], []]; - if (node.inputs && node.inputs.parameters) { - inputsOutputs[0] = node.inputs.parameters.map(p => [p.name, p.value || '']); + const {inputs, outputs} = workflow.status.nodes[nodeId]; + if (!!inputs && !!inputs.parameters) { + inputParams = inputs.parameters.map(p => [p.name, p.value || '']); + } + if (!!outputs && !!outputs.parameters) { + outputParams = outputs.parameters.map(p => [p.name, p.value || '']); + } + return {inputParams, outputParams}; + } + + // Makes sure the workflow object contains the node and returns its + // inputs/outputs artifacts if any, while looking out for any missing link in the chain to + // the node's inputs/outputs. + public static getNodeInputOutputArtifacts(workflow?: Workflow, nodeId?: string): Record<'inputArtifacts' | 'outputArtifacts', Array>> { + type ParamList = Array>; + let inputArtifacts: ParamList = []; + let outputArtifacts: ParamList = []; + if (!nodeId || !workflow || !workflow.status || !workflow.status.nodes || !workflow.status.nodes[nodeId]) { + return {inputArtifacts, outputArtifacts}; + } + + const {inputs, outputs} = workflow.status.nodes[nodeId]; + if (!!inputs && !!inputs.artifacts) { + inputArtifacts = inputs.artifacts.map(({name, s3}) => [name, s3]); } - if (node.outputs && node.outputs.parameters) { - inputsOutputs[1] = node.outputs.parameters.map(p => [p.name, p.value || '']); + if (!!outputs && !!outputs.artifacts) { + outputArtifacts = outputs.artifacts.map(({name, s3}) => [name, s3]); } - return inputsOutputs; + return {inputArtifacts, outputArtifacts}; } // Makes sure the workflow object contains the node and returns its // volume mounts if any. - public static getNodeVolumeMounts(workflow: Workflow, nodeId: string): string[][] { + public static getNodeVolumeMounts(workflow: Workflow, nodeId: string): Array> { if (!workflow || !workflow.status || !workflow.status.nodes || !workflow.status.nodes[nodeId] || !workflow.spec || !workflow.spec.templates) { return []; } const node = workflow.status.nodes[nodeId]; const tmpl = workflow.spec.templates.find(t => !!t && !!t.name && t.name === node.templateName); - let volumeMounts: string[][] = []; + let volumeMounts: Array> = []; if (tmpl && tmpl.container && tmpl.container.volumeMounts) { volumeMounts = tmpl.container.volumeMounts.map(v => [v.mountPath, v.name]); } @@ -203,14 +226,14 @@ export default class WorkflowParser { // Makes sure the workflow object contains the node and returns its // action and manifest. - public static getNodeManifest(workflow: Workflow, nodeId: string): string[][] { + public static getNodeManifest(workflow: Workflow, nodeId: string): Array> { if (!workflow || !workflow.status || !workflow.status.nodes || !workflow.status.nodes[nodeId] || !workflow.spec || !workflow.spec.templates) { return []; } const node = workflow.status.nodes[nodeId]; const tmpl = workflow.spec.templates.find(t => !!t && !!t.name && t.name === node.templateName); - let manifest: string[][] = []; + let manifest: Array> = []; if (tmpl && tmpl.resource && tmpl.resource.action && tmpl.resource.manifest) { manifest = [[tmpl.resource.action, tmpl.resource.manifest]]; } diff --git a/frontend/src/pages/RecurringRunDetails.tsx b/frontend/src/pages/RecurringRunDetails.tsx index 2fcec11352a..cc1b0f8890a 100644 --- a/frontend/src/pages/RecurringRunDetails.tsx +++ b/frontend/src/pages/RecurringRunDetails.tsx @@ -26,6 +26,7 @@ import { RoutePage, RouteParams } from '../components/Router'; import { Breadcrumb, ToolbarProps } from '../components/Toolbar'; import { classes } from 'typestyle'; import { commonCss, padding } from '../Css'; +import { KeyValue } from '../lib/StaticGraphParser'; import { formatDateString, enabledDisplayString, errorToMessage } from '../lib/Utils'; import { triggerDisplayString } from '../lib/TriggerUtils'; @@ -65,9 +66,9 @@ class RecurringRunDetails extends Page<{}, RecurringRunConfigState> { public render(): JSX.Element { const { run } = this.state; - let runDetails: string[][] = []; - let inputParameters: string[][] = []; - let triggerDetails: string[][] = []; + let runDetails: Array> = []; + let inputParameters: Array> = []; + let triggerDetails: Array> = []; if (run && run.pipeline_spec) { runDetails = [ ['Description', run.description!], diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx index 74e3a9279c5..e05fee3524d 100644 --- a/frontend/src/pages/RunDetails.tsx +++ b/frontend/src/pages/RunDetails.tsx @@ -21,6 +21,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import CompareTable from '../components/CompareTable'; import CompareUtils from '../lib/CompareUtils'; import DetailsTable from '../components/DetailsTable'; +import MinioArtifactLink from '../components/MinioArtifactLink'; import Graph from '../components/Graph'; import Hr from '../atoms/Hr'; import InfoIcon from '@material-ui/icons/InfoOutlined'; @@ -36,6 +37,7 @@ import { ApiRun, RunStorageState } from '../apis/run'; import { Apis } from '../lib/Apis'; import { NodePhase, hasFinished } from '../lib/StatusUtils'; import { OutputArtifactLoader } from '../lib/OutputArtifactLoader'; +import { KeyValue } from '../lib/StaticGraphParser'; import { Page } from './Page'; import { RoutePage, RouteParams } from '../components/Router'; import { ToolbarProps } from '../components/Toolbar'; @@ -199,7 +201,8 @@ class RunDetails extends Page { const selectedNodeId = selectedNodeDetails ? selectedNodeDetails.id : ''; const workflowParameters = WorkflowParser.getParameters(workflow); - const nodeInputOutputParams = WorkflowParser.getNodeInputOutputParams(workflow, selectedNodeId); + const {inputParams, outputParams} = WorkflowParser.getNodeInputOutputParams(workflow, selectedNodeId); + const {inputArtifacts, outputArtifacts} = WorkflowParser.getNodeInputOutputArtifacts(workflow, selectedNodeId); const hasMetrics = runMetadata && runMetadata.metrics && runMetadata.metrics.length > 0; const visualizationCreatorConfig: VisualizationCreatorConfig = { allowCustomVisualizations, @@ -261,10 +264,18 @@ class RunDetails extends Page { {sidepanelSelectedTab === SidePaneTab.INPUT_OUTPUT && (
+ fields={inputParams} /> + + + fields={outputParams} /> + +
)} @@ -579,7 +590,7 @@ class RunDetails extends Page { this.setStateSafe({ allArtifactConfigs }); } - private _getDetailsFields(workflow: Workflow, runMetadata?: ApiRun): string[][] { + private _getDetailsFields(workflow: Workflow, runMetadata?: ApiRun): Array> { return !workflow.status ? [] : [ ['Status', workflow.status.phase], ['Description', runMetadata ? runMetadata!.description! : ''], @@ -714,7 +725,7 @@ class RunDetails extends Page { }; generatedVisualizations.push(generatedVisualization); if (selectedNodeDetails) { - const viewerConfigs = selectedNodeDetails.viewerConfigs || []; + const viewerConfigs = selectedNodeDetails.viewerConfigs || []; viewerConfigs.push(generatedVisualization.config); selectedNodeDetails.viewerConfigs = viewerConfigs; } diff --git a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap index 1fe280a7f12..962c621e497 100644 --- a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap @@ -2213,6 +2213,11 @@ exports[`RunDetails switches to inputs/outputs tab in side pane 1`] = ` } title="Input parameters" /> + +
diff --git a/frontend/third_party/argo-ui/kubernetes.ts b/frontend/third_party/argo-ui/kubernetes.ts index fc9627ccab0..405b5d30f02 100644 --- a/frontend/third_party/argo-ui/kubernetes.ts +++ b/frontend/third_party/argo-ui/kubernetes.ts @@ -18,7 +18,6 @@ export type Volume = any; export type EnvFromSource = any; export type EnvVarSource = any; export type ResourceRequirements = any; -export type VolumeMount = any; export type Probe = any; export type Lifecycle = any; export type TerminationMessagePolicy = any; @@ -27,6 +26,12 @@ export type SecurityContext = any; export type PersistentVolumeClaim = any; export type Affinity = any; + +export interface VolumeMount { + name: string; + mountPath?: string; +} + export interface ListMeta { _continue?: string; resourceVersion?: string;