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

Graph ux feature add legend #196

Merged
merged 3 commits into from
Sep 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
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