From b5f3191901d5f7e763047fd6421d642c8edeb2b2 Mon Sep 17 00:00:00 2001 From: Simon Behar Date: Mon, 14 Sep 2020 08:44:20 -0700 Subject: [PATCH] feat(ui): Introduce modal DAG renderer. Fixes: #3595 (#3967) --- .github/workflows/ci-build.yaml | 2 + ui/package.json | 2 + .../workflow-dag-render-options-panel.tsx | 11 ++ .../components/workflow-dag/workflow-dag.scss | 7 + .../components/workflow-dag/workflow-dag.tsx | 100 +++++++++++--- .../workflow-details/workflow-details.scss | 130 +++++++++--------- ui/yarn.lock | 20 +++ 7 files changed, 189 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index fe24615bfce1..6b020b47f94a 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -147,6 +147,8 @@ jobs: path: ui/node_modules key: ${{ runner.os }}-node-dep-v1-${{ hashFiles('**/yarn.lock') }} - name: Install, build and Lint + env: + NODE_OPTIONS: --max-old-space-size=4096 run: | yarn --cwd ui install yarn --cwd ui build diff --git a/ui/package.json b/ui/package.json index 5bd728bbfea8..3e4cc0227e26 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,9 +13,11 @@ "dependencies": { "@babel/core": "^7.0.0-0", "@fortawesome/fontawesome-free": "^5.12.0", + "@types/dagre": "^0.7.44", "argo-ui": "https://github.com/argoproj/argo-ui.git", "classnames": "^2.2.5", "cron-parser": "^2.16.3", + "dagre": "^0.8.5", "formik": "^2.1.2", "history": "^4.7.2", "js-yaml": "^3.13.1", diff --git a/ui/src/app/workflows/components/workflow-dag/workflow-dag-render-options-panel.tsx b/ui/src/app/workflows/components/workflow-dag/workflow-dag-render-options-panel.tsx index 9f7ce79eb759..7fb1984668f5 100644 --- a/ui/src/app/workflows/components/workflow-dag/workflow-dag-render-options-panel.tsx +++ b/ui/src/app/workflows/components/workflow-dag/workflow-dag-render-options-panel.tsx @@ -125,6 +125,17 @@ export class WorkflowDagRenderOptionsPanel extends React.Component + + this.props.onChange({ + ...this.workflowDagRenderOptions, + fastRenderer: !this.props.fastRenderer + }) + } + title='Use a faster, but less pretty, renderer to display the workflow'> + + ); } diff --git a/ui/src/app/workflows/components/workflow-dag/workflow-dag.scss b/ui/src/app/workflows/components/workflow-dag/workflow-dag.scss index 35ba7585ce59..fc50339c50fb 100644 --- a/ui/src/app/workflows/components/workflow-dag/workflow-dag.scss +++ b/ui/src/app/workflows/components/workflow-dag/workflow-dag.scss @@ -13,6 +13,13 @@ a { padding: 10px; color: $argo-color-gray-6; + + &.active { + background-color: $argo-color-gray-3; + border: 1px solid $argo-color-gray-4; + border-radius: 5px; + cursor: default; + } } } diff --git a/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx b/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx index 116ee1ffa6a7..145aafe9f8bc 100644 --- a/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx +++ b/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx @@ -1,4 +1,5 @@ import * as classNames from 'classnames'; +import * as dagre from 'dagre'; import * as React from 'react'; import {NODE_PHASE, NodePhase, NodeStatus} from '../../../../models'; @@ -14,6 +15,7 @@ export interface WorkflowDagRenderOptions { scale: number; nodesToDisplay: string[]; expandNodes: Set; + fastRenderer: boolean; } export interface WorkflowDagProps { @@ -281,7 +283,8 @@ export class WorkflowDag extends React.Component ({})); + + nodes.forEach(node => { + graph.setNode(node, {label: node, width: this.nodeSize, height: this.nodeSize}); + }); + edges.forEach(edge => { + if (edge.v && edge.w) { + graph.setEdge(edge.v, edge.w); + } + }); + dagre.layout(graph); + + const size = this.getGraphSize(graph.nodes().map((id: string) => graph.node(id))); + this.graph = { + width: size.width, + height: size.height, + nodes: new Map(), + edges: [] + }; + + graph.nodes().map((id: string) => { + const node = graph.node(id); + this.graph.nodes.set(node.label, {x: node.x, y: node.y}); + }); + graph.edges().map((edge: Edge) => { + this.graph.edges.push(this.generateEdge(edge)); + }); + } + + private getGraphSize(nodes: dagre.Node[]): {width: number; height: number} { + let width = 0; + let height = 0; + nodes.forEach(node => { + width = Math.max(node.x + node.width / 2, width); + height = Math.max(node.y + node.height / 2, height); + }); + return {width, height}; + } + + private layoutGraphFast(nodes: string[], edges: Edge[]) { const hash = {scale: this.scale, nodeCount: nodes.length, nodesToDisplay: this.state.nodesToDisplay}; // this hash check prevents having to do the expensive layout operation, if the graph does not re-laying out (e.g. phase change only) if (this.hash === hash) { @@ -433,26 +485,28 @@ export class WorkflowDag extends React.Component this.graph.nodes.has(e.v) && this.graph.nodes.has(e.w)).map(e => this.generateEdge(e)); + } + + private generateEdge(edge: Edge) { // `h` and `v` move the arrow heads to next to the node, otherwise they would be behind it const h = this.state.horizontal ? this.nodeSize / 2 : 0; const v = !this.state.horizontal ? this.nodeSize / 2 : 0; - this.graph.edges = edges - .filter(e => this.graph.nodes.has(e.v) && this.graph.nodes.has(e.w)) - .map(e => ({ - v: e.v, - w: e.w, - points: [ - { - // for hidden nodes, we want to size them zero - x: this.graph.nodes.get(e.v).x + (this.hiddenNode(e.v) ? 0 : h), - y: this.graph.nodes.get(e.v).y + (this.hiddenNode(e.v) ? 0 : v) - }, - { - x: this.graph.nodes.get(e.w).x - (this.hiddenNode(e.w) ? 0 : h), - y: this.graph.nodes.get(e.w).y - (this.hiddenNode(e.w) ? 0 : v) - } - ] - })); + return { + v: edge.v, + w: edge.w, + points: [ + { + // for hidden nodes, we want to size them zero + x: this.graph.nodes.get(edge.v).x + (this.hiddenNode(edge.v) ? 0 : h), + y: this.graph.nodes.get(edge.v).y + (this.hiddenNode(edge.v) ? 0 : v) + }, + { + x: this.graph.nodes.get(edge.w).x - (this.hiddenNode(edge.w) ? 0 : h), + y: this.graph.nodes.get(edge.w).y - (this.hiddenNode(edge.w) ? 0 : v) + } + ] + }; } private selectNode(nodeId: string) { @@ -512,4 +566,12 @@ export class WorkflowDag extends React.Component