Skip to content

Commit

Permalink
Graph ux feature add legend (#196)
Browse files Browse the repository at this point in the history
* Checkin and merge to master

Signed-off-by: Jason Porter <[email protected]>

* fixed one more

Signed-off-by: Jason Porter <[email protected]>
  • Loading branch information
jsonporter authored Sep 16, 2021
1 parent 950e080 commit f09f6af
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 167 deletions.
1 change: 0 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`));

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,4 @@
"resolutions": {
"micromatch": "^4.0.0"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({
makeWorkflowQuery(useQueryClient(), workflowId)
);
const nodeExecutionsById = React.useMemo(
() => keyBy(nodeExecutions, 'id.nodeId'),
() => keyBy(nodeExecutions, 'scopedId'),
[nodeExecutions]
);

Expand Down
26 changes: 25 additions & 1 deletion src/components/Executions/nodeExecutionQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -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;
},
Expand Down Expand Up @@ -295,7 +320,6 @@ async function fetchAllChildNodeExecutions(
);
return executions;
}

/**
*
* @param nodeExecutions list of parent node executionId's
Expand Down
93 changes: 48 additions & 45 deletions src/components/WorkflowGraph/transformerWorkflowToDAG.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import {
isEndNode,
isStartNode,
getThenNodeFromBranch,
getDisplayName,
getSubWorkflowFromId,
getNodeTypeFromCompiledNode,
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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);
};
Expand Down
4 changes: 0 additions & 4 deletions src/components/WorkflowGraph/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div style={containerStyle}>
<div style={colorStyle}></div>
<div>{text}</div>
</div>
);
};

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 (
<div style={legendContainerStyle}>
{Object.keys(nodePhaseColorMapping).map(phase => {
return (
<LegendItem
{...nodePhaseColorMapping[phase]}
key={`gl-${nodePhaseColorMapping[phase].text}`}
/>
);
})}
<LegendItem color={'#aaa'} text={'Nested'} />
</div>
);
};

return (
<div style={positionStyle}>
<div>
{isVisible ? renderLegend() : null}
<div style={buttonContainer}>
<Button
style={buttonStyle}
color="default"
id="graph-show-legend"
onClick={toggleVisibility}
variant="contained"
>
{isVisible ? 'Hide' : 'Show'} Legend
</Button>
</div>
</div>
</div>
);
};
10 changes: 8 additions & 2 deletions src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 <ReactFlowWrapper {...ReactFlowProps} />;
return (
<>
<Legend />
<ReactFlowWrapper {...ReactFlowProps} />
</>
);
};

export default ReactFlowGraphComponent;
10 changes: 6 additions & 4 deletions src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,22 @@ const LayoutRC: React.FC<LayoutRCProps> = ({
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);
}
Expand Down Expand Up @@ -98,7 +99,8 @@ export const ReactFlowWrapper: React.FC<RFWrapperProps> = ({
*/
useEffect(() => {
if (layedOut && reactFlowInstance) {
reactFlowInstance?.fitView({ padding: 0 });
reactFlowInstance?.fitView({ padding: 0.15 });
false;
}
}, [layedOut, reactFlowInstance]);
/**
Expand Down
Loading

0 comments on commit f09f6af

Please sign in to comment.