Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve runtime graph starting and running experience #734

Merged
merged 6 commits into from
Jan 30, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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
  • Loading branch information
rileyjbauer committed Jan 25, 2019
commit 5ee8e7ad89a9898cd8c30c4c5c018cf9a08ac750
28 changes: 24 additions & 4 deletions frontend/src/components/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ interface Line {
}

interface Edge {
color?: string;
from: string;
to: string;
lines: Line[];
isPlaceholder?: boolean;
}

const css = stylesheet({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -153,15 +161,22 @@ export default class Graph extends React.Component<GraphProps> {
}
}
}
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 (
<div className={css.root}>
{graph.nodes().map(id => Object.assign(graph.node(id), { id })).map((node, i) => (
<div className={classes(css.node, 'graphNode',
<div className={classes(node.isPlaceholder ? css.placeholderNode : css.node, 'graphNode',
node.id === this.props.selectedNodeId ? css.nodeSelected : '')} key={i}
onClick={() => 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,
}}>
Expand All @@ -173,8 +188,13 @@ export default class Graph extends React.Component<GraphProps> {
{displayEdges.map((edge, i) => (
<div key={i}>
{edge.lines.map((line, l) => (
<div className={classes(css.line, l === edge.lines.length - 1 ? css.lastEdgeLine : '')}
<div className={classes(
css.line,
(l === edge.lines.length - 1 && !edge.isPlaceholder) ? css.lastEdgeLine : ''
)}
key={l} style={{
borderTopColor: edge.color,
borderTopStyle: edge.isPlaceholder ? 'dotted' : 'solid',
left: line.left,
top: line.yMid,
transform: `translate(100px, 44px) rotate(${line.angle}deg)`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
*/

import * as dagre from 'dagre';
import * as React from 'react';
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 } from '../pages/Status';
import { statusToIcon, NodePhase, hasFinished } from '../pages/Status';
import { color } from '../Css';

export enum StorageService {
GCS = 'gcs',
Expand All @@ -37,6 +41,7 @@ export default class WorkflowParser {

const NODE_WIDTH = 180;
const NODE_HEIGHT = 70;
const PLACEHOLDER_NODE_DIMENSION = 28;

if (!workflow || !workflow.status || !workflow.status.nodes ||
!workflow.metadata || !workflow.metadata.name) {
Expand All @@ -50,8 +55,7 @@ export default class WorkflowParser {
// Uses the root node, so this needs to happen before we remove the root
// node below.
const onExitHandlerNodeId =
Object.keys(workflowNodes).find((id) =>
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));
Expand All @@ -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.
Expand Down Expand Up @@ -247,4 +262,14 @@ export default class WorkflowParser {
return '';
}
}

private static _placeholderNodeIcon(): JSX.Element {
return (
<Tooltip title='More nodes may appear here as execution progresses'>
<span style={{ height: 18 }}>
<MoreIcon style={{ color: color.weak, height: 18, width: 18 }} />
</span>
</Tooltip>
);
}
}
50 changes: 43 additions & 7 deletions frontend/src/pages/RunDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<RunDetailsProps, RunDetailsState> {
private _onBlur: EventListener;
private _onFocus: EventListener;
Expand Down Expand Up @@ -127,7 +144,7 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
}

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 : '';

Expand All @@ -142,7 +159,7 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
onSwitch={(tab: number) => this.setStateSafe({ selectedTab: tab })} />
<div className={commonCss.page}>

{selectedTab === 0 && <div className={commonCss.page}>
{selectedTab === 0 && <div className={commonCss.page} style={{ backgroundColor: color.graphBg }}>
{graph && <div className={commonCss.page} style={{ position: 'relative', overflow: 'hidden' }}>
<Graph graph={graph} selectedNodeId={selectedNodeId}
onClick={(id) => this._selectNode(id)} />
Expand All @@ -151,8 +168,7 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
onClose={() => this.setStateSafe({ selectedNodeDetails: null })} title={selectedNodeId}>
{!!selectedNodeDetails && (<React.Fragment>
{!!selectedNodeDetails.phaseMessage && (
<Banner mode='warning'
message={selectedNodeDetails.phaseMessage} />
<Banner mode='warning' message={selectedNodeDetails.phaseMessage} />
)}
<div className={commonCss.page}>
<MD2Tabs tabs={['Artifacts', 'Input/Output', 'Logs']}
Expand Down Expand Up @@ -208,8 +224,28 @@ class RunDetails extends Page<RunDetailsProps, RunDetailsState> {
</div>
</React.Fragment>)}
</SidePanel>

<div className={css.footer}>
<div className={commonCss.flex}>
<InfoIcon style={{ color: color.lowContrast, height: 16, width: 16 }} />
<span className={css.infoSpan}>
Runtime execution graph. Only steps that are currently running or have already completed are shown
</span>
</div>
</div>
</div>}
{!graph && <span style={{ margin: '40px auto' }}>No graph to show</span>}
{!graph && (
<div>
{runFinished && (
<span style={{ margin: '40px auto' }}>
No graph to show
</span>
)}
{!runFinished && (
<CircularProgress size={30} className={commonCss.absoluteCenter} />
)}
</div>
)}
</div>}

{selectedTab === 1 && (
Expand Down