From 5ee8e7ad89a9898cd8c30c4c5c018cf9a08ac750 Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Thu, 24 Jan 2019 16:00:00 -0800 Subject: [PATCH 1/6] Improve runtime graph starting and running experience - displays spinner when no nodes have started - adds info message to bottom of runtime graph indicating that it is runtime and that the graph will update over time - adds placeholder nodes at end of graph to indicate future progress --- frontend/src/components/Graph.tsx | 28 +++++++++-- .../{WorkflowParser.ts => WorkflowParser.tsx} | 35 +++++++++++-- frontend/src/pages/RunDetails.tsx | 50 ++++++++++++++++--- 3 files changed, 97 insertions(+), 16 deletions(-) rename frontend/src/lib/{WorkflowParser.ts => WorkflowParser.tsx} (87%) diff --git a/frontend/src/components/Graph.tsx b/frontend/src/components/Graph.tsx index 9fa531b26c5..1a125f22c98 100644 --- a/frontend/src/components/Graph.tsx +++ b/frontend/src/components/Graph.tsx @@ -32,9 +32,11 @@ interface Line { } interface Edge { + color?: string; from: string; to: string; lines: Line[]; + isPlaceholder?: boolean; } const css = stylesheet({ @@ -93,6 +95,12 @@ const css = stylesheet({ backgroundColor: '#e4ebff !important', borderColor: color.theme, }, + placeholderNode: { + margin: 10, + position: 'absolute', + // TODO: can this be calculated? + transform: 'translate(76px, 20px)' + }, root: { backgroundColor: color.graphBg, borderLeft: 'solid 1px ' + color.divider, @@ -153,15 +161,22 @@ export default class Graph extends React.Component { } } } - displayEdges.push({ from: edgeInfo.v, to: edgeInfo.w, lines }); + displayEdges.push({ + color: edge.color, + from: edgeInfo.v, + isPlaceholder: edge.isPlaceholder, + lines, + to: edgeInfo.w + }); }); return (
{graph.nodes().map(id => Object.assign(graph.node(id), { id })).map((node, i) => ( -
this.props.onClick && this.props.onClick(node.id)} style={{ + onClick={() => (!node.isPlaceholder && this.props.onClick) && this.props.onClick(node.id)} + style={{ backgroundColor: node.bgColor, left: node.x, maxHeight: node.height, minHeight: node.height, top: node.y, width: node.width, }}> @@ -173,8 +188,13 @@ export default class Graph extends React.Component { {displayEdges.map((edge, i) => (
{edge.lines.map((line, l) => ( -
- workflowNodes[id].name === `${workflowName}.onExit`); + Object.keys(workflowNodes).find((id) => workflowNodes[id].name === `${workflowName}.onExit`); if (onExitHandlerNodeId) { this.getOutboundNodes(workflow, workflowName).forEach((nodeId) => g.setEdge(nodeId, onExitHandlerNodeId)); @@ -64,17 +68,28 @@ export default class WorkflowParser { delete workflowNodes[workflowName]; } + const runningNodeSuffix = '-running-placeholder'; + // Create dagre graph nodes from workflow nodes. (Object as any).values(workflowNodes) .forEach((node: NodeStatus) => { - const workflowNode = workflowNodes[node.id]; g.setNode(node.id, { height: NODE_HEIGHT, - icon: statusToIcon(workflowNode.phase as NodePhase, workflowNode.startedAt, workflowNode.finishedAt), + icon: statusToIcon(node.phase as NodePhase, node.startedAt, node.finishedAt), label: node.displayName || node.id, width: NODE_WIDTH, ...node, }); + + if (!hasFinished(node.phase as NodePhase)) { + g.setNode(node.id + runningNodeSuffix, { + height: PLACEHOLDER_NODE_DIMENSION, + icon: this._placeholderNodeIcon(), + isPlaceholder: true, + width: PLACEHOLDER_NODE_DIMENSION, + }); + g.setEdge(node.id, node.id + runningNodeSuffix, { color: color.weak, isPlaceholder: true }); + } }); // Connect dagre graph nodes with edges. @@ -247,4 +262,14 @@ export default class WorkflowParser { return ''; } } + + private static _placeholderNodeIcon(): JSX.Element { + return ( + + + + + + ); + } } diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx index bfd987ef24b..139e971095f 100644 --- a/frontend/src/pages/RunDetails.tsx +++ b/frontend/src/pages/RunDetails.tsx @@ -20,6 +20,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import DetailsTable from '../components/DetailsTable'; import Graph from '../components/Graph'; import Hr from '../atoms/Hr'; +import InfoIcon from '@material-ui/icons/InfoOutlined'; import LogViewer from '../components/LogViewer'; import MD2Tabs from '../atoms/MD2Tabs'; import PlotCard from '../components/PlotCard'; @@ -38,8 +39,8 @@ import { ToolbarProps } from '../components/Toolbar'; import { URLParser } from '../lib/URLParser'; import { ViewerConfig } from '../components/viewers/Viewer'; import { Workflow } from '../../third_party/argo-ui/argo_template'; -import { classes } from 'typestyle'; -import { commonCss, padding } from '../Css'; +import { classes, stylesheet } from 'typestyle'; +import { commonCss, padding, color, fonts, fontsize } from '../Css'; import { componentMap } from '../components/viewers/ViewerContainer'; import { flatten } from 'lodash'; import { formatDateString, getRunTime, logger, errorToMessage } from '../lib/Utils'; @@ -82,6 +83,22 @@ interface RunDetailsState { workflow?: Workflow; } +export const css = stylesheet({ + footer: { + background: color.graphBg, + display: 'flex', + padding: '0 0 20px 20px', + }, + infoSpan: { + color: color.lowContrast, + fontFamily: fonts.secondary, + fontSize: fontsize.small, + letterSpacing: '0.21px', + lineHeight: '24px', + paddingLeft: 6, + }, +}); + class RunDetails extends Page { private _onBlur: EventListener; private _onFocus: EventListener; @@ -127,7 +144,7 @@ class RunDetails extends Page { } public render(): JSX.Element { - const { allArtifactConfigs, graph, runMetadata, selectedTab, selectedNodeDetails, + const { allArtifactConfigs, graph, runFinished, runMetadata, selectedTab, selectedNodeDetails, sidepanelSelectedTab, workflow } = this.state; const selectedNodeId = selectedNodeDetails ? selectedNodeDetails.id : ''; @@ -142,7 +159,7 @@ class RunDetails extends Page { onSwitch={(tab: number) => this.setStateSafe({ selectedTab: tab })} />
- {selectedTab === 0 &&
+ {selectedTab === 0 &&
{graph &&
this._selectNode(id)} /> @@ -151,8 +168,7 @@ class RunDetails extends Page { onClose={() => this.setStateSafe({ selectedNodeDetails: null })} title={selectedNodeId}> {!!selectedNodeDetails && ( {!!selectedNodeDetails.phaseMessage && ( - + )}
{
)} + +
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
} - {!graph && No graph to show} + {!graph && ( +
+ {runFinished && ( + + No graph to show + + )} + {!runFinished && ( + + )} +
+ )}
} {selectedTab === 1 && ( From 7ed6260a4e12014335ef470678fb3cef9bd5d28a Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Fri, 25 Jan 2019 13:30:56 -0800 Subject: [PATCH 2/6] Update test snapshots --- .../__snapshots__/Graph.test.tsx.snap | 52 +++ frontend/src/lib/WorkflowParser.tsx | 2 +- .../__snapshots__/RunDetails.test.tsx.snap | 320 +++++++++++++++++- 3 files changed, 365 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/__snapshots__/Graph.test.tsx.snap b/frontend/src/components/__snapshots__/Graph.test.tsx.snap index 02d906451e2..fde8592f503 100644 --- a/frontend/src/components/__snapshots__/Graph.test.tsx.snap +++ b/frontend/src/components/__snapshots__/Graph.test.tsx.snap @@ -60,6 +60,8 @@ exports[`Graph gracefully renders a graph with a selected node id that does not key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -72,6 +74,8 @@ exports[`Graph gracefully renders a graph with a selected node id that does not key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -252,6 +256,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 39.57233047033631, "top": 22.5, "transform": "translate(100px, 44px) rotate(-45deg)", @@ -264,6 +270,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 32.25, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -280,6 +288,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 74.57233047033631, "top": 22.5, "transform": "translate(100px, 44px) rotate(-135deg)", @@ -292,6 +302,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 92.25, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -308,6 +320,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 32.25, "top": 82.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -320,6 +334,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 32.25, "top": 107.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -336,6 +352,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 0.375, "top": 81.875, "transform": "translate(100px, 44px) rotate(-36.86989764584402deg)", @@ -348,6 +366,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -10.25, "top": 110, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -360,6 +380,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="2" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -10.25, "top": 140, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -372,6 +394,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="3" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 167.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -388,6 +412,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 45.375, "top": 81.875, "transform": "translate(100px, 44px) rotate(-143.13010235415598deg)", @@ -400,6 +426,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 69.75, "top": 110, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -412,6 +440,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="2" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 69.75, "top": 140, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -424,6 +454,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="3" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 72.25, "top": 167.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -440,6 +472,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 32.25, "top": 142.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -452,6 +486,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 5.375, "top": 168.125, "transform": "translate(100px, 44px) rotate(-36.86989764584402deg)", @@ -468,6 +504,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 46.50406530937789, "top": 141.25, "transform": "translate(100px, 44px) rotate(-153.43494882292202deg)", @@ -480,6 +518,8 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": 81.39353635223337, "top": 167.5, "transform": "translate(100px, 44px) rotate(-56.30993247402021deg)", @@ -628,6 +668,8 @@ exports[`Graph renders a graph with a selected node 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -640,6 +682,8 @@ exports[`Graph renders a graph with a selected node 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -753,6 +797,8 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -765,6 +811,8 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -847,6 +895,8 @@ exports[`Graph renders a graph with two connectd nodes in reverse order 1`] = ` key="0" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", @@ -859,6 +909,8 @@ exports[`Graph renders a graph with two connectd nodes in reverse order 1`] = ` key="1" style={ Object { + "borderTopColor": undefined, + "borderTopStyle": "solid", "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", diff --git a/frontend/src/lib/WorkflowParser.tsx b/frontend/src/lib/WorkflowParser.tsx index af989219a04..e8f5866e1ff 100644 --- a/frontend/src/lib/WorkflowParser.tsx +++ b/frontend/src/lib/WorkflowParser.tsx @@ -265,7 +265,7 @@ export default class WorkflowParser { private static _placeholderNodeIcon(): JSX.Element { return ( - + diff --git a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap index 0b9970b5d0d..f0f454c35f4 100644 --- a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap @@ -23,6 +23,11 @@ exports[`RunDetails closes side panel when close button is clicked 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -90,6 +117,11 @@ exports[`RunDetails does not load logs if clicked node status is skipped 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -189,6 +243,11 @@ exports[`RunDetails keeps side pane open and on same tab when logs change after >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -382,6 +463,11 @@ exports[`RunDetails loads and shows logs in side pane 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -532,6 +640,11 @@ exports[`RunDetails opens side panel when graph node is clicked 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -622,16 +757,23 @@ exports[`RunDetails renders an empty run 1`] = ` >
- + - No graph to show - + > + No graph to show + +
@@ -661,6 +803,11 @@ exports[`RunDetails shows a one-node graph 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -728,6 +897,11 @@ exports[`RunDetails shows clicked node message in side panel 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -822,6 +1018,11 @@ exports[`RunDetails shows clicked node output in side pane 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -931,6 +1154,11 @@ exports[`RunDetails shows error banner atop logs area if fetching logs failed 1` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -1356,6 +1606,11 @@ exports[`RunDetails switches to inputs/outputs tab in side pane 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
@@ -1473,6 +1750,11 @@ exports[`RunDetails switches to logs tab in side pane 1`] = ` >
+
+
+ + + Runtime execution graph. Only steps that are currently running or have already completed are shown + +
+
From 2c6da1db170933cd6bbc73f8d97f8108c0ec67ca Mon Sep 17 00:00:00 2001 From: Riley Bauer Date: Fri, 25 Jan 2019 15:41:44 -0800 Subject: [PATCH 3/6] Fix bug with virtual nodes, and add transition to graph --- frontend/src/components/Graph.tsx | 7 +- .../__snapshots__/Graph.test.tsx.snap | 43 +++++ frontend/src/lib/WorkflowParser.tsx | 2 +- frontend/src/pages/RunDetails.tsx | 14 +- .../__snapshots__/RunDetails.test.tsx.snap | 150 ++---------------- 5 files changed, 72 insertions(+), 144 deletions(-) diff --git a/frontend/src/components/Graph.tsx b/frontend/src/components/Graph.tsx index 1a125f22c98..f528e3e9c4a 100644 --- a/frontend/src/components/Graph.tsx +++ b/frontend/src/components/Graph.tsx @@ -178,7 +178,11 @@ export default class Graph extends React.Component { onClick={() => (!node.isPlaceholder && this.props.onClick) && this.props.onClick(node.id)} style={{ backgroundColor: node.bgColor, left: node.x, - maxHeight: node.height, minHeight: node.height, top: node.y, width: node.width, + maxHeight: node.height, + minHeight: node.height, + top: node.y, + transition: 'left 0.5s, top 0.5s', + width: node.width, }}>
{node.label}
{node.icon}
@@ -198,6 +202,7 @@ export default class Graph extends React.Component { left: line.left, top: line.yMid, transform: `translate(100px, 44px) rotate(${line.angle}deg)`, + transition: 'left 0.5s, top 0.5s', width: line.distance, }} /> ))} diff --git a/frontend/src/components/__snapshots__/Graph.test.tsx.snap b/frontend/src/components/__snapshots__/Graph.test.tsx.snap index fde8592f503..f52dc976d92 100644 --- a/frontend/src/components/__snapshots__/Graph.test.tsx.snap +++ b/frontend/src/components/__snapshots__/Graph.test.tsx.snap @@ -15,6 +15,7 @@ exports[`Graph gracefully renders a graph with a selected node id that does not "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -39,6 +40,7 @@ exports[`Graph gracefully renders a graph with a selected node id that does not "maxHeight": 10, "minHeight": 10, "top": 65, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -65,6 +67,7 @@ exports[`Graph gracefully renders a graph with a selected node id that does not "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -79,6 +82,7 @@ exports[`Graph gracefully renders a graph with a selected node id that does not "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -115,6 +119,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -139,6 +144,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 65, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -163,6 +169,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 65, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -187,6 +194,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 125, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -211,6 +219,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 185, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -235,6 +244,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 185, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -261,6 +271,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 39.57233047033631, "top": 22.5, "transform": "translate(100px, 44px) rotate(-45deg)", + "transition": "left 0.5s, top 0.5s", "width": 35.85533905932738, } } @@ -275,6 +286,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 32.25, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -293,6 +305,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 74.57233047033631, "top": 22.5, "transform": "translate(100px, 44px) rotate(-135deg)", + "transition": "left 0.5s, top 0.5s", "width": 35.85533905932738, } } @@ -307,6 +320,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 92.25, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -325,6 +339,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 32.25, "top": 82.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -339,6 +354,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 32.25, "top": 107.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -357,6 +373,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 0.375, "top": 81.875, "transform": "translate(100px, 44px) rotate(-36.86989764584402deg)", + "transition": "left 0.5s, top 0.5s", "width": 44.25, } } @@ -371,6 +388,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": -10.25, "top": 110, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 30.5, } } @@ -385,6 +403,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": -10.25, "top": 140, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 30.5, } } @@ -399,6 +418,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": -7.75, "top": 167.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -417,6 +437,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 45.375, "top": 81.875, "transform": "translate(100px, 44px) rotate(-143.13010235415598deg)", + "transition": "left 0.5s, top 0.5s", "width": 44.25, } } @@ -431,6 +452,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 69.75, "top": 110, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 30.5, } } @@ -445,6 +467,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 69.75, "top": 140, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 30.5, } } @@ -459,6 +482,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 72.25, "top": 167.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -477,6 +501,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 32.25, "top": 142.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -491,6 +516,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 5.375, "top": 168.125, "transform": "translate(100px, 44px) rotate(-36.86989764584402deg)", + "transition": "left 0.5s, top 0.5s", "width": 44.25, } } @@ -509,6 +535,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 46.50406530937789, "top": 141.25, "transform": "translate(100px, 44px) rotate(-153.43494882292202deg)", + "transition": "left 0.5s, top 0.5s", "width": 61.991869381244214, } } @@ -523,6 +550,7 @@ exports[`Graph renders a complex graph with six nodes and seven edges 1`] = ` "left": 81.39353635223337, "top": 167.5, "transform": "translate(100px, 44px) rotate(-56.30993247402021deg)", + "transition": "left 0.5s, top 0.5s", "width": 30.54626062886658, } } @@ -623,6 +651,7 @@ exports[`Graph renders a graph with a selected node 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -647,6 +676,7 @@ exports[`Graph renders a graph with a selected node 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 65, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -673,6 +703,7 @@ exports[`Graph renders a graph with a selected node 1`] = ` "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -687,6 +718,7 @@ exports[`Graph renders a graph with a selected node 1`] = ` "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -721,6 +753,7 @@ exports[`Graph renders a graph with one node 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -752,6 +785,7 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -776,6 +810,7 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 65, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -802,6 +837,7 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -816,6 +852,7 @@ exports[`Graph renders a graph with two connectd nodes 1`] = ` "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -850,6 +887,7 @@ exports[`Graph renders a graph with two connectd nodes in reverse order 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 65, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -874,6 +912,7 @@ exports[`Graph renders a graph with two connectd nodes in reverse order 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -900,6 +939,7 @@ exports[`Graph renders a graph with two connectd nodes in reverse order 1`] = ` "left": -7.75, "top": 22.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -914,6 +954,7 @@ exports[`Graph renders a graph with two connectd nodes in reverse order 1`] = ` "left": -7.75, "top": 47.5, "transform": "translate(100px, 44px) rotate(-90deg)", + "transition": "left 0.5s, top 0.5s", "width": 25.5, } } @@ -948,6 +989,7 @@ exports[`Graph renders a graph with two disparate nodes 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } @@ -972,6 +1014,7 @@ exports[`Graph renders a graph with two disparate nodes 1`] = ` "maxHeight": 10, "minHeight": 10, "top": 5, + "transition": "left 0.5s, top 0.5s", "width": 10, } } diff --git a/frontend/src/lib/WorkflowParser.tsx b/frontend/src/lib/WorkflowParser.tsx index e8f5866e1ff..069264a8de2 100644 --- a/frontend/src/lib/WorkflowParser.tsx +++ b/frontend/src/lib/WorkflowParser.tsx @@ -81,7 +81,7 @@ export default class WorkflowParser { ...node, }); - if (!hasFinished(node.phase as NodePhase)) { + if (!hasFinished(node.phase as NodePhase) && !this.isVirtual(node)) { g.setNode(node.id + runningNodeSuffix, { height: PLACEHOLDER_NODE_DIMENSION, icon: this._placeholderNodeIcon(), diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx index 139e971095f..ff72dc30eca 100644 --- a/frontend/src/pages/RunDetails.tsx +++ b/frontend/src/pages/RunDetails.tsx @@ -89,6 +89,11 @@ export const css = stylesheet({ display: 'flex', padding: '0 0 20px 20px', }, + graphPane: { + backgroundColor: color.graphBg, + overflow: 'hidden', + position: 'relative', + }, infoSpan: { color: color.lowContrast, fontFamily: fonts.secondary, @@ -159,8 +164,8 @@ class RunDetails extends Page { onSwitch={(tab: number) => this.setStateSafe({ selectedTab: tab })} />
- {selectedTab === 0 &&
- {graph &&
+ {selectedTab === 0 &&
+ {graph &&
this._selectNode(id)} /> @@ -404,9 +409,10 @@ class RunDetails extends Page { private _stopAutoRefresh(): void { if (this._interval !== undefined) { clearInterval(this._interval); + + // Reset interval to indicate that a new one can be set + this._interval = undefined; } - // Reset interval to indicate that a new one can be set - this._interval = undefined; } private async _loadAllOutputs(): Promise { diff --git a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap index f0f454c35f4..edefa6afbe1 100644 --- a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap @@ -22,21 +22,10 @@ exports[`RunDetails closes side panel when close button is clicked 1`] = ` className="page" >
Date: Mon, 28 Jan 2019 11:32:16 -0800 Subject: [PATCH 4/6] PR comments, updating tests --- frontend/src/Css.tsx | 7 +- frontend/src/components/Graph.tsx | 2 +- frontend/src/lib/WorkflowParser.tsx | 4 +- frontend/src/pages/PipelineDetails.tsx | 2 +- frontend/src/pages/RunDetails.tsx | 4 +- .../ExperimentList.test.tsx.snap | 2 +- .../PipelineDetails.test.tsx.snap | 40 +------ .../__snapshots__/RunDetails.test.tsx.snap | 110 ++++-------------- .../pages/__snapshots__/Status.test.tsx.snap | 2 +- 9 files changed, 41 insertions(+), 132 deletions(-) diff --git a/frontend/src/Css.tsx b/frontend/src/Css.tsx index 2eaaf162248..34e55e89e45 100644 --- a/frontend/src/Css.tsx +++ b/frontend/src/Css.tsx @@ -38,7 +38,7 @@ export const color = { theme: '#1a73e8', themeDarker: '#0b59dc', warningBg: '#f9f9e1', - weak: '#999', + weak: '#9AA0A6', }; export const dimension = { @@ -223,6 +223,11 @@ export const commonCss = stylesheet({ paddingBottom: 16, paddingTop: 20, }, + infoIcon: { + color: color.lowContrast, + height: 16, + width: 16 + }, link: { $nest: { '&:hover': { diff --git a/frontend/src/components/Graph.tsx b/frontend/src/components/Graph.tsx index f528e3e9c4a..d1244da3094 100644 --- a/frontend/src/components/Graph.tsx +++ b/frontend/src/components/Graph.tsx @@ -99,7 +99,7 @@ const css = stylesheet({ margin: 10, position: 'absolute', // TODO: can this be calculated? - transform: 'translate(76px, 20px)' + transform: 'translate(73px, 16px)' }, root: { backgroundColor: color.graphBg, diff --git a/frontend/src/lib/WorkflowParser.tsx b/frontend/src/lib/WorkflowParser.tsx index 069264a8de2..43a2b6f73cf 100644 --- a/frontend/src/lib/WorkflowParser.tsx +++ b/frontend/src/lib/WorkflowParser.tsx @@ -266,8 +266,8 @@ export default class WorkflowParser { private static _placeholderNodeIcon(): JSX.Element { return ( - - + + ); diff --git a/frontend/src/pages/PipelineDetails.tsx b/frontend/src/pages/PipelineDetails.tsx index 3be2e9be343..fdbba7a50ea 100644 --- a/frontend/src/pages/PipelineDetails.tsx +++ b/frontend/src/pages/PipelineDetails.tsx @@ -223,7 +223,7 @@ class PipelineDetails extends Page<{}, PipelineDetailsState> { )}
- + Static pipeline graph
diff --git a/frontend/src/pages/RunDetails.tsx b/frontend/src/pages/RunDetails.tsx index ff72dc30eca..ffbbe3e8916 100644 --- a/frontend/src/pages/RunDetails.tsx +++ b/frontend/src/pages/RunDetails.tsx @@ -232,9 +232,9 @@ class RunDetails extends Page {
- + - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
diff --git a/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap b/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap index 648fe24dae7..6d684d4c38f 100644 --- a/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/ExperimentList.test.tsx.snap @@ -241,7 +241,7 @@ exports[`ExperimentList renders last 5 runs statuses 1`] = ` - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -176,18 +170,12 @@ exports[`RunDetails does not load logs if clicked node status is skipped 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -291,18 +279,12 @@ exports[`RunDetails keeps side pane open and on same tab when logs change after className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -500,18 +482,12 @@ exports[`RunDetails loads and shows logs in side pane 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -657,18 +633,12 @@ exports[`RunDetails opens side panel when graph node is clicked 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -781,18 +751,12 @@ exports[`RunDetails shows a one-node graph 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -891,18 +855,12 @@ exports[`RunDetails shows clicked node message in side panel 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -1016,18 +974,12 @@ exports[`RunDetails shows clicked node output in side pane 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -1129,18 +1081,12 @@ exports[`RunDetails shows error banner atop logs area if fetching logs failed 1` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -1590,18 +1536,12 @@ exports[`RunDetails switches to inputs/outputs tab in side pane 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
@@ -1709,18 +1649,12 @@ exports[`RunDetails switches to logs tab in side pane 1`] = ` className="flex" > - Runtime execution graph. Only steps that are currently running or have already completed are shown + Runtime execution graph. Only steps that are currently running or have already completed are shown.
diff --git a/frontend/src/pages/__snapshots__/Status.test.tsx.snap b/frontend/src/pages/__snapshots__/Status.test.tsx.snap index 93abc666d0a..aea28e41549 100644 --- a/frontend/src/pages/__snapshots__/Status.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/Status.test.tsx.snap @@ -3318,7 +3318,7 @@ exports[`Status statusToIcon renders an icon with tooltip for phase: PENDING 1`] Date: Mon, 28 Jan 2019 17:05:36 -0800 Subject: [PATCH 5/6] Moves IconWithTooltip to atoms/ and adds tests --- frontend/src/atoms/IconWithTooltip.test.tsx | 37 ++ frontend/src/atoms/IconWithTooltip.tsx | 41 ++ .../IconWithTooltip.test.tsx.snap | 69 +++ frontend/src/components/Graph.test.tsx | 36 +- .../__snapshots__/Graph.test.tsx.snap | 433 +++++++++++++++++- frontend/src/lib/WorkflowParser.test.ts | 151 +++++- .../{WorkflowParser.tsx => WorkflowParser.ts} | 21 +- frontend/src/pages/RunDetails.test.tsx | 44 ++ .../__snapshots__/RunDetails.test.tsx.snap | 77 ++++ 9 files changed, 867 insertions(+), 42 deletions(-) create mode 100644 frontend/src/atoms/IconWithTooltip.test.tsx create mode 100644 frontend/src/atoms/IconWithTooltip.tsx create mode 100644 frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap rename frontend/src/lib/{WorkflowParser.tsx => WorkflowParser.ts} (96%) diff --git a/frontend/src/atoms/IconWithTooltip.test.tsx b/frontend/src/atoms/IconWithTooltip.test.tsx new file mode 100644 index 00000000000..a6b79d76450 --- /dev/null +++ b/frontend/src/atoms/IconWithTooltip.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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 * as React from 'react'; +import IconWithTooltip from './IconWithTooltip'; +import TestIcon from '@material-ui/icons/Help'; +import { create } from 'react-test-renderer'; + +describe('IconWithTooltip', () => { + it('renders without height or weight', () => { + const tree = create( + + ); + expect(tree).toMatchSnapshot(); + }); + + it('renders with height and weight', () => { + const tree = create( + + ); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/atoms/IconWithTooltip.tsx b/frontend/src/atoms/IconWithTooltip.tsx new file mode 100644 index 00000000000..c8487a60f65 --- /dev/null +++ b/frontend/src/atoms/IconWithTooltip.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2018 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 * as React from 'react'; +import Tooltip from '@material-ui/core/Tooltip'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; + +interface IconWithTooltipProps { + Icon: React.ComponentType; + height?: number; + iconColor: string; + tooltip: string; + width?: number; +} + +export default (props: IconWithTooltipProps) => { + const { height, Icon, iconColor, tooltip, width } = props; + + return ( + + + + ); +}; diff --git a/frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap b/frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap new file mode 100644 index 00000000000..b4c33081005 --- /dev/null +++ b/frontend/src/atoms/__snapshots__/IconWithTooltip.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IconWithTooltip renders with height and weight 1`] = ` + +`; + +exports[`IconWithTooltip renders without height or weight 1`] = ` + +`; diff --git a/frontend/src/components/Graph.test.tsx b/frontend/src/components/Graph.test.tsx index d12d7af7b5a..4de7c1baefd 100644 --- a/frontend/src/components/Graph.test.tsx +++ b/frontend/src/components/Graph.test.tsx @@ -18,6 +18,8 @@ import * as dagre from 'dagre'; import * as React from 'react'; import { shallow } from 'enzyme'; import Graph from './Graph'; +import SuccessIcon from '@material-ui/icons/CheckCircle'; +import Tooltip from '@material-ui/core/Tooltip'; function newGraph(): dagre.graphlib.Graph { const graph = new dagre.graphlib.Graph(); @@ -26,8 +28,17 @@ function newGraph(): dagre.graphlib.Graph { return graph; } -const newNode = (label: string) => ({ +const testIcon = ( + + + +); + +const newNode = (label: string, isPlaceHolder?: boolean, color?: string, icon?: JSX.Element) => ({ + bgColor: color, height: 10, + icon: icon || testIcon, + isPlaceholder: isPlaceHolder || false, label, width: 10, }); @@ -86,6 +97,29 @@ describe('Graph', () => { expect(shallow()).toMatchSnapshot(); }); + it('renders a graph with colored nodes', () => { + const graph = newGraph(); + graph.setNode('node1', newNode('node1', false, 'red')); + graph.setNode('node2', newNode('node2', false, 'green')); + expect(shallow()).toMatchSnapshot(); + }); + + it('renders a graph with colored edges', () => { + const graph = newGraph(); + graph.setNode('node1', newNode('node1')); + graph.setNode('node2', newNode('node2')); + graph.setEdge('node1', 'node2', { color: 'red' }); + expect(shallow()).toMatchSnapshot(); + }); + + it('renders a graph with a placeholder node and edge', () => { + const graph = newGraph(); + graph.setNode('node1', newNode('node1', false)); + graph.setNode('node2', newNode('node2', true)); + graph.setEdge('node1', 'node2', { isPlaceholder: true }); + expect(shallow()).toMatchSnapshot(); + }); + it('calls onClick callback when node is clicked', () => { const graph = newGraph(); graph.setNode('node1', newNode('node1')); diff --git a/frontend/src/components/__snapshots__/Graph.test.tsx.snap b/frontend/src/components/__snapshots__/Graph.test.tsx.snap index f52dc976d92..60b6d75c279 100644 --- a/frontend/src/components/__snapshots__/Graph.test.tsx.snap +++ b/frontend/src/components/__snapshots__/Graph.test.tsx.snap @@ -27,7 +27,13 @@ exports[`Graph gracefully renders a graph with a selected node id that does not
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
`; +exports[`Graph renders a graph with a placeholder node and edge 1`] = ` +
+
+
+ node1 +
+
+ + + +
+
+
+
+ node2 +
+
+ + + +
+
+
+
+
+
+
+
+`; + exports[`Graph renders a graph with a selected node 1`] = `
+ > + + + +
+ > + + + +
`; -exports[`Graph renders a graph with one node 1`] = ` +exports[`Graph renders a graph with colored edges 1`] = `
@@ -765,7 +939,196 @@ exports[`Graph renders a graph with one node 1`] = `
+ + + +
+
+
+
+ node2 +
+
+ + + +
+
+
+
+
+
+
+
+`; + +exports[`Graph renders a graph with colored nodes 1`] = ` +
+
+
+ node1 +
+
+ + + +
+
+
+
+ node2 +
+
+ + + +
+
+
+`; + +exports[`Graph renders a graph with one node 1`] = ` +
+
+
+ node1 +
+
+ + + +
`; @@ -797,7 +1160,13 @@ exports[`Graph renders a graph with two connectd nodes 1`] = `
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
+ > + + + +
`; diff --git a/frontend/src/lib/WorkflowParser.test.ts b/frontend/src/lib/WorkflowParser.test.ts index dd846136dff..51a5e13e13f 100644 --- a/frontend/src/lib/WorkflowParser.test.ts +++ b/frontend/src/lib/WorkflowParser.test.ts @@ -16,6 +16,7 @@ import WorkflowParser, { StorageService } from './WorkflowParser'; import { NodePhase } from '../pages/Status'; +import { color } from '../Css'; describe('WorkflowParser', () => { describe('createRuntimeGraph', () => { @@ -79,37 +80,165 @@ describe('WorkflowParser', () => { it('creates graph with exit handler attached', () => { const workflow = { - metadata: { name: 'node1' }, + metadata: { name: 'virtualRoot' }, status: { nodes: { node1: { displayName: 'node1', id: 'node1', name: 'node1', - outboundNodes: ['node2'], phase: 'Succeeded', - type: 'Steps', + type: 'Pod', }, node2: { displayName: 'node2', id: 'node2', - name: 'node2', + name: 'virtualRoot.onExit', phase: 'Succeeded', type: 'Pod', }, - node3: { - displayName: 'node3', - id: 'node3', - name: 'node1.onExit', + virtualRoot: { + displayName: 'virtualRoot', + id: 'virtualRoot', + name: 'virtualRoot', + outboundNodes: ['node1'], + phase: 'Succeeded', + type: 'Steps', + }, + }, + } + }; + const g = WorkflowParser.createRuntimeGraph(workflow as any); + expect(g.nodes()).toEqual(['node1', 'node2']); + expect(g.edges()).toEqual([{ v: 'node1', w: 'node2' }]); + }); + + it('creates a graph with placeholder nodes for steps that are not finished', () => { + const workflow = { + metadata: { name: 'testWorkflow' }, + status: { + nodes: { + finishedNode: { + displayName: 'finishedNode', + id: 'finishedNode', + name: 'finishedNode', phase: 'Succeeded', type: 'Pod', - } + }, + pendingNode: { + displayName: 'pendingNode', + id: 'pendingNode', + name: 'pendingNode', + phase: 'Pending', + type: 'Pod', + }, + root: { + children: ['pendingNode', 'runningNode', 'finishedNode'], + displayName: 'root', + id: 'root', + name: 'root', + phase: 'Succeeded', + type: 'Pod', + }, + runningNode: { + displayName: 'runningNode', + id: 'runningNode', + name: 'runningNode', + phase: 'Running', + type: 'Pod', + }, + }, + } + }; + const g = WorkflowParser.createRuntimeGraph(workflow as any); + expect(g.nodes()).toEqual([ + 'finishedNode', + 'pendingNode', + 'pendingNode-running-placeholder', + 'root', + 'runningNode', + 'runningNode-running-placeholder' + ]); + expect(g.edges()).toEqual(expect.arrayContaining([ + { v: 'root', w: 'pendingNode' }, + { v: 'root', w: 'runningNode' }, + { v: 'root', w: 'finishedNode' }, + { v: 'pendingNode', w: 'pendingNode-running-placeholder' }, + { v: 'runningNode', w: 'runningNode-running-placeholder' }, + ])); + }); + + it('sets specific properties for placeholder nodes', () => { + const workflow = { + metadata: { name: 'testWorkflow' }, + status: { + nodes: { + root: { + children: ['runningNode'], + displayName: 'root', + id: 'root', + name: 'root', + phase: 'Succeeded', + type: 'Pod', + }, + runningNode: { + displayName: 'runningNode', + id: 'runningNode', + name: 'runningNode', + phase: 'Running', + type: 'Pod', + }, }, } }; const g = WorkflowParser.createRuntimeGraph(workflow as any); - expect(g.nodes()).toEqual(['node2', 'node3']); - expect(g.edges()).toEqual([{ v: 'node2', w: 'node3' }]); + + const runningNode = g.node('runningNode'); + expect(runningNode.height).toEqual(70); + expect(runningNode.width).toEqual(180); + expect(runningNode.label).toEqual('runningNode'); + expect(runningNode.isPlaceholder).toBeUndefined(); + + const placeholderNode = g.node('runningNode-running-placeholder'); + expect(placeholderNode.height).toEqual(28); + expect(placeholderNode.width).toEqual(28); + expect(placeholderNode.label).toBeUndefined(); + expect(placeholderNode.isPlaceholder).toBe(true); + }); + + it('sets extra properties for placeholder node edges', () => { + const workflow = { + metadata: { name: 'testWorkflow' }, + status: { + nodes: { + root: { + children: ['runningNode'], + displayName: 'root', + id: 'root', + name: 'root', + phase: 'Succeeded', + type: 'Pod', + }, + runningNode: { + displayName: 'runningNode', + id: 'runningNode', + name: 'runningNode', + phase: 'Running', + type: 'Pod', + }, + }, + } + }; + const g = WorkflowParser.createRuntimeGraph(workflow as any); + + g.edges().map(edgeInfo => g.edge(edgeInfo)).forEach(edge => { + if (edge.isPlaceholder) { + expect(edge.color).toEqual(color.weak); + } else { + expect(edge.color).toBeUndefined(); + } + }); + }); it('deletes virtual nodes', () => { diff --git a/frontend/src/lib/WorkflowParser.tsx b/frontend/src/lib/WorkflowParser.ts similarity index 96% rename from frontend/src/lib/WorkflowParser.tsx rename to frontend/src/lib/WorkflowParser.ts index 43a2b6f73cf..519dbb21b28 100644 --- a/frontend/src/lib/WorkflowParser.tsx +++ b/frontend/src/lib/WorkflowParser.ts @@ -15,9 +15,8 @@ */ import * as dagre from 'dagre'; -import * as React from 'react'; +import IconWithTooltip from '../atoms/IconWithTooltip'; import MoreIcon from '@material-ui/icons/MoreHoriz'; -import Tooltip from '@material-ui/core/Tooltip'; import { Workflow, NodeStatus, Parameter } from '../../third_party/argo-ui/argo_template'; import { statusToIcon, NodePhase, hasFinished } from '../pages/Status'; import { color } from '../Css'; @@ -84,7 +83,13 @@ export default class WorkflowParser { if (!hasFinished(node.phase as NodePhase) && !this.isVirtual(node)) { g.setNode(node.id + runningNodeSuffix, { height: PLACEHOLDER_NODE_DIMENSION, - icon: this._placeholderNodeIcon(), + icon: IconWithTooltip({ + Icon: MoreIcon, + height: 24, + iconColor: color.weak, + tooltip: 'More nodes may appear here', + width: 24, + }), isPlaceholder: true, width: PLACEHOLDER_NODE_DIMENSION, }); @@ -262,14 +267,4 @@ export default class WorkflowParser { return ''; } } - - private static _placeholderNodeIcon(): JSX.Element { - return ( - - - - - - ); - } } diff --git a/frontend/src/pages/RunDetails.test.tsx b/frontend/src/pages/RunDetails.test.tsx index 5c4d264a6e1..828cb830847 100644 --- a/frontend/src/pages/RunDetails.test.tsx +++ b/frontend/src/pages/RunDetails.test.tsx @@ -668,6 +668,50 @@ describe('RunDetails', () => { expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined); }); + it('displays a spinner if graph is not defined and run has not finished', async () => { + const unfinishedRun = { + pipeline_runtime: { + // No graph + workflow_manifest: '{}', + }, + run: { + id: 'test-run-id', + name: 'test run', + // Run has not finished + status: 'Running', + }, + }; + getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); + + tree = shallow(); + await getRunSpy; + await TestUtils.flushPromises(); + + expect(tree).toMatchSnapshot(); + }); + + it('displays a message indicating there is no graph if graph is not defined and run has finished', async () => { + const unfinishedRun = { + pipeline_runtime: { + // No graph + workflow_manifest: '{}', + }, + run: { + id: 'test-run-id', + name: 'test run', + // Run has finished + status: 'Succeeded', + }, + }; + getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); + + tree = shallow(); + await getRunSpy; + await TestUtils.flushPromises(); + + expect(tree).toMatchSnapshot(); + }); + describe('auto refresh', () => { beforeEach(() => { testRun.run!.status = NodePhase.PENDING; diff --git a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap index 1a6d0f98459..6c2763ccd04 100644 --- a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap @@ -77,6 +77,83 @@ exports[`RunDetails closes side panel when close button is clicked 1`] = `
`; +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has finished 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has not finished 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+
+`; + exports[`RunDetails does not load logs if clicked node status is skipped 1`] = `
Date: Wed, 30 Jan 2019 12:57:47 -0800 Subject: [PATCH 6/6] Update copyright and add further tests --- frontend/src/atoms/IconWithTooltip.test.tsx | 2 +- frontend/src/atoms/IconWithTooltip.tsx | 2 +- frontend/src/pages/RunDetails.test.tsx | 74 +++---- .../__snapshots__/RunDetails.test.tsx.snap | 199 +++++++++++++++++- 4 files changed, 237 insertions(+), 40 deletions(-) diff --git a/frontend/src/atoms/IconWithTooltip.test.tsx b/frontend/src/atoms/IconWithTooltip.test.tsx index a6b79d76450..a59f595b1f8 100644 --- a/frontend/src/atoms/IconWithTooltip.test.tsx +++ b/frontend/src/atoms/IconWithTooltip.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC + * 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. diff --git a/frontend/src/atoms/IconWithTooltip.tsx b/frontend/src/atoms/IconWithTooltip.tsx index c8487a60f65..6c3b4c76106 100644 --- a/frontend/src/atoms/IconWithTooltip.tsx +++ b/frontend/src/atoms/IconWithTooltip.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google LLC + * 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. diff --git a/frontend/src/pages/RunDetails.test.tsx b/frontend/src/pages/RunDetails.test.tsx index 828cb830847..daa42a8f9e2 100644 --- a/frontend/src/pages/RunDetails.test.tsx +++ b/frontend/src/pages/RunDetails.test.tsx @@ -668,48 +668,50 @@ describe('RunDetails', () => { expect(tree.state('selectedNodeDetails')).toHaveProperty('phaseMessage', undefined); }); - it('displays a spinner if graph is not defined and run has not finished', async () => { - const unfinishedRun = { - pipeline_runtime: { - // No graph - workflow_manifest: '{}', - }, - run: { - id: 'test-run-id', - name: 'test run', - // Run has not finished - status: 'Running', - }, - }; - getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); + [NodePhase.RUNNING, NodePhase.PENDING, NodePhase.UNKNOWN].forEach(unfinishedStatus => { + it(`displays a spinner if graph is not defined and run has status: ${unfinishedStatus}`, async () => { + const unfinishedRun = { + pipeline_runtime: { + // No graph + workflow_manifest: '{}', + }, + run: { + id: 'test-run-id', + name: 'test run', + status: unfinishedStatus, + }, + }; + getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); - tree = shallow(); - await getRunSpy; - await TestUtils.flushPromises(); + tree = shallow(); + await getRunSpy; + await TestUtils.flushPromises(); - expect(tree).toMatchSnapshot(); + expect(tree).toMatchSnapshot(); + }); }); - it('displays a message indicating there is no graph if graph is not defined and run has finished', async () => { - const unfinishedRun = { - pipeline_runtime: { - // No graph - workflow_manifest: '{}', - }, - run: { - id: 'test-run-id', - name: 'test run', - // Run has finished - status: 'Succeeded', - }, - }; - getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); + [NodePhase.ERROR, NodePhase.FAILED, NodePhase.SUCCEEDED, NodePhase.SKIPPED].forEach(finishedStatus => { + it(`displays a message indicating there is no graph if graph is not defined and run has status: ${finishedStatus}`, async () => { + const unfinishedRun = { + pipeline_runtime: { + // No graph + workflow_manifest: '{}', + }, + run: { + id: 'test-run-id', + name: 'test run', + status: finishedStatus, + }, + }; + getRunSpy.mockImplementationOnce(() => Promise.resolve(unfinishedRun)); - tree = shallow(); - await getRunSpy; - await TestUtils.flushPromises(); + tree = shallow(); + await getRunSpy; + await TestUtils.flushPromises(); - expect(tree).toMatchSnapshot(); + expect(tree).toMatchSnapshot(); + }); }); describe('auto refresh', () => { diff --git a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap index 6c2763ccd04..3015dab3a15 100644 --- a/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap +++ b/frontend/src/pages/__snapshots__/RunDetails.test.tsx.snap @@ -77,7 +77,7 @@ exports[`RunDetails closes side panel when close button is clicked 1`] = `
`; -exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has finished 1`] = ` +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Error 1`] = `
@@ -118,7 +118,202 @@ exports[`RunDetails displays a message indicating there is no graph if graph is
`; -exports[`RunDetails displays a spinner if graph is not defined and run has not finished 1`] = ` +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Failed 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Skipped 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a message indicating there is no graph if graph is not defined and run has status: Succeeded 1`] = ` +
+
+ +
+
+
+ + No graph to show + +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has status: Pending 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has status: Running 1`] = ` +
+
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`RunDetails displays a spinner if graph is not defined and run has status: Unknown 1`] = `