From e276493e956958a8e5858785ad5f5a4d31d5eb76 Mon Sep 17 00:00:00 2001 From: Pianist <26953709+Pianist038801@users.noreply.github.com> Date: Tue, 18 Jan 2022 13:15:52 -0500 Subject: [PATCH] feat: show details panel for failed node (#254) * feat: show details panel for failed node Signed-off-by: Pianist038801 * feat: use DAG for details panel content Signed-off-by: Pianist038801 * fix: failing tests Signed-off-by: Pianist038801 * fix: failing tests Signed-off-by: Pianist038801 * fix: compound nodeId bug Signed-off-by: Pianist038801 * fix: feedback on the PR Signed-off-by: Pianist038801 * chore: force casesensitive file rename to be consumed by git Signed-off-by: Nastya Rusina Co-authored-by: Pianist038801 Co-authored-by: Nastya Rusina --- assetsTransformer.js | 9 ++ jest.config.js | 4 + .../ExecutionWorkflowGraph.tsx | 15 ++- .../NodeExecutionDetailsPanelContent.tsx | 116 +++++++++++++++++- .../Workflow/StaticGraphContainer.tsx | 4 +- .../WorkflowGraph/WorkflowGraph.tsx | 4 +- ...ToDAG.tsx => transformerWorkflowToDag.tsx} | 4 +- src/components/WorkflowGraph/utils.ts | 31 ++++- .../ReactFlow/ReactFlowGraphComponent.tsx | 2 +- 9 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 assetsTransformer.js rename src/components/WorkflowGraph/{transformerWorkflowToDAG.tsx => transformerWorkflowToDag.tsx} (98%) diff --git a/assetsTransformer.js b/assetsTransformer.js new file mode 100644 index 000000000..bbdc8a401 --- /dev/null +++ b/assetsTransformer.js @@ -0,0 +1,9 @@ +const path = require('path'); + +module.exports = { + process(_src, filename, _config, _options) { + return ( + 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';' + ); + } +}; diff --git a/jest.config.js b/jest.config.js index ef1a2f6b1..976cd82d9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,6 +19,10 @@ module.exports = { '^.+\\.(j|t)sx?$': 'ts-jest' }, transformIgnorePatterns: ['/node_modules/(?!@flyteorg/flyteidl)'], + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/assetsTransformer.js' + }, coverageDirectory: '.coverage', collectCoverageFrom: ['**/*.{ts,tsx}'], coveragePathIgnorePatterns: [ diff --git a/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx b/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx index 350a21ab1..8cdb45129 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, 'scopedId'), + () => keyBy(nodeExecutions, 'id.nodeId'), [nodeExecutions] ); @@ -36,10 +36,6 @@ export const ExecutionWorkflowGraph: React.FC = ({ if (nodeId === startNodeId || nodeId === endNodeId) { return false; } - const execution = nodeExecutionsById[nodeId]; - if (!execution) { - return false; - } return true; }); setSelectedNodes(validSelection); @@ -48,7 +44,14 @@ export const ExecutionWorkflowGraph: React.FC = ({ // Note: flytegraph allows multiple selection, but we only support showing // a single item in the details panel const selectedExecution = selectedNodes.length - ? nodeExecutionsById[selectedNodes[0]].id + ? nodeExecutionsById[selectedNodes[0]] + ? nodeExecutionsById[selectedNodes[0]].id + : { + nodeId: selectedNodes[0], + executionId: + nodeExecutionsById[Object.keys(nodeExecutionsById)[0]].id + .executionId + } : null; const onCloseDetailsPanel = () => setSelectedNodes([]); diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index b41a1fd3a..b6e96e92b 100644 --- a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react'; import { IconButton, Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import Tab from '@material-ui/core/Tab'; @@ -6,11 +7,13 @@ import Close from '@material-ui/icons/Close'; import * as classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; import { InfoIcon } from 'components/common/Icons/InfoIcon'; +import { bodyFontFamily, smallFontSize } from 'components/Theme/constants'; import { ExecutionStatusBadge } from 'components/Executions/ExecutionStatusBadge'; import { LocationState } from 'components/hooks/useLocationState'; import { useTabState } from 'components/hooks/useTabState'; import { LocationDescriptor } from 'history'; import { PaginatedEntityResponse } from 'models/AdminEntity/types'; +import { Workflow } from 'models/Workflow/types'; import { NodeExecution, NodeExecutionIdentifier, @@ -19,7 +22,7 @@ import { import { TaskTemplate } from 'models/Task/types'; import * as React from 'react'; import Skeleton from 'react-loading-skeleton'; -import { useQuery } from 'react-query'; +import { useQuery, useQueryClient } from 'react-query'; import { Link as RouterLink } from 'react-router-dom'; import { Routes } from 'routes/routes'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; @@ -35,11 +38,34 @@ import { NodeExecutionOutputs } from './NodeExecutionOutputs'; import { NodeExecutionTaskDetails } from './NodeExecutionTaskDetails'; import { getTaskExecutionDetailReasons } from './utils'; import { ExpandableMonospaceText } from '../../common/ExpandableMonospaceText'; +import { fetchWorkflowExecution } from '../useWorkflowExecution'; +import { RemoteLiteralMapViewer } from 'components/Literals/RemoteLiteralMapViewer'; +import { fetchWorkflow } from 'components/Workflow/workflowQueries'; +import { dNode } from 'models/Graph/types'; +import { + transformWorkflowToKeyedDag, + getNodeNameFromDag +} from 'components/WorkflowGraph/utils'; const useStyles = makeStyles((theme: Theme) => { const paddingVertical = `${theme.spacing(2)}px`; const paddingHorizontal = `${theme.spacing(3)}px`; return { + notRunStatus: { + alignItems: 'center', + backgroundColor: 'gray', + borderRadius: '4px', + color: theme.palette.text.primary, + display: 'flex', + flex: '0 0 auto', + height: theme.spacing(3), + fontSize: smallFontSize, + justifyContent: 'center', + textTransform: 'uppercase', + width: theme.spacing(11), + fontFamily: bodyFontFamily, + fontWeight: 'bold' + }, closeButton: { marginLeft: theme.spacing(1) }, @@ -218,13 +244,64 @@ const NodeExecutionTabs: React.FC<{ ); }; +const WorkflowTabs: React.FC<{ + dagData: dNode; + nodeId: string; +}> = ({ dagData, nodeId }) => { + const styles = useStyles(); + const tabState = useTabState(tabIds, tabIds.inputs); + const commonStyles = useCommonStyles(); + let tabContent: JSX.Element | null = null; + const id = nodeId.slice(nodeId.lastIndexOf('-') + 1); + const taskTemplate = dagData[id].value.template; + + switch (tabState.value) { + case tabIds.inputs: { + tabContent = taskTemplate ? ( +
+
+ +
+
+ ) : null; + break; + } + case tabIds.task: { + tabContent = taskTemplate ? ( + + ) : null; + break; + } + } + return ( + <> + + + {!!taskTemplate && } + +
{tabContent}
+ + ); +}; + /** DetailsPanel content which renders execution information about a given NodeExecution */ export const NodeExecutionDetailsPanelContent: React.FC = ({ nodeExecutionId, onClose }) => { + const [mounted, setMounted] = useState(true); + useEffect(() => { + return () => { + setMounted(false); + }; + }, []); + const queryClient = useQueryClient(); const [isReasonsVisible, setReasonsVisible] = React.useState(false); + const [dag, setDag] = React.useState(null); const nodeExecutionQuery = useQuery({ ...makeNodeExecutionQuery(nodeExecutionId), // The selected NodeExecution has been fetched at this point, we don't want to @@ -238,6 +315,26 @@ export const NodeExecutionDetailsPanelContent: React.FC { + const workflowExecution = await fetchWorkflowExecution( + queryClient, + nodeExecutionId.executionId + ); + const workflowData: Workflow = await fetchWorkflow( + queryClient, + workflowExecution.closure.workflowId + ); + if (workflowData) { + const keyedDag = transformWorkflowToKeyedDag(workflowData); + if (mounted) setDag(keyedDag); + } + }; + + if (!nodeExecution) { + getWorkflowDag(); + } else { + if (dag) setDag(null); + } const listTaskExecutionsQuery = useQuery< PaginatedEntityResponse, Error @@ -299,7 +396,9 @@ export const NodeExecutionDetailsPanelContent: React.FC )} - ) : null; + ) : ( +
NOT RUN
+ ); const detailsContent = nodeExecution ? ( <> @@ -319,7 +418,6 @@ export const NodeExecutionDetailsPanelContent: React.FC ) : null; - return (
@@ -348,13 +446,19 @@ export const NodeExecutionDetailsPanelContent: React.FC - {displayName} + {dag + ? getNodeNameFromDag(dag, nodeExecutionId.nodeId) + : displayName} {statusContent} - {detailsContent} + {!dag && detailsContent}
- {tabsContent} + {dag ? ( + + ) : ( + tabsContent + )}
); }; diff --git a/src/components/Workflow/StaticGraphContainer.tsx b/src/components/Workflow/StaticGraphContainer.tsx index 655040609..3ecaa396a 100644 --- a/src/components/Workflow/StaticGraphContainer.tsx +++ b/src/components/Workflow/StaticGraphContainer.tsx @@ -4,7 +4,7 @@ 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 { 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'; @@ -19,7 +19,7 @@ export const renderStaticGraph = props => { const workflow = props.closure.compiledWorkflow; const version = props.id.version; - const dag: dNode = transformerWorkflowToDAG(workflow); + const dag: dNode = transformerWorkflowToDag(workflow); const rfGraphJson = ConvertFlyteDagToReactFlows({ root: dag, maxRenderDepth: 0, diff --git a/src/components/WorkflowGraph/WorkflowGraph.tsx b/src/components/WorkflowGraph/WorkflowGraph.tsx index cc8965d3e..3f96917ab 100644 --- a/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -1,4 +1,4 @@ -import { transformerWorkflowToDAG } from './transformerWorkflowToDAG'; +import { transformerWorkflowToDag } from './transformerWorkflowToDag'; import { dNode } from 'models/Graph/types'; import { Workflow } from 'models/Workflow/types'; import * as React from 'react'; @@ -31,7 +31,7 @@ function workflowToDag(workflow: Workflow): PrepareDAGResult { throw new Error('Workflow closure missing a compiled workflow'); } const { compiledWorkflow } = workflow.closure; - const dag: dNode = transformerWorkflowToDAG(compiledWorkflow); + const dag: dNode = transformerWorkflowToDag(compiledWorkflow); return { dag }; } catch (e) { return { diff --git a/src/components/WorkflowGraph/transformerWorkflowToDAG.tsx b/src/components/WorkflowGraph/transformerWorkflowToDag.tsx similarity index 98% rename from src/components/WorkflowGraph/transformerWorkflowToDAG.tsx rename to src/components/WorkflowGraph/transformerWorkflowToDag.tsx index bacd5d99d..8247bf98a 100644 --- a/src/components/WorkflowGraph/transformerWorkflowToDAG.tsx +++ b/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -25,7 +25,7 @@ import { * @param context input can be either CompiledWorkflow or CompiledNode * @returns Display name */ -export const transformerWorkflowToDAG = ( +export const transformerWorkflowToDag = ( workflow: CompiledWorkflowClosure ): dNode => { const { primary } = workflow; @@ -255,7 +255,7 @@ export const buildOutWorkflowEdges = ( const list = context.downstream[ingress].ids; for (let i = 0; i < list.length; i++) { const edge: dEdge = { - sourceId: nodeMap[ingress].dNode.scopedId, + sourceId: nodeMap[ingress] && nodeMap[ingress].dNode.scopedId, targetId: nodeMap[list[i]].dNode.scopedId }; root.edges.push(edge); diff --git a/src/components/WorkflowGraph/utils.ts b/src/components/WorkflowGraph/utils.ts index 280123752..49b3da310 100644 --- a/src/components/WorkflowGraph/utils.ts +++ b/src/components/WorkflowGraph/utils.ts @@ -1,10 +1,10 @@ import { Identifier } from 'models/Common/types'; import { endNodeId, startNodeId } from 'models/Node/constants'; -import { CompiledWorkflow } from 'models/Workflow/types'; +import { CompiledWorkflow, Workflow } from 'models/Workflow/types'; import { CompiledNode, TaskNode } from 'models/Node/types'; import { CompiledTask, TaskTemplate } from 'models/Task/types'; -import { dTypes } from 'models/Graph/types'; - +import { dTypes, dNode } from 'models/Graph/types'; +import { transformerWorkflowToDag } from './transformerWorkflowToDag'; /** * @TODO these are dupes for testing, remove once tests fixed */ @@ -116,3 +116,28 @@ export const getTaskTypeFromCompiledNode = ( } return null; }; + +export const getNodeNameFromDag = (dagData: dNode, nodeId: string) => { + const id = nodeId.slice(nodeId.lastIndexOf('-') + 1); + const value = dagData[id].value; + + if (value.taskNode) { + return value.taskNode.referenceId.name; + } else if (value.workflowNode) { + return value.workflowNode.subWorkflowRef.name; + } + return ''; +}; + +export const transformWorkflowToKeyedDag = (workflow: Workflow) => { + if (!workflow.closure?.compiledWorkflow) return {}; + + const dagData = transformerWorkflowToDag( + workflow.closure?.compiledWorkflow + ); + const data = {}; + dagData.nodes.forEach(node => { + data[`${node.id}`] = node; + }); + return data; +}; diff --git a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index f6e3b439c..350091816 100644 --- a/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -7,7 +7,7 @@ import { Legend } from './NodeStatusLegend'; /** * Renders workflow graph using React Flow. - * @param props.data DAG from transformerWorkflowToDAG + * @param props.data DAG from transformerWorkflowToDag * @returns ReactFlow Graph as */ const ReactFlowGraphComponent = props => {