From f09f6af664981544783f9253b87763050803b6d6 Mon Sep 17 00:00:00 2001 From: Jason Porter <84735036+jsonporter@users.noreply.github.com> Date: Thu, 16 Sep 2021 10:27:29 -0700 Subject: [PATCH] Graph ux feature add legend (#196) * Checkin and merge to master Signed-off-by: Jason Porter * fixed one more Signed-off-by: Jason Porter --- index.js | 1 - package.json | 2 +- .../ExecutionWorkflowGraph.tsx | 2 +- .../Executions/nodeExecutionQueries.ts | 26 ++- .../transformerWorkflowToDAG.tsx | 93 +++++----- src/components/WorkflowGraph/utils.ts | 4 - .../flytegraph/ReactFlow/NodeStatusLegend.tsx | 106 +++++++++++ .../ReactFlow/ReactFlowGraphComponent.tsx | 10 +- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 10 +- .../ReactFlow/customNodeComponents.tsx | 56 +++--- .../ReactFlow/transformerDAGToReactFlow.tsx | 26 ++- src/components/flytegraph/ReactFlow/utils.tsx | 169 +++++++++++------- src/models/Execution/types.ts | 1 + yarn.lock | 5 - 14 files changed, 344 insertions(+), 167 deletions(-) create mode 100644 src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx diff --git a/index.js b/index.js index 8177979b2..6b645f4bc 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,6 @@ const app = express(); // Enable logging for HTTP access app.use(morgan('combined')); app.use(express.json()); - app.get(`${env.BASE_URL}/healthz`, (_req, res) => res.status(200).send()); app.use(corsProxy(`${env.BASE_URL}${env.CORS_PROXY_PREFIX}`)); diff --git a/package.json b/package.json index 187b85fbc..430584d5e 100644 --- a/package.json +++ b/package.json @@ -211,4 +211,4 @@ "resolutions": { "micromatch": "^4.0.0" } -} +} \ No newline at end of file diff --git a/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx b/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx index b3fc87e4c..350a21ab1 100644 --- a/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx +++ b/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx @@ -26,7 +26,7 @@ export const ExecutionWorkflowGraph: React.FC = ({ makeWorkflowQuery(useQueryClient(), workflowId) ); const nodeExecutionsById = React.useMemo( - () => keyBy(nodeExecutions, 'id.nodeId'), + () => keyBy(nodeExecutions, 'scopedId'), [nodeExecutions] ); diff --git a/src/components/Executions/nodeExecutionQueries.ts b/src/components/Executions/nodeExecutionQueries.ts index 3aff92ba9..0cb4e5df5 100644 --- a/src/components/Executions/nodeExecutionQueries.ts +++ b/src/components/Executions/nodeExecutionQueries.ts @@ -78,6 +78,13 @@ export function makeNodeExecutionListQuery( const nodeExecutions = removeSystemNodes( (await listNodeExecutions(id, config)).entities ); + nodeExecutions.map(exe => { + if (exe.metadata) { + return (exe.scopedId = exe.metadata.specNodeId); + } else { + return (exe.scopedId = exe.id.nodeId); + } + }); cacheNodeExecutions(queryClient, nodeExecutions); return nodeExecutions; } @@ -226,6 +233,11 @@ async function fetchGroupsForParentNodeExecution( } }; + /** @TODO there is likely a better way to do this; eg, in a previous call */ + if (!nodeExecution.scopedId) { + nodeExecution.scopedId = nodeExecution.metadata?.specNodeId; + } + const children = await fetchNodeExecutionList( queryClient, nodeExecution.id.executionId, @@ -239,6 +251,19 @@ async function fetchGroupsForParentNodeExecution( group = { name: retryAttempt, nodeExecutions: [] }; out.set(retryAttempt, group); } + /** + * GraphUX uses workflowClosure which uses scopedId + * This builds a scopedId via parent nodeExecution + * to enable mapping between graph and other components + */ + let scopedId: string | undefined = + nodeExecution.metadata?.specNodeId; + if (scopedId != undefined) { + scopedId += `-${child.metadata?.retryGroup}-${child.metadata?.specNodeId}`; + child['scopedId'] = scopedId; + } else { + child['scopedId'] = child.metadata?.specNodeId; + } group.nodeExecutions.push(child); return out; }, @@ -295,7 +320,6 @@ async function fetchAllChildNodeExecutions( ); return executions; } - /** * * @param nodeExecutions list of parent node executionId's diff --git a/src/components/WorkflowGraph/transformerWorkflowToDAG.tsx b/src/components/WorkflowGraph/transformerWorkflowToDAG.tsx index dcc7e067d..bacd5d99d 100644 --- a/src/components/WorkflowGraph/transformerWorkflowToDAG.tsx +++ b/src/components/WorkflowGraph/transformerWorkflowToDAG.tsx @@ -14,7 +14,6 @@ import { import { isEndNode, isStartNode, - getThenNodeFromBranch, getDisplayName, getSubWorkflowFromId, getNodeTypeFromCompiledNode, @@ -115,6 +114,19 @@ export const buildBranchStartEndNodes = (root: dNode) => { }; }; +export const buildBranchNodeWidthType = (node, root, workflow) => { + const taskNode = node.taskNode as TaskNode; + let taskType: CompiledTask | null = null; + if (taskNode) { + taskType = getTaskTypeFromCompiledNode( + taskNode, + workflow.tasks + ) as CompiledTask; + } + const dNode = createDNode(node as CompiledNode, root, taskType); + root.nodes.push(dNode); +}; + /** * Will parse values when dealing with a Branch and recursively find and build * any other node types. @@ -127,53 +139,46 @@ export const parseBranch = ( parentCompiledNode: CompiledNode, workflow: CompiledWorkflowClosure ) => { - const thenNodeCompiledNode = getThenNodeFromBranch(parentCompiledNode); - const thenNodeDNode = createDNode(thenNodeCompiledNode, root); - const { startNode, endNode } = buildBranchStartEndNodes(root); + const otherNode = parentCompiledNode.branchNode?.ifElse?.other; + const thenNode = parentCompiledNode.branchNode?.ifElse?.case + ?.thenNode as CompiledNode; + const elseNode = parentCompiledNode.branchNode?.ifElse + ?.elseNode as CompiledNode; - /* We must push container node regardless */ - root.nodes.push(thenNodeDNode); - - if (thenNodeCompiledNode.branchNode) { - buildDAG(thenNodeDNode, thenNodeCompiledNode, dTypes.branch, workflow); + /* Check: if thenNode has branch : else add theNode */ + if (thenNode.branchNode) { + const thenNodeDNode = createDNode(thenNode, root); + buildDAG(thenNodeDNode, thenNode, dTypes.branch, workflow); + root.nodes.push(thenNodeDNode); } else { - /* Find any 'other' (other means 'else', 'else if') nodes */ - const otherArr = parentCompiledNode.branchNode?.ifElse?.other; + buildBranchNodeWidthType(thenNode, root, workflow); + } - if (otherArr) { - otherArr.map(otherItem => { - const otherCompiledNode: CompiledNode = otherItem.thenNode as CompiledNode; - if (otherCompiledNode.branchNode) { - const otherDNodeBranch = createDNode( - otherCompiledNode, - root - ); - buildDAG( - otherDNodeBranch, - otherCompiledNode, - dTypes.branch, - workflow - ); - } else { - const taskNode = otherCompiledNode.taskNode as TaskNode; - let taskType: CompiledTask | null = null; - if (taskNode) { - taskType = getTaskTypeFromCompiledNode( - taskNode, - workflow.tasks - ) as CompiledTask; - } - const otherDNode = createDNode( - otherCompiledNode, - root, - taskType - ); - root.nodes.push(otherDNode); - } - }); - } + /* Check: else case */ + if (elseNode) { + buildBranchNodeWidthType(elseNode, root, workflow); } + /* Check: other case */ + if (otherNode) { + otherNode.map(otherItem => { + const otherCompiledNode: CompiledNode = otherItem.thenNode as CompiledNode; + if (otherCompiledNode.branchNode) { + const otherDNodeBranch = createDNode(otherCompiledNode, root); + buildDAG( + otherDNodeBranch, + otherCompiledNode, + dTypes.branch, + workflow + ); + } else { + buildBranchNodeWidthType(otherCompiledNode, root, workflow); + } + }); + } + + /* Add edges and add start/end nodes */ + const { startNode, endNode } = buildBranchStartEndNodes(root); for (let i = 0; i < root.nodes.length; i++) { const startEdge: dEdge = { sourceId: startNode.id, @@ -186,8 +191,6 @@ export const parseBranch = ( root.edges.push(startEdge); root.edges.push(endEdge); } - - /* Add back to root */ root.nodes.push(startNode); root.nodes.push(endNode); }; diff --git a/src/components/WorkflowGraph/utils.ts b/src/components/WorkflowGraph/utils.ts index 5e57a659a..280123752 100644 --- a/src/components/WorkflowGraph/utils.ts +++ b/src/components/WorkflowGraph/utils.ts @@ -67,10 +67,6 @@ export const getDisplayName = (context: any): string => { } }; -export const getThenNodeFromBranch = (node: CompiledNode) => { - return node.branchNode?.ifElse?.case?.thenNode as CompiledNode; -}; - /** * Returns the id for CompiledWorkflow * @param context will find id for this entity diff --git a/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx b/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx new file mode 100644 index 000000000..2ea851c5f --- /dev/null +++ b/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx @@ -0,0 +1,106 @@ +import * as React from 'react'; +import { useState, CSSProperties } from 'react'; +import { Button } from '@material-ui/core'; +import { nodePhaseColorMapping } from './utils'; + +export const LegendItem = ({ color, text }) => { + /** + * @TODO temporary check for nested graph until + * nested functionality is deployed + */ + const isNested = text == 'Nested'; + + const containerStyle: CSSProperties = { + display: 'flex', + flexDirection: 'row', + width: '100%', + padding: '.5rem 0' + }; + const colorStyle: CSSProperties = { + width: '28px', + height: '22px', + background: isNested ? color : 'none', + border: `3px solid ${color}`, + borderRadius: '4px', + paddingRight: '10px', + marginRight: '1rem' + }; + return ( +
+
+
{text}
+
+ ); +}; + +export const Legend = () => { + const [isVisible, setIsVisible] = useState(true); + + const positionStyle: CSSProperties = { + bottom: '1rem', + right: '1rem', + zIndex: 10000, + position: 'absolute', + width: '150px' + }; + + const buttonContainer: CSSProperties = { + width: '100%', + display: 'flex', + justifyContent: 'center' + }; + + const buttonStyle: CSSProperties = { + color: '#555', + width: '100%' + }; + + const toggleVisibility = () => { + setIsVisible(!isVisible); + }; + + const renderLegend = () => { + const legendContainerStyle: CSSProperties = { + width: '100%', + padding: '1rem', + background: 'rgba(255,255,255,1)', + border: `1px solid #ddd`, + borderRadius: '4px', + boxShadow: '2px 4px 10px rgba(50,50,50,.2)', + marginBottom: '1rem' + }; + + return ( +
+ {Object.keys(nodePhaseColorMapping).map(phase => { + return ( + + ); + })} + +
+ ); + }; + + return ( +
+
+ {isVisible ? renderLegend() : null} +
+ +
+
+
+ ); +}; diff --git a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 36148a2ae..703584842 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { RFWrapperProps, RFGraphTypes } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; +import { Legend } from './NodeStatusLegend'; /** * Renders workflow graph using React Flow. @@ -17,13 +18,18 @@ const ReactFlowGraphComponent = props => { onNodeSelectionChanged ); - const backgroundStyle = getRFBackground(data.nodeExecutionStatus).nested; + const backgroundStyle = getRFBackground().nested; const ReactFlowProps: RFWrapperProps = { backgroundStyle, rfGraphJson: rfGraphJson, type: RFGraphTypes.main }; - return ; + return ( + <> + + + + ); }; export default ReactFlowGraphComponent; diff --git a/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index 247c2dd7e..f5c91841c 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -41,21 +41,22 @@ const LayoutRC: React.FC = ({ setElements, setLayout }: LayoutRCProps) => { - const [computeLayout, setComputeLayout] = useState(true); - /* strore is only populated onLoad for each flow */ const nodes = useStoreState(store => store.nodes); const edges = useStoreState(store => store.edges); + const [computeLayout, setComputeLayout] = useState(true); + if (nodes.length > 0 && computeLayout) { if (nodes[0].__rf.width) { setComputeLayout(false); } } + useEffect(() => { if (!computeLayout) { const nodesAndEdges = (nodes as any[]).concat(edges); - const graph = setReactFlowGraphLayout(nodesAndEdges, 'LR'); + const { graph } = setReactFlowGraphLayout(nodesAndEdges, 'LR'); setElements(graph); setLayout(true); } @@ -98,7 +99,8 @@ export const ReactFlowWrapper: React.FC = ({ */ useEffect(() => { if (layedOut && reactFlowInstance) { - reactFlowInstance?.fitView({ padding: 0 }); + reactFlowInstance?.fitView({ padding: 0.15 }); + false; } }, [layedOut, reactFlowInstance]); /** diff --git a/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index d72104831..e4706b983 100644 --- a/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -8,12 +8,14 @@ import { } from 'react-flow-renderer'; import { dTypes } from 'models/Graph/types'; import { ReactFlowWrapper } from './ReactFlowWrapper'; -import { +import setReactFlowGraphLayout, { COLOR_TASK_TYPE, COLOR_GRAPH_BACKGROUND, getGraphHandleStyle, getGraphNodeStyle, - getRFBackground + getRFBackground, + getNestedContainerStyle, + getNestedGraphContainerStyle } from './utils'; import { RFGraphTypes, RFHandleProps } from './types'; @@ -244,13 +246,10 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { */ export const ReactFlowCustomSubworkflowNode = ({ data }: any) => { const { dag } = data; - const backgroundStyle = getRFBackground(data.nodeExecutionStatus).nested; - - const rfContainerStyle: React.CSSProperties = { - width: `300px`, - height: `200px` - }; - + const backgroundStyle = getRFBackground().nested; + const borderStyle = getNestedContainerStyle(data.nodeExecutionStatus); + const { estimatedDimensions } = setReactFlowGraphLayout(dag, 'LR', true); + const graphContainer = getNestedGraphContainerStyle(estimatedDimensions); return ( <> {renderDefaultHandles( @@ -258,12 +257,14 @@ export const ReactFlowCustomSubworkflowNode = ({ data }: any) => { getGraphHandleStyle('source'), getGraphHandleStyle('target') )} -
- +
+
+ +
); @@ -276,12 +277,11 @@ export const ReactFlowCustomSubworkflowNode = ({ data }: any) => { */ export const ReactFlowCustomBranchNode = ({ data }: any) => { const { dag } = data; - const backgroundStyle = getRFBackground(data.nodeExecutionStatus).nested; - - const rfContainerStyle: React.CSSProperties = { - width: `300px`, - height: `200px` - }; + console.log('@ReactFlowCustomBranchNode: data'); + const backgroundStyle = getRFBackground().nested; + const borderStyle = getNestedContainerStyle(data.nodeExecutionStatus); + const { estimatedDimensions } = setReactFlowGraphLayout(dag, 'LR', true); + const graphContainer = getNestedGraphContainerStyle(estimatedDimensions); return ( <> @@ -290,12 +290,14 @@ export const ReactFlowCustomBranchNode = ({ data }: any) => { getGraphHandleStyle('source'), getGraphHandleStyle('target') )} -
- +
+
+ +
); diff --git a/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx b/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx index b7f45a388..08afc5a3d 100644 --- a/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx +++ b/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx @@ -1,9 +1,5 @@ import { dEdge, dNode, dTypes } from 'models/Graph/types'; -import { - DISPLAY_NAME_START, - DISPLAY_NAME_END -} from 'components/WorkflowGraph/utils'; -import { MAX_RENDER_DEPTH, ReactFlowGraphConfig } from './utils'; +import { MAX_NESTED_DEPTH, ReactFlowGraphConfig } from './utils'; import { Edge, Elements, Node, Position } from 'react-flow-renderer'; import { NodeExecutionsById } from 'models/Execution/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; @@ -34,12 +30,14 @@ export const buildReactFlowNode = ( const taskType = dNode?.value?.template ? dNode.value.template.type : null; /** - * @TODO decide which to display after demo + * @TODO Implement a toggle that will allow users to view either the display + * name or the nodeId. */ - const displayName = - dNode.name == DISPLAY_NAME_START || dNode.name == DISPLAY_NAME_END - ? dNode.name - : dNode.scopedId; + // const displayName = + // dNode.name == DISPLAY_NAME_START || dNode.name == DISPLAY_NAME_END + // ? dNode.name + // : dNode.scopedId; + const displayName = dNode.name; const mapNodeExecutionStatus = () => { if (nodeExecutionsById[dNode.scopedId]) { @@ -83,16 +81,16 @@ export const nodeMapToArr = map => { export const dagToReactFlow = ( dag: dNode, nodeExecutionsById: NodeExecutionsById, - currentDepth = 0, + nestedDepth = 0, onNodeSelectionChanged ) => { const nodes: any = {}; const edges: any = {}; dag.nodes?.map(dNode => { - if (dNode.nodes?.length > 0 && currentDepth <= MAX_RENDER_DEPTH) { + if (dNode.nodes?.length > 0 && nestedDepth <= MAX_NESTED_DEPTH) { /* Note: currentDepth will be replaced once nested toggle */ - if (currentDepth == MAX_RENDER_DEPTH) { + if (nestedDepth == MAX_NESTED_DEPTH) { nodes[dNode.id] = buildReactFlowNode( dNode, [], @@ -106,7 +104,7 @@ export const dagToReactFlow = ( dagToReactFlow( dNode, nodeExecutionsById, - currentDepth + 1, + nestedDepth + 1, onNodeSelectionChanged ), nodeExecutionsById, diff --git a/src/components/flytegraph/ReactFlow/utils.tsx b/src/components/flytegraph/ReactFlow/utils.tsx index efea7de06..27f420860 100644 --- a/src/components/flytegraph/ReactFlow/utils.tsx +++ b/src/components/flytegraph/ReactFlow/utils.tsx @@ -13,7 +13,7 @@ export const COLOR_GRAPH_BACKGROUND = '#666666'; export const DISPLAY_NAME_START = 'start'; export const DISPLAY_NAME_END = 'end'; -export const MAX_RENDER_DEPTH = 1; +export const MAX_NESTED_DEPTH = 1; export const HANDLE_ICON = require('assets/SmallArrow.svg') as string; export const ReactFlowGraphConfig = { @@ -79,6 +79,16 @@ export const getGraphHandleStyle = ( } }; +export const nodePhaseColorMapping = { + [NodeExecutionPhase.FAILED]: { color: '#e90000', text: 'Failed' }, + [NodeExecutionPhase.FAILING]: { color: '#f2a4ad', text: 'Failing' }, + [NodeExecutionPhase.SUCCEEDED]: { color: '#37b789', text: 'Succeded' }, + [NodeExecutionPhase.ABORTED]: { color: '#be25d7', text: 'Aborted' }, + [NodeExecutionPhase.RUNNING]: { color: '#2892f4', text: 'Running' }, + [NodeExecutionPhase.QUEUED]: { color: '#dfd71b', text: 'Queued' }, + [NodeExecutionPhase.UNDEFINED]: { color: '#4a2839', text: 'Undefined' } +}; + /** * Maps node execution phases to UX colors * @param nodeExecutionStatus @@ -87,33 +97,45 @@ export const getGraphHandleStyle = ( export const getStatusColor = ( nodeExecutionStatus: NodeExecutionPhase ): string => { - let nodePrimaryColor = ''; - switch (nodeExecutionStatus) { - case NodeExecutionPhase.FAILED: - nodePrimaryColor = '#f2a4ad'; - break; - case NodeExecutionPhase.FAILING: - nodePrimaryColor = '#f2a4ad'; - break; - case NodeExecutionPhase.SUCCEEDED: - nodePrimaryColor = '#37b789'; - break; - case NodeExecutionPhase.ABORTED: - nodePrimaryColor = '#be25d7'; - break; - case NodeExecutionPhase.RUNNING: - nodePrimaryColor = '#2892f4'; - break; - case NodeExecutionPhase.QUEUED: - nodePrimaryColor = '#dfd71b'; - break; - case NodeExecutionPhase.UNDEFINED: - nodePrimaryColor = '#4a2839'; - break; - default: - nodePrimaryColor = '#c6c6c6'; + if (nodePhaseColorMapping[nodeExecutionStatus]) { + return nodePhaseColorMapping[nodeExecutionStatus].color; + } else { + /** @TODO decide what we want default color to be */ + return '#c6c6c6'; } - return nodePrimaryColor; +}; + +export const getNestedGraphContainerStyle = overwrite => { + let width = overwrite.width; + let height = overwrite.height; + + const maxHeight = 500; + const minHeight = 200; + const maxWidth = 700; + const minWidth = 300; + + if (overwrite) { + width = width > maxWidth ? maxWidth : width; + width = width < minWidth ? minWidth : width; + height = height > maxHeight ? maxHeight : height; + height = height < minHeight ? minHeight : height; + } + + const output: React.CSSProperties = { + width: `${width}px`, + height: `${height}px` + }; + + return output; +}; + +export const getNestedContainerStyle = nodeExecutionStatus => { + const style = { + border: `1px dashed ${getStatusColor(nodeExecutionStatus)}`, + borderRadius: '8px', + background: 'rgba(255,255,255,.9)' + } as React.CSSProperties; + return style; }; export const getGraphNodeStyle = ( @@ -199,7 +221,7 @@ export const getGraphNodeStyle = ( return output; }; -export const getRFBackground = (nodeExecutionStatus: NodeExecutionPhase) => { +export const getRFBackground = () => { return { main: { background: { @@ -210,13 +232,6 @@ export const getRFBackground = (nodeExecutionStatus: NodeExecutionPhase) => { gridSpacing: 20 } as RFBackgroundProps, nested: { - background: { - border: `1px dashed ${getStatusColor(nodeExecutionStatus)}`, - borderRadius: '8px', - background: 'rgba(255,255,255,1)', - padding: 0, - margin: 0 - }, gridColor: 'none', gridSpacing: 1 } as RFBackgroundProps @@ -232,16 +247,20 @@ export const getRFBackground = (nodeExecutionStatus: NodeExecutionPhase) => { */ export const setReactFlowGraphLayout = ( elements: Elements, - direction: string + direction: string, + estimate = false ) => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); const isHorizontal = direction === 'LR'; + const ESTIMATE_HEIGHT = 25; + const ESTIMATE_WIDTH_FACTOR = 6; + dagreGraph.setGraph({ rankdir: direction, - edgesep: 20, - nodesep: 40, + edgesep: 60, + nodesep: 30, ranker: 'longest-path', acyclicer: 'greedy' }); @@ -251,8 +270,10 @@ export const setReactFlowGraphLayout = ( */ elements.forEach(el => { if (isNode(el)) { - const nodeWidth = el.__rf.width; - const nodeHeight = el.__rf.height; + const nodeWidth = estimate + ? el.data.text.length * ESTIMATE_WIDTH_FACTOR + : el.__rf.width; + const nodeHeight = estimate ? ESTIMATE_HEIGHT : el.__rf.height; dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight }); } else { dagreGraph.setEdge(el.source, el.target); @@ -260,29 +281,53 @@ export const setReactFlowGraphLayout = ( }); dagre.layout(dagreGraph); + const graphWidth = dagreGraph.graph().width; + const graphHeight = dagreGraph.graph().height; + if (estimate) { + return { + estimatedDimensions: { + width: graphWidth, + height: graphHeight + } + }; + } else { + return { + graph: elements.map(el => { + if (isNode(el)) { + el.targetPosition = isHorizontal + ? Position.Left + : Position.Top; + el.sourcePosition = isHorizontal + ? Position.Right + : Position.Bottom; + const nodeWidth = estimate + ? el.data.text.length * ESTIMATE_WIDTH_FACTOR + : el.__rf.width; + const nodeHeight = estimate + ? ESTIMATE_HEIGHT + : el.__rf.height; + const nodeWithPosition = dagreGraph.node(el.id); - return elements.map(el => { - if (isNode(el)) { - el.targetPosition = isHorizontal ? Position.Left : Position.Top; - el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; - const nodeWidth = el.__rf.width; - const nodeHeight = el.__rf.height; - const nodeWithPosition = dagreGraph.node(el.id); - - /** Keep both position and .__rf.position in sync */ - const x = nodeWithPosition.x - nodeWidth / 2; - const y = nodeWithPosition.y - nodeHeight / 2; - el.position = { - x: x, - y: y - }; - el.__rf.position = { - x: x, - y: y - }; - } - return el; - }); + /** Keep both position and .__rf.position in sync */ + const x = nodeWithPosition.x - nodeWidth / 2; + const y = nodeWithPosition.y - nodeHeight / 2; + el.position = { + x: x, + y: y + }; + el.__rf.position = { + x: x, + y: y + }; + } + return el; + }), + dimensions: { + width: graphWidth, + height: graphHeight + } + }; + } }; export default setReactFlowGraphLayout; diff --git a/src/models/Execution/types.ts b/src/models/Execution/types.ts index b99ea3790..062f40c41 100644 --- a/src/models/Execution/types.ts +++ b/src/models/Execution/types.ts @@ -89,6 +89,7 @@ export interface NodeExecution extends Admin.INodeExecution { inputUri: string; closure: NodeExecutionClosure; metadata?: NodeExecutionMetadata; + scopedId?: string; } export interface NodeExecutionsById { diff --git a/yarn.lock b/yarn.lock index 7b4b40e06..1bb3b89d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13403,11 +13403,6 @@ nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nanoid@^3.1.23: - version "3.1.25" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" - integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== - napi-build-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"