diff --git a/src/components/Entities/EntityDetails.tsx b/src/components/Entities/EntityDetails.tsx index 89262beb65..03651b942c 100644 --- a/src/components/Entities/EntityDetails.tsx +++ b/src/components/Entities/EntityDetails.tsx @@ -13,6 +13,8 @@ import { EntityExecutions } from './EntityExecutions'; import { EntitySchedules } from './EntitySchedules'; import { EntityVersions } from './EntityVersions'; import classNames from 'classnames'; +import { StaticGraphContainer } from 'components/Workflow/StaticGraphContainer'; +import { WorkflowId } from 'models/Workflow/types'; const useStyles = makeStyles((theme: Theme) => ({ metadataContainer: { @@ -48,6 +50,7 @@ const useStyles = makeStyles((theme: Theme) => ({ export interface EntityDetailsProps { id: ResourceIdentifier; versionView?: boolean; + showStaticGraph?: boolean; } function getLaunchProps(id: ResourceIdentifier) { @@ -67,9 +70,11 @@ function getLaunchProps(id: ResourceIdentifier) { */ export const EntityDetails: React.FC = ({ id, - versionView = false + versionView = false, + showStaticGraph = false }) => { const sections = entitySections[id.resourceType]; + const workflowId = id as WorkflowId; const project = useProject(id.project); const styles = useStyles(); const [showLaunchForm, setShowLaunchForm] = React.useState(false); @@ -99,13 +104,18 @@ export const EntityDetails: React.FC = ({ )} {sections.versions ? ( -
- -
+ <> + {showStaticGraph ? ( + + ) : null} +
+ +
+ ) : null} {sections.executions && !versionView ? (
diff --git a/src/components/Workflow/StaticGraphContainer.tsx b/src/components/Workflow/StaticGraphContainer.tsx new file mode 100644 index 0000000000..f03fbec2b8 --- /dev/null +++ b/src/components/Workflow/StaticGraphContainer.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Workflow, WorkflowId } from 'models/Workflow/types'; +import { useQuery, useQueryClient } from 'react-query'; +import { makeWorkflowQuery } from './workflowQueries'; +import { WaitForQuery } from 'components/common/WaitForQuery'; +import { DataError } from 'components/Errors/DataError'; +import { transformerWorkflowToDAG } from 'components/WorkflowGraph/transformerWorkflowToDAG'; +import { ReactFlowWrapper } from 'components/flytegraph/ReactFlow/ReactFlowWrapper'; +import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformerDAGToReactFlow'; +import { dNode } from 'models/Graph/types'; +import { getRFBackground } from 'components/flytegraph/ReactFlow/utils'; +import { + ConvertDagProps, + RFGraphTypes, + RFWrapperProps +} from 'components/flytegraph/ReactFlow/types'; + +export const renderStaticGraph = props => { + const workflow = props.closure.compiledWorkflow; + const version = props.id.version; + + const dag: dNode = transformerWorkflowToDAG(workflow); + const rfGraphJson = ConvertFlyteDagToReactFlows({ + root: dag, + maxRenderDepth: 0, + isStaticGraph: true + } as ConvertDagProps); + const backgroundStyle = getRFBackground().static; + const ReactFlowProps: RFWrapperProps = { + backgroundStyle, + rfGraphJson: rfGraphJson, + type: RFGraphTypes.static, + version: version + }; + return ; +}; + +export interface StaticGraphContainerProps { + workflowId: WorkflowId; +} + +export const StaticGraphContainer: React.FC = ({ + workflowId +}) => { + const containerStyle = { + width: '100%', + height: '30%', + maxHeight: '400px', + minHeight: '220px' + }; + const workflowQuery = useQuery( + makeWorkflowQuery(useQueryClient(), workflowId) + ); + + return ( +
+ + {renderStaticGraph} + +
+ ); +}; diff --git a/src/components/Workflow/WorkflowVersionDetails.tsx b/src/components/Workflow/WorkflowVersionDetails.tsx index 297909b76d..fc33c3b761 100644 --- a/src/components/Workflow/WorkflowVersionDetails.tsx +++ b/src/components/Workflow/WorkflowVersionDetails.tsx @@ -7,6 +7,7 @@ export interface WorkflowVersionDetailsRouteParams { projectId: string; domainId: string; workflowName: string; + workflowVersion: string; } export type WorkflowDetailsProps = WorkflowVersionDetailsRouteParams; @@ -19,18 +20,20 @@ export type WorkflowDetailsProps = WorkflowVersionDetailsRouteParams; export const WorkflowVersionDetailsContainer: React.FC = ({ projectId, domainId, - workflowName + workflowName, + workflowVersion }) => { const id = React.useMemo( () => ({ resourceType: ResourceType.WORKFLOW, project: projectId, domain: domainId, - name: workflowName + name: workflowName, + version: workflowVersion }), - [projectId, domainId, workflowName] + [projectId, domainId, workflowName, workflowVersion] ); - return ; + return ; }; export const WorkflowVersionDetails = withRouteParams< diff --git a/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx b/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx index 2ea851c5f4..ab1317afa0 100644 --- a/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx +++ b/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx @@ -39,7 +39,7 @@ export const Legend = () => { const positionStyle: CSSProperties = { bottom: '1rem', right: '1rem', - zIndex: 10000, + zIndex: 10, position: 'absolute', width: '150px' }; diff --git a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 7035848422..2c3b8d9add 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,6 +1,6 @@ import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformerDAGToReactFlow'; import * as React from 'react'; -import { RFWrapperProps, RFGraphTypes } from './types'; +import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; @@ -12,11 +12,12 @@ import { Legend } from './NodeStatusLegend'; */ const ReactFlowGraphComponent = props => { const { data, onNodeSelectionChanged, nodeExecutionsById } = props; - const rfGraphJson = ConvertFlyteDagToReactFlows( - data, - nodeExecutionsById, - onNodeSelectionChanged - ); + const rfGraphJson = ConvertFlyteDagToReactFlows({ + root: data, + nodeExecutionsById: nodeExecutionsById, + onNodeSelectionChanged: onNodeSelectionChanged, + maxRenderDepth: 2 + } as ConvertDagProps); const backgroundStyle = getRFBackground().nested; const ReactFlowProps: RFWrapperProps = { diff --git a/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index f5c91841c8..e998a35f47 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -13,7 +13,9 @@ import { ReactFlowCustomStartNode, ReactFlowCustomTaskNode, ReactFlowCustomSubworkflowNode, - ReactFlowCustomMaxNested + ReactFlowCustomMaxNested, + ReactFlowStaticNested, + ReactFlowStaticNode } from './customNodeComponents'; import setReactFlowGraphLayout from './utils'; @@ -28,7 +30,9 @@ const CustomNodeTypes = { FlyteNode_end: ReactFlowCustomEndNode, FlyteNode_nestedStart: ReactFlowCustomNestedPoint, FlyteNode_nestedEnd: ReactFlowCustomNestedPoint, - FlyteNode_nestedMaxDepth: ReactFlowCustomMaxNested + FlyteNode_nestedMaxDepth: ReactFlowCustomMaxNested, + FlyteNode_staticNode: ReactFlowStaticNode, + FlyteNode_staticNestedNode: ReactFlowStaticNested }; /** @@ -39,7 +43,8 @@ const CustomNodeTypes = { */ const LayoutRC: React.FC = ({ setElements, - setLayout + setLayout, + hasLayout }: LayoutRCProps) => { /* strore is only populated onLoad for each flow */ const nodes = useStoreState(store => store.nodes); @@ -53,6 +58,12 @@ const LayoutRC: React.FC = ({ } } + useEffect(() => { + if (!hasLayout && !computeLayout) { + setComputeLayout(true); + } + }, [hasLayout, computeLayout]); + useEffect(() => { if (!computeLayout) { const nodesAndEdges = (nodes as any[]).concat(edges); @@ -82,10 +93,12 @@ const LayoutRC: React.FC = ({ */ export const ReactFlowWrapper: React.FC = ({ rfGraphJson, - backgroundStyle + backgroundStyle, + version }) => { const [elements, setElements] = useState(rfGraphJson); - const [layedOut, setLayout] = useState(false); + const [currentVersion, setCurrentVersion] = useState(version); + const [hasLayout, setHasLayout] = useState(false); const [reactFlowInstance, setReactFlowInstance] = useState( null ); @@ -94,15 +107,22 @@ export const ReactFlowWrapper: React.FC = ({ setReactFlowInstance(rf); }; + useEffect(() => { + if (version != currentVersion) { + setHasLayout(false); + setElements(rfGraphJson); + } + }, [version, rfGraphJson, currentVersion]); + /** * Note: setLayout passed/called by */ useEffect(() => { - if (layedOut && reactFlowInstance) { - reactFlowInstance?.fitView({ padding: 0.15 }); - false; + if (hasLayout && reactFlowInstance) { + reactFlowInstance?.fitView({ padding: 0 }); + setCurrentVersion(version); } - }, [layedOut, reactFlowInstance]); + }, [hasLayout, reactFlowInstance]); /** * STEPS: * - have each node click return nodes {text.data} @@ -124,7 +144,8 @@ export const ReactFlowWrapper: React.FC = ({ /> diff --git a/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index e4706b9830..5756b7c056 100644 --- a/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -171,6 +171,80 @@ export const ReactFlowCustomMaxNested = ({ data }: any) => { ); }; +export const ReactFlowStaticNested = ({ data }: any) => { + const styles = getGraphNodeStyle(dTypes.staticNestedNode); + const containerStyle = {}; + const taskContainerStyle: React.CSSProperties = { + position: 'absolute', + top: '-.55rem', + zIndex: 0, + right: '.15rem' + }; + const taskTypeStyle: React.CSSProperties = { + backgroundColor: COLOR_GRAPH_BACKGROUND, + color: 'white', + padding: '.1rem .2rem', + fontSize: '.3rem' + }; + + const renderTaskType = () => { + return ( +
+
{data.taskType}
+
+ ); + }; + + return ( +
+ {data.taskType ? renderTaskType() : null} +
{data.text}
+ {renderDefaultHandles( + data.scopedId, + getGraphHandleStyle('source'), + getGraphHandleStyle('target') + )} +
+ ); +}; + +export const ReactFlowStaticNode = ({ data }: any) => { + const styles = getGraphNodeStyle(dTypes.staticNode); + const containerStyle = {}; + const taskContainerStyle: React.CSSProperties = { + position: 'absolute', + top: '-.55rem', + zIndex: 0, + right: '.15rem' + }; + const taskTypeStyle: React.CSSProperties = { + backgroundColor: COLOR_GRAPH_BACKGROUND, + color: 'white', + padding: '.1rem .2rem', + fontSize: '.3rem' + }; + + const renderTaskType = () => { + return ( +
+
{data.taskType}
+
+ ); + }; + + return ( +
+ {data.taskType ? renderTaskType() : null} +
{data.text}
+ {renderDefaultHandles( + data.scopedId, + getGraphHandleStyle('source'), + getGraphHandleStyle('target') + )} +
+ ); +}; + /** * Custom component used by ReactFlow. Renders a label (text) * and any edge handles. @@ -178,6 +252,9 @@ export const ReactFlowCustomMaxNested = ({ data }: any) => { */ export const ReactFlowCustomTaskNode = ({ data }: any) => { + // console.log(`\n\n@ReactFlowCustomTaskNode: ${data.text}`); + // console.log('\t data.nodeType:', dTypes[data.nodeType]); + // console.log('\t data.nodeExecutionStatus:', data.nodeExecutionStatus); const styles = getGraphNodeStyle(data.nodeType, data.nodeExecutionStatus); const onNodeSelectionChanged = data.onNodeSelectionChanged; const [selectedNode, setSelectedNode] = useState(false); @@ -277,7 +354,7 @@ export const ReactFlowCustomSubworkflowNode = ({ data }: any) => { */ export const ReactFlowCustomBranchNode = ({ data }: any) => { const { dag } = data; - console.log('@ReactFlowCustomBranchNode: data'); + console.log('@ReactFlowCustomBranchNode: data', data); const backgroundStyle = getRFBackground().nested; const borderStyle = getNestedContainerStyle(data.nodeExecutionStatus); const { estimatedDimensions } = setReactFlowGraphLayout(dag, 'LR', true); diff --git a/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx b/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx index 08afc5a3d8..a693853b0f 100644 --- a/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx +++ b/src/components/flytegraph/ReactFlow/transformerDAGToReactFlow.tsx @@ -1,8 +1,8 @@ -import { dEdge, dNode, dTypes } from 'models/Graph/types'; -import { MAX_NESTED_DEPTH, ReactFlowGraphConfig } from './utils'; +import { dEdge, dTypes } from 'models/Graph/types'; +import { ReactFlowGraphConfig } from './utils'; import { Edge, Elements, Node, Position } from 'react-flow-renderer'; -import { NodeExecutionsById } from 'models/Execution/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; +import { BuildRFNodeProps, ConvertDagProps, DagToFRProps } from './types'; export const buildCustomNodeName = (type: dTypes) => { return `${ReactFlowGraphConfig.customNodePrefix}_${dTypes[type]}`; @@ -19,51 +19,52 @@ export const buildReactFlowEdge = (edge: dEdge): Edge => { } as Edge; }; -export const buildReactFlowNode = ( - dNode: dNode, - dag: any = [], - nodeExecutionsById: NodeExecutionsById, - typeOverride?: dTypes | null, - onNodeSelectionChanged?: any | null -): Node => { +export const buildReactFlowNode = (props: BuildRFNodeProps): Node => { + const { + dNode, + dag, + nodeExecutionsById, + typeOverride, + onNodeSelectionChanged + } = props; + const type = typeOverride ? typeOverride : dNode.type; const taskType = dNode?.value?.template ? dNode.value.template.type : null; - - /** - * @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; const mapNodeExecutionStatus = () => { - if (nodeExecutionsById[dNode.scopedId]) { - return nodeExecutionsById[dNode.scopedId].closure - .phase as NodeExecutionPhase; + if (nodeExecutionsById) { + if (nodeExecutionsById[dNode.scopedId]) { + return nodeExecutionsById[dNode.scopedId].closure + .phase as NodeExecutionPhase; + } else { + return NodeExecutionPhase.SKIPPED; + } } else { - return NodeExecutionPhase.SKIPPED; + return NodeExecutionPhase.UNDEFINED; } }; const nodeExecutionStatus = mapNodeExecutionStatus(); + const dataProps = { + nodeExecutionStatus: nodeExecutionStatus, + text: displayName, + handles: [], + nodeType: type, + scopedId: dNode.scopedId, + dag: dag, + taskType: taskType, + onNodeSelectionChanged: () => { + if (onNodeSelectionChanged) { + onNodeSelectionChanged([dNode.scopedId]); + } + } + }; + return { id: dNode.scopedId, type: buildCustomNodeName(type), - data: { - nodeExecutionStatus: nodeExecutionStatus, - text: displayName, - handles: [], - nodeType: type, - scopedId: dNode.scopedId, - dag: dag, - taskType: taskType, - onNodeSelectionChanged: () => { - onNodeSelectionChanged([dNode.scopedId]); - } - }, + data: dataProps, position: { x: 0, y: 0 }, sourcePosition: Position.Right, targetPosition: Position.Left @@ -78,51 +79,58 @@ export const nodeMapToArr = map => { return output; }; -export const dagToReactFlow = ( - dag: dNode, - nodeExecutionsById: NodeExecutionsById, - nestedDepth = 0, - onNodeSelectionChanged -) => { +export const dagToReactFlow = (props: DagToFRProps) => { + const { + root, + nodeExecutionsById, + currentDepth, + onNodeSelectionChanged, + maxRenderDepth, + isStaticGraph + } = props; + const nodes: any = {}; const edges: any = {}; - dag.nodes?.map(dNode => { - if (dNode.nodes?.length > 0 && nestedDepth <= MAX_NESTED_DEPTH) { + root.nodes?.map(dNode => { + /* Base props to build RF Node */ + const buildNodeProps = { + dNode: dNode, + dag: [], + nodeExecutionsById: nodeExecutionsById, + typeOverride: null, + onNodeSelectionChanged: onNodeSelectionChanged, + isStaticGraph: isStaticGraph + } as BuildRFNodeProps; + if (dNode.nodes?.length > 0 && currentDepth <= maxRenderDepth) { /* Note: currentDepth will be replaced once nested toggle */ - if (nestedDepth == MAX_NESTED_DEPTH) { - nodes[dNode.id] = buildReactFlowNode( - dNode, - [], - nodeExecutionsById, - dTypes.nestedMaxDepth, - onNodeSelectionChanged - ); + if (currentDepth == maxRenderDepth) { + buildNodeProps.typeOverride = isStaticGraph + ? dTypes.staticNestedNode + : dTypes.nestedMaxDepth; } else { - nodes[dNode.id] = buildReactFlowNode( - dNode, - dagToReactFlow( - dNode, - nodeExecutionsById, - nestedDepth + 1, - onNodeSelectionChanged - ), - nodeExecutionsById, - null, - onNodeSelectionChanged - ); + const nestedDagProps: DagToFRProps = { + root: dNode, + nodeExecutionsById: nodeExecutionsById, + currentDepth: currentDepth + 1, + onNodeSelectionChanged: onNodeSelectionChanged, + maxRenderDepth: maxRenderDepth, + isStaticGraph: isStaticGraph + }; + buildNodeProps.dag = dagToReactFlow(nestedDagProps); + buildNodeProps.typeOverride = isStaticGraph + ? dTypes.staticNode + : null; } } else { - nodes[dNode.id] = buildReactFlowNode( - dNode, - [], - nodeExecutionsById, - null, - onNodeSelectionChanged - ); + buildNodeProps.typeOverride = isStaticGraph + ? dTypes.staticNode + : null; } + /* Build and add node to map */ + nodes[dNode.id] = buildReactFlowNode(buildNodeProps); }); - dag.edges?.map(edge => { + root.edges?.map(edge => { const rfEdge = buildReactFlowEdge(edge); edges[rfEdge.id] = rfEdge; }); @@ -131,16 +139,9 @@ export const dagToReactFlow = ( }; export const ConvertFlyteDagToReactFlows = ( - root: dNode, - nodeExecutionsById: NodeExecutionsById, - onNodeSelectionChanged + props: ConvertDagProps ): Elements => { - const rfJson = dagToReactFlow( - root, - nodeExecutionsById, - 0, - onNodeSelectionChanged - ); - + const dagProps = { ...props, currentDepth: 0 } as DagToFRProps; + const rfJson = dagToReactFlow(dagProps); return rfJson; }; diff --git a/src/components/flytegraph/ReactFlow/types.ts b/src/components/flytegraph/ReactFlow/types.ts index b68d9f0b03..49412edcf8 100644 --- a/src/components/flytegraph/ReactFlow/types.ts +++ b/src/components/flytegraph/ReactFlow/types.ts @@ -1,5 +1,5 @@ import { NodeExecutionsById } from 'models/Execution/types'; -import { dTypes } from 'models/Graph/types'; +import { dNode, dTypes } from 'models/Graph/types'; import { Elements, HandleProps } from 'react-flow-renderer'; export interface RFWrapperProps { @@ -7,6 +7,7 @@ export interface RFWrapperProps { backgroundStyle: RFBackgroundProps; type: RFGraphTypes; onNodeSelectionChanged?: any; + version?: string; } /* Note: extending to allow applying styles directly to handle */ @@ -16,12 +17,14 @@ export interface RFHandleProps extends HandleProps { export enum RFGraphTypes { main, - nested + nested, + static } export interface LayoutRCProps { setElements: any; setLayout: any; + hasLayout: boolean; } /* React Flow params and styles (background is styles) */ @@ -31,6 +34,27 @@ export interface RFBackgroundProps { gridSpacing: number; } +export interface BuildRFNodeProps { + dNode: dNode; + dag?: any[]; + nodeExecutionsById: any; + typeOverride: dTypes | null; + onNodeSelectionChanged: any; + isStaticGraph: boolean; +} + +export interface ConvertDagProps { + root: dNode; + nodeExecutionsById: any; + onNodeSelectionChanged: any; + maxRenderDepth: number; + isStaticGraph: boolean; +} + +export interface DagToFRProps extends ConvertDagProps { + currentDepth: number; +} + export interface RFCustomData { nodeExecutionStatus: NodeExecutionsById; text: string; diff --git a/src/components/flytegraph/ReactFlow/utils.tsx b/src/components/flytegraph/ReactFlow/utils.tsx index 27f4208603..01d017c9fb 100644 --- a/src/components/flytegraph/ReactFlow/utils.tsx +++ b/src/components/flytegraph/ReactFlow/utils.tsx @@ -211,6 +211,15 @@ export const getGraphNodeStyle = ( }, task: { borderColor: nodePrimaryColor + }, + staticNode: { + backgroundColor: '#fff', + borderColor: '#bfbfbf', + borderWidth: '.05rem' + }, + staticNestedNode: { + backgroundColor: '#dfdfdf', + border: 'none' } }; const key = String(dTypes[type]); @@ -234,6 +243,14 @@ export const getRFBackground = () => { nested: { gridColor: 'none', gridSpacing: 1 + } as RFBackgroundProps, + static: { + background: { + border: 'none', + backgroundColor: 'rgb(255,255,255)' + }, + gridColor: 'none', + gridSpacing: 20 } as RFBackgroundProps }; }; diff --git a/src/models/Graph/types.ts b/src/models/Graph/types.ts index 36c1ddb7ac..458ef74465 100644 --- a/src/models/Graph/types.ts +++ b/src/models/Graph/types.ts @@ -24,7 +24,9 @@ export enum dTypes { end, nestedEnd, nestedStart, - nestedMaxDepth + nestedMaxDepth, + staticNode, + staticNestedNode } /**