From 7bed57ea1886b2bcff9ac99a701f625c14f8d284 Mon Sep 17 00:00:00 2001 From: olga-union <101579322+olga-union@users.noreply.github.com> Date: Fri, 13 May 2022 10:15:18 -0500 Subject: [PATCH] fix: update node executions to display map tasks (#455) * fix: update node executions to display map tasks * fix: update map task logs styles * test: add/update unit tests * fix: fix flickering and unnecessary re-renders Signed-off-by: Olga Nad --- .../ExecutionWorkflowGraph.tsx | 36 +- .../NodeExecutionDetailsPanelContent.tsx | 104 +++-- .../NodeExecutionTabs/index.tsx | 14 +- .../NodeExecutionTabs/test/index.test.tsx | 61 +++ .../MapTaskExecutionListItem.tsx | 41 +- .../TaskExecutionsList/TaskExecutionsList.tsx | 10 +- .../TaskExecutionsList/constants.ts | 12 + .../Executions/TaskExecutionsList/utils.ts | 16 +- .../WorkflowGraph/WorkflowGraph.tsx | 15 +- .../__stories__/WorkflowGraph.stories.tsx | 7 +- .../WorkflowGraph/test/WorkflowGraph.test.tsx | 39 ++ .../test/nodeExecutionsById.mock.ts | 150 +++++++ .../WorkflowGraph/test/workflow.mock.ts | 370 ++++++++++++++++++ .../MapTaskStatusInfo.test.tsx | 16 +- .../MapTaskStatusInfo.tsx | 14 +- .../ReactFlow/ReactFlowGraphComponent.tsx | 58 ++- .../ReactFlow/customNodeComponents.tsx | 120 +++++- .../ReactFlow/transformDAGToReactFlowV2.tsx | 104 ++--- .../components/flytegraph/ReactFlow/utils.tsx | 6 +- .../console/src/models/Execution/types.ts | 5 +- 20 files changed, 1031 insertions(+), 167 deletions(-) create mode 100644 packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/test/index.test.tsx create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/constants.ts create mode 100644 packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx create mode 100644 packages/zapp/console/src/components/WorkflowGraph/test/nodeExecutionsById.mock.ts create mode 100644 packages/zapp/console/src/components/WorkflowGraph/test/workflow.mock.ts diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx index ad0c32f59..379d850ce 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx @@ -4,12 +4,16 @@ import { DataError } from 'components/Errors/DataError'; import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; import { keyBy } from 'lodash'; -import { NodeExecution } from 'models/Execution/types'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types'; import { endNodeId, startNodeId } from 'models/Node/constants'; import { Workflow, WorkflowId } from 'models/Workflow/types'; import * as React from 'react'; +import { useMemo, useState } from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { NodeExecutionsContext } from '../contexts'; +import { getGroupedLogs } from '../TaskExecutionsList/utils'; +import { useTaskExecutions, useTaskExecutionsRefresher } from '../useTaskExecutions'; import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; export interface ExecutionWorkflowGraphProps { @@ -23,12 +27,30 @@ export const ExecutionWorkflowGraph: React.FC = ({ workflowId, }) => { const workflowQuery = useQuery(makeWorkflowQuery(useQueryClient(), workflowId)); - const nodeExecutionsById = React.useMemo( - () => keyBy(nodeExecutions, 'scopedId'), - [nodeExecutions], + + const nodeExecutionsWithResources = nodeExecutions.map((nodeExecution) => { + const taskExecutions = useTaskExecutions(nodeExecution.id); + useTaskExecutionsRefresher(nodeExecution, taskExecutions); + + const externalResources: ExternalResource[] = taskExecutions.value + .map((taskExecution) => taskExecution.closure.metadata?.externalResources) + .flat() + .filter((resource): resource is ExternalResource => !!resource); + + const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); + + return { + ...nodeExecution, + ...(logsByPhase.size > 0 && { logsByPhase }), + }; + }); + + const nodeExecutionsById = useMemo( + () => keyBy(nodeExecutionsWithResources, 'scopedId'), + [nodeExecutionsWithResources], ); - const [selectedNodes, setSelectedNodes] = React.useState([]); + const [selectedNodes, setSelectedNodes] = useState([]); const onNodeSelectionChanged = (newSelection: string[]) => { const validSelection = newSelection.filter((nodeId) => { if (nodeId === startNodeId || nodeId === endNodeId) { @@ -52,9 +74,12 @@ export const ExecutionWorkflowGraph: React.FC = ({ const onCloseDetailsPanel = () => setSelectedNodes([]); + const [selectedPhase, setSelectedPhase] = useState(undefined); + const renderGraph = (workflow: Workflow) => ( @@ -71,6 +96,7 @@ export const ExecutionWorkflowGraph: React.FC = ({ {selectedExecution && ( )} diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index 561ec53aa..09ad0f06f 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { IconButton, Typography, Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import Close from '@material-ui/icons/Close'; +import ArrowBackIos from '@material-ui/icons/ArrowBackIos'; import classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; import { InfoIcon } from 'components/common/Icons/InfoIcon'; @@ -23,7 +24,7 @@ import { fetchWorkflow } from 'components/Workflow/workflowQueries'; import { PanelSection } from 'components/common/PanelSection'; import { DumpJSON } from 'components/common/DumpJSON'; import { dNode } from 'models/Graph/types'; -import { NodeExecutionPhase } from 'models/Execution/enums'; +import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums'; import { transformWorkflowToKeyedDag, getNodeNameFromDag } from 'components/WorkflowGraph/utils'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; import { makeListTaskExecutionsQuery, makeNodeExecutionQuery } from '../nodeExecutionQueries'; @@ -126,6 +127,7 @@ const tabIds = { interface NodeExecutionDetailsProps { nodeExecutionId: NodeExecutionIdentifier; + phase?: TaskExecutionPhase; onClose?: () => void; } @@ -218,8 +220,19 @@ const WorkflowTabs: React.FC<{ */ export const NodeExecutionDetailsPanelContent: React.FC = ({ nodeExecutionId, + phase, onClose, }) => { + const commonStyles = useCommonStyles(); + const styles = useStyles(); + const queryClient = useQueryClient(); + const detailsContext = useNodeExecutionContext(); + + const [isReasonsVisible, setReasonsVisible] = useState(false); + const [dag, setDag] = useState(null); + const [details, setDetails] = useState(); + const [shouldShowTaskDetails, setShouldShowTaskDetails] = useState(false); // TODO to be reused in https://github.com/flyteorg/flyteconsole/issues/312 + const isMounted = useRef(false); useEffect(() => { isMounted.current = true; @@ -228,13 +241,6 @@ export const NodeExecutionDetailsPanelContent: React.FC(null); - const [details, setDetails] = React.useState(); - const nodeExecutionQuery = useQuery({ ...makeNodeExecutionQuery(nodeExecutionId), // The selected NodeExecution has been fetched at this point, we don't want to @@ -242,7 +248,7 @@ export const NodeExecutionDetailsPanelContent: React.FC { + useEffect(() => { let isCurrent = true; detailsContext.getNodeExecutionDetails(nodeExecution).then((res) => { if (isCurrent) { @@ -255,7 +261,7 @@ export const NodeExecutionDetailsPanelContent: React.FC { + useEffect(() => { setReasonsVisible(false); }, [nodeExecutionId]); @@ -288,20 +294,48 @@ export const NodeExecutionDetailsPanelContent: React.FC; + const onBackClick = () => { + setShouldShowTaskDetails(false); + }; + + const headerTitle = useMemo(() => { + // TODO to be reused in https://github.com/flyteorg/flyteconsole/issues/312 + // // eslint-disable-next-line no-useless-escape + // const regex = /\-([\w\s-]+)\-/; // extract string between first and last dash + + // const mapTaskHeader = `${mapTask?.[0].externalId?.match(regex)?.[1]} of ${ + // nodeExecutionId.nodeId + // }`; + // const header = shouldShowTaskDetails ? mapTaskHeader : nodeExecutionId.nodeId; + const header = nodeExecutionId.nodeId; - const isRunningPhase = React.useMemo(() => { + return ( + +
+ {shouldShowTaskDetails && ( + + + + )} + {header} +
+ + + +
+ ); + }, [nodeExecutionId, shouldShowTaskDetails]); + + const isRunningPhase = useMemo(() => { return ( nodeExecution?.closure.phase === NodeExecutionPhase.QUEUED || nodeExecution?.closure.phase === NodeExecutionPhase.RUNNING ); }, [nodeExecution]); - const handleReasonsVisibility = React.useCallback(() => { - setReasonsVisible((prevVisibility) => !prevVisibility); - }, []); + const handleReasonsVisibility = () => { + setReasonsVisible(!isReasonsVisible); + }; const statusContent = nodeExecution ? (
@@ -321,26 +355,32 @@ export const NodeExecutionDetailsPanelContent: React.FCNOT RUN
); - const detailsContent = nodeExecution ? ( - <> - - - - ) : null; + let detailsContent: JSX.Element | null = null; + if (nodeExecution) { + detailsContent = ( + <> + + + + ); + } - const tabsContent = nodeExecution ? ( - + const tabsContent: JSX.Element | null = nodeExecution ? ( + ) : null; + + const displayName = details?.displayName ?? ; + return (
- - {nodeExecutionId.nodeId} - - - - + {headerTitle} = ({ nodeExecution, taskTemplate }) => { +}> = ({ nodeExecution, shouldShowTaskDetails, taskTemplate, phase }) => { const styles = useStyles(); const tabState = useTabState(tabIds, defaultTab); @@ -55,7 +58,7 @@ export const NodeExecutionTabs: React.FC<{ let tabContent: JSX.Element | null = null; switch (tabState.value) { case tabIds.executions: { - tabContent = ; + tabContent = ; break; } case tabIds.inputs: { @@ -76,7 +79,12 @@ export const NodeExecutionTabs: React.FC<{ } } - const executionLabel = isMapTaskType(taskTemplate?.type) ? 'Map Execution' : 'Executions'; + const executionLabel = isMapTaskType(taskTemplate?.type) + ? shouldShowTaskDetails + ? 'Execution' + : 'Map Execution' + : 'Executions'; + return ( <> diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/test/index.test.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/test/index.test.tsx new file mode 100644 index 000000000..1e5de265f --- /dev/null +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/test/index.test.tsx @@ -0,0 +1,61 @@ +import { render } from '@testing-library/react'; +import { useTabState } from 'components/hooks/useTabState'; +import { extractTaskTemplates } from 'components/hooks/utils'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { createMockNodeExecutions } from 'models/Execution/__mocks__/mockNodeExecutionsData'; +import { TaskType } from 'models/Task/constants'; +import { createMockWorkflow } from 'models/__mocks__/workflowData'; +import * as React from 'react'; +import { NodeExecutionTabs } from '../index'; + +const getMockNodeExecution = () => createMockNodeExecutions(1).executions[0]; +const nodeExecution = getMockNodeExecution(); +const workflow = createMockWorkflow('SampleWorkflow'); +const taskTemplate = { ...extractTaskTemplates(workflow)[0], type: TaskType.ARRAY }; +const phase = TaskExecutionPhase.SUCCEEDED; + +jest.mock('components/hooks/useTabState'); + +describe('NodeExecutionTabs', () => { + const mockUseTabState = useTabState as jest.Mock; + mockUseTabState.mockReturnValue({ onChange: jest.fn(), value: 'executions' }); + describe('with map tasks', () => { + it('should display proper tab name when it was provided and shouldShow is TRUE', async () => { + const { queryByText, queryAllByRole } = render( + , + ); + expect(queryAllByRole('tab')).toHaveLength(4); + expect(queryByText('Execution')).toBeInTheDocument(); + }); + + it('should display proper tab name when it was provided and shouldShow is FALSE', async () => { + const { queryByText, queryAllByRole } = render( + , + ); + + expect(queryAllByRole('tab')).toHaveLength(4); + expect(queryByText('Map Execution')).toBeInTheDocument(); + }); + }); + + describe('without map tasks', () => { + it('should display proper tab name when mapTask was not provided', async () => { + const { queryAllByRole, queryByText } = render( + , + ); + + expect(queryAllByRole('tab')).toHaveLength(3); + expect(queryByText('Executions')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx index 078d4b680..3fdf143c2 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx @@ -11,6 +11,7 @@ import { TaskExecutionDetails } from './TaskExecutionDetails'; import { TaskExecutionError } from './TaskExecutionError'; import { TaskExecutionLogs } from './TaskExecutionLogs'; import { formatRetryAttempt, getGroupedLogs } from './utils'; +import { RENDER_ORDER } from './constants'; const useStyles = makeStyles((theme: Theme) => ({ detailsLink: { @@ -33,23 +34,14 @@ const useStyles = makeStyles((theme: Theme) => ({ interface MapTaskExecutionsListItemProps { taskExecution: TaskExecution; showAttempts: boolean; + selectedPhase?: TaskExecutionPhase; } -const RENDER_ORDER: TaskExecutionPhase[] = [ - TaskExecutionPhase.UNDEFINED, - TaskExecutionPhase.INITIALIZING, - TaskExecutionPhase.WAITING_FOR_RESOURCES, - TaskExecutionPhase.QUEUED, - TaskExecutionPhase.RUNNING, - TaskExecutionPhase.SUCCEEDED, - TaskExecutionPhase.ABORTED, - TaskExecutionPhase.FAILED, -]; - /** Renders an individual `TaskExecution` record as part of a list */ export const MapTaskExecutionsListItem: React.FC = ({ taskExecution, showAttempts, + selectedPhase, }) => { const commonStyles = useCommonStyles(); const styles = useStyles(); @@ -57,16 +49,7 @@ export const MapTaskExecutionsListItem: React.FC const { closure } = taskExecution; const taskHasStarted = closure.phase >= TaskExecutionPhase.QUEUED; const headerText = formatRetryAttempt(taskExecution.id.retryAttempt); - const logsInfo = getGroupedLogs(closure.metadata?.externalResources ?? []); - - // Set UI elements in a proper rendering order - const logsSections: JSX.Element[] = []; - for (const key of RENDER_ORDER) { - const values = logsInfo.get(key); - if (values) { - logsSections.push(); - } - } + const logsByPhase = getGroupedLogs(closure.metadata?.externalResources ?? []); return ( @@ -94,7 +77,21 @@ export const MapTaskExecutionsListItem: React.FC
) : null} {/* child/array logs separated by subtasks phase */} - {logsSections} + {RENDER_ORDER.map((phase, id) => { + const logs = logsByPhase.get(phase); + if (!logs) { + return null; + } + const key = `${id}-${phase}`; + return ( + + ); + })} {/* If map task is actively started - show 'started' and 'run time' details */} {taskHasStarted && ( diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx index 16915dac9..ef354206c 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx @@ -5,6 +5,7 @@ import { NonIdealState } from 'components/common/NonIdealState'; import { WaitForData } from 'components/common/WaitForData'; import { NodeExecution, TaskExecution } from 'models/Execution/types'; import { isMapTaskType } from 'models/Task/utils'; +import { TaskExecutionPhase } from 'models/Execution/enums'; import { useTaskExecutions, useTaskExecutionsRefresher } from '../useTaskExecutions'; import { MapTaskExecutionsListItem } from './MapTaskExecutionListItem'; import { TaskExecutionsListItem } from './TaskExecutionsListItem'; @@ -18,11 +19,13 @@ const useStyles = makeStyles((theme: Theme) => ({ interface TaskExecutionsListProps { nodeExecution: NodeExecution; + phase?: TaskExecutionPhase; } export const TaskExecutionsListContent: React.FC<{ taskExecutions: TaskExecution[]; -}> = ({ taskExecutions }) => { + phase?: TaskExecutionPhase; +}> = ({ taskExecutions, phase }) => { const styles = useStyles(); if (!taskExecutions.length) { return ( @@ -45,6 +48,7 @@ export const TaskExecutionsListContent: React.FC<{ key={getUniqueTaskExecutionName(taskExecution)} taskExecution={taskExecution} showAttempts={taskExecutions.length > 1} + selectedPhase={phase} /> ) : ( = ({ nodeExecution }) => { +export const TaskExecutionsList: React.FC = ({ nodeExecution, phase }) => { const taskExecutions = useTaskExecutions(nodeExecution.id); useTaskExecutionsRefresher(nodeExecution, taskExecutions); return ( - + ); }; diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/constants.ts b/packages/zapp/console/src/components/Executions/TaskExecutionsList/constants.ts new file mode 100644 index 000000000..82c26c290 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/constants.ts @@ -0,0 +1,12 @@ +import { TaskExecutionPhase } from 'models/Execution/enums'; + +export const RENDER_ORDER: TaskExecutionPhase[] = [ + TaskExecutionPhase.UNDEFINED, + TaskExecutionPhase.INITIALIZING, + TaskExecutionPhase.WAITING_FOR_RESOURCES, + TaskExecutionPhase.QUEUED, + TaskExecutionPhase.RUNNING, + TaskExecutionPhase.SUCCEEDED, + TaskExecutionPhase.ABORTED, + TaskExecutionPhase.FAILED, +]; diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts b/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts index 356d51b6d..e307f7ec1 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts @@ -1,7 +1,7 @@ +import { LogsByPhase, TaskExecution } from 'models/Execution/types'; import { leftPaddedNumber } from 'common/formatters'; -import { Core, Event } from 'flyteidl'; +import { Event } from 'flyteidl'; import { TaskExecutionPhase } from 'models/Execution/enums'; -import { TaskExecution } from 'models/Execution/types'; /** Generates a unique name for a task execution, suitable for display in a * header and use as a child component key. The name is a combination of task @@ -25,10 +25,8 @@ export function formatRetryAttempt(attempt: number | string | undefined): string return `Attempt ${leftPaddedNumber(parsed + 1, 2)}`; } -export const getGroupedLogs = ( - resources: Event.IExternalResourceInfo[], -): Map => { - const logsInfo = new Map(); +export const getGroupedLogs = (resources: Event.IExternalResourceInfo[]): LogsByPhase => { + const logsByPhase: LogsByPhase = new Map(); // sort output sample [0-2, 0-1, 0, 1, 2], where 0-1 means index = 0 retry = 1 resources.sort((a, b) => { @@ -51,15 +49,15 @@ export const getGroupedLogs = ( continue; } const phase = item.phase ?? TaskExecutionPhase.UNDEFINED; - const currentValue = logsInfo.get(phase); + const currentValue = logsByPhase.get(phase); lastIndex = item.index ?? 0; if (item.logs) { // if there is no log with active url, just create an item with externalId, // for user to understand which array items are in this state const newLogs = item.logs.length > 0 ? item.logs : [{ name: item.externalId }]; - logsInfo.set(phase, currentValue ? [...currentValue, ...newLogs] : [...newLogs]); + logsByPhase.set(phase, currentValue ? [...currentValue, ...newLogs] : [...newLogs]); } } - return logsInfo; + return logsByPhase; }; diff --git a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx index 5ddcc7a57..6b2a6bfe1 100644 --- a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -5,17 +5,17 @@ import ReactFlowGraphComponent from 'components/flytegraph/ReactFlow/ReactFlowGr import { Error } from 'models/Common/types'; import { NonIdealState } from 'components/common/NonIdealState'; import { DataError } from 'components/Errors/DataError'; -import { NodeExecutionsContext } from 'components/Executions/contexts'; import { WaitForQuery } from 'components/common/WaitForQuery'; import { useQuery } from 'react-query'; import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; import { createDebugLogger } from 'common/log'; import { CompiledNode } from 'models/Node/types'; +import { TaskExecutionPhase } from 'models/Execution/enums'; import { transformerWorkflowToDag } from './transformerWorkflowToDag'; export interface WorkflowGraphProps { onNodeSelectionChanged: (selectedNodes: string[]) => void; - selectedNodes?: string[]; + onPhaseSelectionChanged: (phase: TaskExecutionPhase) => void; workflow: Workflow; nodeExecutionsById?: any; } @@ -56,7 +56,7 @@ export interface DynamicWorkflowMapping { dynamicExecutions: any[]; } export const WorkflowGraph: React.FC = (props) => { - const { onNodeSelectionChanged, nodeExecutionsById, workflow } = props; + const { onNodeSelectionChanged, onPhaseSelectionChanged, nodeExecutionsById, workflow } = props; const { dag, staticExecutionIdsMap, error } = workflowToDag(workflow); /** * Note: @@ -112,6 +112,7 @@ export const WorkflowGraph: React.FC = (props) => { nodeExecutionsById={nodeExecutionsById} data={merged} onNodeSelectionChanged={onNodeSelectionChanged} + onPhaseSelectionChanged={onPhaseSelectionChanged} /> ); }; @@ -120,11 +121,9 @@ export const WorkflowGraph: React.FC = (props) => { return ; } else { return ( - - - {renderReactFlowGraph} - - + + {renderReactFlowGraph} + ); } }; diff --git a/packages/zapp/console/src/components/WorkflowGraph/__stories__/WorkflowGraph.stories.tsx b/packages/zapp/console/src/components/WorkflowGraph/__stories__/WorkflowGraph.stories.tsx index cd67dd13d..ecedabf25 100644 --- a/packages/zapp/console/src/components/WorkflowGraph/__stories__/WorkflowGraph.stories.tsx +++ b/packages/zapp/console/src/components/WorkflowGraph/__stories__/WorkflowGraph.stories.tsx @@ -22,6 +22,7 @@ const workflow: Workflow = { }; const onNodeSelectionChanged = action('nodeSelected'); +const onMapTaskSelectionChanged = action('mapTaskSelected'); const cache = createCache(); const taskTemplates = extractTaskTemplates(workflow); @@ -46,5 +47,9 @@ stories.addDecorator((story) => ( )); stories.add('TaskNodeRenderer', () => ( - + )); diff --git a/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx b/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx new file mode 100644 index 000000000..5d542d914 --- /dev/null +++ b/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx @@ -0,0 +1,39 @@ +import { act, render, screen, waitFor } from '@testing-library/react'; +import * as React from 'react'; +import { createTestQueryClient } from 'test/utils'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { WorkflowGraph } from '../WorkflowGraph'; +import { workflow } from './workflow.mock'; +import { nodeExecutionsById } from './nodeExecutionsById.mock'; + +jest.mock('../../flytegraph/ReactFlow/ReactFlowWrapper.tsx', () => ({ + ReactFlowWrapper: jest.fn(({ children }) => ( +
{children}
+ )), +})); + +describe('WorkflowGraph', () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = createTestQueryClient(); + }); + + it('should render map task logs when all props were provided', async () => { + act(() => { + render( + + + , + ); + }); + + const graph = await waitFor(() => screen.getByTestId('react-flow-wrapper')); + expect(graph).toBeInTheDocument(); + }); +}); diff --git a/packages/zapp/console/src/components/WorkflowGraph/test/nodeExecutionsById.mock.ts b/packages/zapp/console/src/components/WorkflowGraph/test/nodeExecutionsById.mock.ts new file mode 100644 index 000000000..fb71d920c --- /dev/null +++ b/packages/zapp/console/src/components/WorkflowGraph/test/nodeExecutionsById.mock.ts @@ -0,0 +1,150 @@ +export const nodeExecutionsById = { + n0: { + id: { + nodeId: 'n0', + executionId: { + project: 'flytesnacks', + domain: 'development', + name: 'fc027ce9fe4cf4f5eba8', + }, + }, + inputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-fc027ce9fe4cf4f5eba8/n0/data/inputs.pb', + closure: { + phase: 3, + startedAt: { + seconds: { + low: 1649888546, + high: 0, + unsigned: false, + }, + nanos: 773100279, + }, + duration: { + seconds: { + low: 22, + high: 0, + unsigned: false, + }, + nanos: 800572640, + }, + createdAt: { + seconds: { + low: 1649888546, + high: 0, + unsigned: false, + }, + nanos: 697168683, + }, + updatedAt: { + seconds: { + low: 1649888569, + high: 0, + unsigned: false, + }, + nanos: 573672640, + }, + outputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-fc027ce9fe4cf4f5eba8/n0/data/0/outputs.pb', + }, + metadata: { + specNodeId: 'n0', + }, + scopedId: 'n0', + logsByPhase: new Map([ + [ + 3, + [ + { + logs: [ + { + uri: 'http://localhost:30082/#!/log/flytesnacks-development/fc027ce9fe4cf4f5eba8-n0-0-0/pod?namespace=flytesnacks-development', + name: 'Kubernetes Logs #0-0', + messageFormat: 2, + }, + ], + externalId: 'fc027ce9fe4cf4f5eba8-n0-0-0', + phase: 3, + }, + { + logs: [ + { + uri: 'http://localhost:30082/#!/log/flytesnacks-development/fc027ce9fe4cf4f5eba8-n0-0-1/pod?namespace=flytesnacks-development', + name: 'Kubernetes Logs #0-1', + messageFormat: 2, + }, + ], + externalId: 'fc027ce9fe4cf4f5eba8-n0-0-1', + index: 1, + phase: 3, + }, + { + logs: [ + { + uri: 'http://localhost:30082/#!/log/flytesnacks-development/fc027ce9fe4cf4f5eba8-n0-0-2/pod?namespace=flytesnacks-development', + name: 'Kubernetes Logs #0-2', + messageFormat: 2, + }, + ], + externalId: 'fc027ce9fe4cf4f5eba8-n0-0-2', + index: 2, + phase: 3, + }, + ], + ], + ]), + }, + n1: { + id: { + nodeId: 'n1', + executionId: { + project: 'flytesnacks', + domain: 'development', + name: 'fc027ce9fe4cf4f5eba8', + }, + }, + inputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-fc027ce9fe4cf4f5eba8/n1/data/inputs.pb', + closure: { + phase: 3, + startedAt: { + seconds: { + low: 1649888569, + high: 0, + unsigned: false, + }, + nanos: 782695018, + }, + duration: { + seconds: { + low: 9, + high: 0, + unsigned: false, + }, + nanos: 811268323, + }, + createdAt: { + seconds: { + low: 1649888569, + high: 0, + unsigned: false, + }, + nanos: 685160925, + }, + updatedAt: { + seconds: { + low: 1649888579, + high: 0, + unsigned: false, + }, + nanos: 593963323, + }, + outputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-fc027ce9fe4cf4f5eba8/n1/data/0/outputs.pb', + }, + metadata: { + specNodeId: 'n1', + }, + scopedId: 'n1', + }, +}; diff --git a/packages/zapp/console/src/components/WorkflowGraph/test/workflow.mock.ts b/packages/zapp/console/src/components/WorkflowGraph/test/workflow.mock.ts new file mode 100644 index 000000000..6813a9a5b --- /dev/null +++ b/packages/zapp/console/src/components/WorkflowGraph/test/workflow.mock.ts @@ -0,0 +1,370 @@ +import { Workflow } from 'models/Workflow/types'; +import { long } from 'test/utils'; + +export const workflow: Workflow = { + id: { + resourceType: 2, + project: 'flytesnacks', + domain: 'development', + name: 'core.control_flow.map_task.my_map_workflow', + version: 'MT76cyUZeeYX-hA6qeIotA==', + }, + closure: { + compiledWorkflow: { + subWorkflows: [], + tasks: [ + { + template: { + config: {}, + id: { + resourceType: 1, + project: 'flytesnacks', + domain: 'development', + name: 'core.control_flow.map_task.coalesce', + version: 'MT76cyUZeeYX-hA6qeIotA==', + }, + type: 'python-task', + metadata: { + runtime: { + type: 1, + version: '0.0.0+develop', + flavor: 'python', + }, + retries: {}, + }, + interface: { + inputs: { + variables: { + b: { + type: { + collectionType: { + simple: 3, + }, + }, + description: 'b', + }, + }, + }, + outputs: { + variables: { + o0: { + type: { + simple: 3, + }, + description: 'o0', + }, + }, + }, + }, + container: { + command: [], + args: [ + 'pyflyte-fast-execute', + '--additional-distribution', + 's3://flyte-demo/zn/flytesnacks/development/MT76cyUZeeYX+hA6qeIotA==/scriptmode.tar.gz', + '--dest-dir', + '/root', + '--', + 'pyflyte-execute', + '--inputs', + '{{.input}}', + '--output-prefix', + '{{.outputPrefix}}', + '--raw-output-data-prefix', + '{{.rawOutputDataPrefix}}', + '--checkpoint-path', + '{{.checkpointOutputPrefix}}', + '--prev-checkpoint', + '{{.prevCheckpointPrefix}}', + '--resolver', + 'flytekit.core.python_auto_container.default_task_resolver', + '--', + 'task-module', + 'core.control_flow.map_task', + 'task-name', + 'coalesce', + ], + env: [], + config: [], + ports: [], + image: 'ghcr.io/flyteorg/flytekit:py3.9-latest', + resources: { + requests: [], + limits: [], + }, + }, + }, + }, + { + template: { + config: {}, + id: { + resourceType: 1, + project: 'flytesnacks', + domain: 'development', + name: 'core.control_flow.map_task.mapper_a_mappable_task_0', + version: 'MT76cyUZeeYX-hA6qeIotA==', + }, + type: 'container_array', + metadata: { + runtime: { + type: 1, + version: '0.0.0+develop', + flavor: 'python', + }, + retries: {}, + }, + interface: { + inputs: { + variables: { + a: { + type: { + collectionType: { + simple: 1, + }, + }, + description: 'a', + }, + }, + }, + outputs: { + variables: { + o0: { + type: { + collectionType: { + simple: 3, + }, + }, + description: 'o0', + }, + }, + }, + }, + custom: { + fields: { + minSuccessRatio: { + numberValue: 1, + }, + }, + }, + taskTypeVersion: 1, + container: { + command: [], + args: [ + 'pyflyte-fast-execute', + '--additional-distribution', + 's3://flyte-demo/zn/flytesnacks/development/MT76cyUZeeYX+hA6qeIotA==/scriptmode.tar.gz', + '--dest-dir', + '/root', + '--', + 'pyflyte-map-execute', + '--inputs', + '{{.input}}', + '--output-prefix', + '{{.outputPrefix}}', + '--raw-output-data-prefix', + '{{.rawOutputDataPrefix}}', + '--checkpoint-path', + '{{.checkpointOutputPrefix}}', + '--prev-checkpoint', + '{{.prevCheckpointPrefix}}', + '--resolver', + 'flytekit.core.python_auto_container.default_task_resolver', + '--', + 'task-module', + 'core.control_flow.map_task', + 'task-name', + 'a_mappable_task', + ], + env: [], + config: [], + ports: [], + image: 'ghcr.io/flyteorg/flytekit:py3.9-latest', + resources: { + requests: [], + limits: [], + }, + }, + }, + }, + ], + primary: { + template: { + nodes: [ + { + inputs: [], + upstreamNodeIds: [], + outputAliases: [], + id: 'start-node', + }, + { + inputs: [ + { + var: 'o0', + binding: { + promise: { + nodeId: 'n1', + var: 'o0', + }, + }, + }, + ], + upstreamNodeIds: [], + outputAliases: [], + id: 'end-node', + }, + { + inputs: [ + { + var: 'a', + binding: { + promise: { + nodeId: 'start-node', + var: 'a', + }, + }, + }, + ], + upstreamNodeIds: [], + outputAliases: [], + id: 'n0', + metadata: { + name: 'mapper_a_mappable_task_0', + retries: { + retries: 1, + }, + }, + taskNode: { + overrides: { + resources: { + requests: [ + { + name: 3, + value: '300Mi', + }, + ], + limits: [ + { + name: 3, + value: '500Mi', + }, + ], + }, + }, + referenceId: { + resourceType: 1, + project: 'flytesnacks', + domain: 'development', + name: 'core.control_flow.map_task.mapper_a_mappable_task_0', + version: 'MT76cyUZeeYX-hA6qeIotA==', + }, + }, + }, + { + inputs: [ + { + var: 'b', + binding: { + promise: { + nodeId: 'n0', + var: 'o0', + }, + }, + }, + ], + upstreamNodeIds: ['n0'], + outputAliases: [], + id: 'n1', + metadata: { + name: 'coalesce', + retries: {}, + }, + taskNode: { + overrides: {}, + referenceId: { + resourceType: 1, + project: 'flytesnacks', + domain: 'development', + name: 'core.control_flow.map_task.coalesce', + version: 'MT76cyUZeeYX-hA6qeIotA==', + }, + }, + }, + ], + outputs: [ + { + var: 'o0', + binding: { + promise: { + nodeId: 'n1', + var: 'o0', + }, + }, + }, + ], + id: { + resourceType: 2, + project: 'flytesnacks', + domain: 'development', + name: 'core.control_flow.map_task.my_map_workflow', + version: 'MT76cyUZeeYX-hA6qeIotA==', + }, + metadata: {}, + interface: { + inputs: { + variables: { + a: { + type: { + collectionType: { + simple: 1, + }, + }, + description: 'a', + }, + }, + }, + outputs: { + variables: { + o0: { + type: { + simple: 3, + }, + description: 'o0', + }, + }, + }, + }, + metadataDefaults: {}, + }, + connections: { + downstream: { + 'start-node': { + ids: ['n0'], + }, + n0: { + ids: ['n1'], + }, + n1: { + ids: ['end-node'], + }, + }, + upstream: { + 'end-node': { + ids: ['n1'], + }, + n0: { + ids: ['start-node'], + }, + n1: { + ids: ['n0'], + }, + }, + }, + }, + }, + createdAt: { + seconds: long(0), + nanos: 343264000, + }, + }, +}; diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx index 219cd4d32..399a92309 100644 --- a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx @@ -14,15 +14,15 @@ const taskLogs = [ describe('MapTaskStatusInfo', () => { it('Phase and amount of links rendered correctly', async () => { - const status = TaskExecutionPhase.RUNNING; - const phaseData = getTaskExecutionPhaseConstants(status); + const phase = TaskExecutionPhase.RUNNING; + const phaseData = getTaskExecutionPhaseConstants(phase); const { queryByText, getByTitle } = render( - , + , ); expect(queryByText(phaseData.text)).toBeInTheDocument(); - expect(queryByText(`x${taskLogs.length}`)).toBeInTheDocument(); + expect(queryByText(`×${taskLogs.length}`)).toBeInTheDocument(); expect(queryByText('Logs')).not.toBeInTheDocument(); // Expand item - see logs section @@ -34,15 +34,15 @@ describe('MapTaskStatusInfo', () => { }); it('Phase with no links show proper texts when opened', () => { - const status = TaskExecutionPhase.ABORTED; - const phaseData = getTaskExecutionPhaseConstants(status); + const phase = TaskExecutionPhase.ABORTED; + const phaseData = getTaskExecutionPhaseConstants(phase); const { queryByText } = render( - , + , ); expect(queryByText(phaseData.text)).toBeInTheDocument(); - expect(queryByText(`x0`)).toBeInTheDocument(); + expect(queryByText(`×0`)).toBeInTheDocument(); expect(queryByText(noLogsFoundString)).toBeInTheDocument(); }); }); diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx index 7ed9fca1a..cef9b2be6 100644 --- a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx @@ -37,19 +37,19 @@ const useStyles = makeStyles((_theme: Theme) => ({ interface MapTaskStatusInfoProps { taskLogs: Core.ITaskLog[]; - status: TaskExecutionPhase; - expanded: boolean; + phase: TaskExecutionPhase; + isExpanded: boolean; } -export const MapTaskStatusInfo = (props: MapTaskStatusInfoProps) => { - const [expanded, setExpanded] = useState(props.expanded); +export const MapTaskStatusInfo = ({ taskLogs, phase, isExpanded }: MapTaskStatusInfoProps) => { + const [expanded, setExpanded] = useState(isExpanded); const styles = useStyles(); const toggleExpanded = () => { setExpanded(!expanded); }; - const phaseData = getTaskExecutionPhaseConstants(props.status); + const phaseData = getTaskExecutionPhaseConstants(phase); return (
@@ -58,11 +58,11 @@ export const MapTaskStatusInfo = (props: MapTaskStatusInfoProps) => { {phaseData.text} - {`x${props.taskLogs.length}`} + {`×${taskLogs.length}`}
{expanded && (
- +
)}
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index a1dd4fb8f..a3c4079f5 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -17,6 +17,24 @@ const nodeExecutionStatusChanged = (previous, nodeExecutionsById) => { return false; }; +const nodeExecutionLogsChanged = (previous, nodeExecutionsById) => { + for (const exe in nodeExecutionsById) { + const oldLogs = previous[exe]?.logsByPhase ?? new Map(); + const newLogs = nodeExecutionsById[exe]?.logsByPhase ?? new Map(); + if (oldLogs.size !== newLogs.size) { + return true; + } + for (const phase in newLogs) { + const oldNumOfLogs = oldLogs.get(phase)?.length ?? 0; + const newNumOfLogs = newLogs.get(phase)?.length ?? 0; + if (oldNumOfLogs !== newNumOfLogs) { + return true; + } + } + } + return false; +}; + const graphNodeCountChanged = (previous, data) => { if (previous.nodes.length !== data.nodes.length) { return true; @@ -26,13 +44,20 @@ const graphNodeCountChanged = (previous, data) => { }; const ReactFlowGraphComponent = (props) => { - const { data, onNodeSelectionChanged, nodeExecutionsById, dynamicWorkflows } = props; + const { + data, + onNodeSelectionChanged, + onPhaseSelectionChanged, + nodeExecutionsById, + dynamicWorkflows, + } = props; const [state, setState] = useState({ - data: data, - dynamicWorkflows: dynamicWorkflows, + data, + dynamicWorkflows, currentNestedView: {}, - nodeExecutionsById: nodeExecutionsById, - onNodeSelectionChanged: onNodeSelectionChanged, + nodeExecutionsById, + onNodeSelectionChanged, + onPhaseSelectionChanged, rfGraphJson: null, }); @@ -50,7 +75,7 @@ const ReactFlowGraphComponent = (props) => { const onRemoveNestedView = (viewParent, viewIndex) => { const currentNestedView: any = { ...state.currentNestedView }; currentNestedView[viewParent] = currentNestedView[viewParent]?.filter( - (item, i) => i <= viewIndex, + (_item, i) => i <= viewIndex, ); if (currentNestedView[viewParent]?.length < 1) { delete currentNestedView[viewParent]; @@ -66,8 +91,9 @@ const ReactFlowGraphComponent = (props) => { root: state.data, nodeExecutionsById: state.nodeExecutionsById, onNodeSelectionChanged: state.onNodeSelectionChanged, - onAddNestedView: onAddNestedView, - onRemoveNestedView: onRemoveNestedView, + onPhaseSelectionChanged: state.onPhaseSelectionChanged, + onAddNestedView, + onRemoveNestedView, currentNestedView: state.currentNestedView, maxRenderDepth: 1, } as ConvertDagProps); @@ -79,7 +105,7 @@ const ReactFlowGraphComponent = (props) => { ...state, rfGraphJson: newRFGraphData, })); - }, [state.currentNestedView]); + }, [state.currentNestedView, state.nodeExecutionsById]); useEffect(() => { if (graphNodeCountChanged(state.data, data)) { @@ -88,10 +114,13 @@ const ReactFlowGraphComponent = (props) => { data: data, })); } - if (nodeExecutionStatusChanged(state.nodeExecutionsById, nodeExecutionsById)) { + if ( + nodeExecutionStatusChanged(state.nodeExecutionsById, nodeExecutionsById) || + nodeExecutionLogsChanged(state.nodeExecutionsById, nodeExecutionsById) + ) { setState((state) => ({ ...state, - nodeExecutionsById: nodeExecutionsById, + nodeExecutionsById, })); } }, [data, nodeExecutionsById]); @@ -99,9 +128,10 @@ const ReactFlowGraphComponent = (props) => { useEffect(() => { setState((state) => ({ ...state, - onNodeSelectionChanged: onNodeSelectionChanged, + onNodeSelectionChanged, + onPhaseSelectionChanged, })); - }, [onNodeSelectionChanged]); + }, [onNodeSelectionChanged, onPhaseSelectionChanged]); const backgroundStyle = getRFBackground().nested; @@ -118,7 +148,7 @@ const ReactFlowGraphComponent = (props) => { backgroundStyle, rfGraphJson: state.rfGraphJson, type: RFGraphTypes.main, - nodeExecutionsById: nodeExecutionsById, + nodeExecutionsById, currentNestedView: state.currentNestedView, }; return ( diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index ca755503e..791bcd8b3 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -3,14 +3,17 @@ import { useState, useEffect } from 'react'; import { Handle, Position } from 'react-flow-renderer'; import { dTypes } from 'models/Graph/types'; import CachedOutlined from '@material-ui/icons/CachedOutlined'; -import { CatalogCacheStatus } from 'models/Execution/enums'; +import { CatalogCacheStatus, TaskExecutionPhase } from 'models/Execution/enums'; import { PublishedWithChangesOutlined } from 'components/common/PublishedWithChanges'; +import { RENDER_ORDER } from 'components/Executions/TaskExecutionsList/constants'; +import { whiteColor } from 'components/Theme/constants'; import { COLOR_TASK_TYPE, COLOR_GRAPH_BACKGROUND, getGraphHandleStyle, getGraphNodeStyle, getNestedContainerStyle, + getStatusColor, } from './utils'; import { RFHandleProps } from './types'; @@ -199,6 +202,55 @@ export const ReactFlowStaticNode = ({ data }: any) => { ); }; +/** + * Component that renders a map task item within the mapped node. + * @param props.numberOfTasks number of tasks of certain completion phase + * @param props.color string value of color of the block + * @param props.phase phase of the current map task item + * @param props.onPhaseSelectionChanged callback from the parent component + */ + +interface TaskPhaseItemProps { + numberOfTasks: number; + color: string; + phase: TaskExecutionPhase; + setSelectedPhase: (phase: TaskExecutionPhase) => void; + setSelectedNode: (val: boolean) => void; +} + +const TaskPhaseItem = ({ + numberOfTasks, + color, + phase, + setSelectedPhase, + setSelectedNode, +}: TaskPhaseItemProps) => { + const taskPhaseStyles: React.CSSProperties = { + borderRadius: '2px', + backgroundColor: color, + color: whiteColor, + margin: '0 1px', + padding: '0 2px', + fontSize: '8px', + lineHeight: '14px', + minWidth: '14px', + textAlign: 'center', + cursor: 'pointer', + }; + + const handleMapTaskClick = (e) => { + e.stopPropagation(); + setSelectedNode(true); + setSelectedPhase(phase); + }; + + return ( +
+ ×{numberOfTasks} +
+ ); +}; + /** * Custom component used by ReactFlow. Renders a label (text) * and any edge handles. @@ -208,14 +260,18 @@ export const ReactFlowStaticNode = ({ data }: any) => { export const ReactFlowCustomTaskNode = ({ data }: any) => { const styles = getGraphNodeStyle(data.nodeType, data.nodeExecutionStatus); const onNodeSelectionChanged = data.onNodeSelectionChanged; - const [selectedNode, setSelectedNode] = useState(false); + const onPhaseSelectionChanged = data.onPhaseSelectionChanged; + const [selectedNode, setSelectedNode] = useState(false); + const [selectedPhase, setSelectedPhase] = useState(undefined); useEffect(() => { if (selectedNode === true) { onNodeSelectionChanged(selectedNode); setSelectedNode(false); + onPhaseSelectionChanged(selectedPhase); + setSelectedPhase(selectedPhase); } - }, [selectedNode, onNodeSelectionChanged]); + }, [selectedNode, onNodeSelectionChanged, selectedPhase, onPhaseSelectionChanged]); const taskContainerStyle: React.CSSProperties = { position: 'absolute', @@ -229,6 +285,20 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { padding: '.1rem .2rem', fontSize: '.3rem', }; + const mapTaskContainerStyle: React.CSSProperties = { + position: 'absolute', + top: '-.82rem', + zIndex: 0, + right: '.15rem', + }; + const taskNameStyle: React.CSSProperties = { + backgroundColor: getStatusColor(data.nodeExecutionStatus), + color: 'white', + padding: '.1rem .2rem', + fontSize: '.4rem', + borderRadius: '.15rem', + }; + const cacheIconStyles: React.CSSProperties = { width: '8px', height: '8px', @@ -236,9 +306,13 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { marginTop: '1px', color: COLOR_GRAPH_BACKGROUND, }; + const mapTaskWrapper: React.CSSProperties = { + display: 'flex', + }; - const handleClick = (_e) => { + const handleNodeClick = (_e) => { setSelectedNode(true); + setSelectedPhase(undefined); }; const renderTaskType = () => { @@ -249,6 +323,14 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { ); }; + const renderTaskName = () => { + return ( +
+
{data.text}
+
+ ); + }; + const renderCacheIcon = (cacheStatus) => { switch (cacheStatus) { case CatalogCacheStatus.CACHE_HIT: @@ -260,11 +342,35 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { } }; + const renderTaskPhases = (logsByPhase) => { + return ( +
+ {RENDER_ORDER.map((phase, id) => { + if (!logsByPhase.has(phase)) { + return null; + } + + const key = `${id}-${phase}`; + return ( + + ); + })} +
+ ); + }; + return ( -
- {data.taskType ? renderTaskType() : null} +
+ {data.nodeLogsByPhase ? renderTaskName() : data.taskType ? renderTaskType() : null}
- {data.text} + {data.nodeLogsByPhase ? renderTaskPhases(data.nodeLogsByPhase) : data.text} {renderCacheIcon(data.cacheStatus)}
{renderDefaultHandles( diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index d3b11a33b..0f3d85a6e 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -1,7 +1,8 @@ import { dEdge, dNode, dTypes } from 'models/Graph/types'; import { Edge, Node, Position } from 'react-flow-renderer'; -import { CatalogCacheStatus, NodeExecutionPhase } from 'models/Execution/enums'; +import { CatalogCacheStatus, NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums'; import { createDebugLogger } from 'common/log'; +import { LogsByPhase } from 'models/Execution/types'; import { ReactFlowGraphConfig } from './utils'; import { ConvertDagProps } from './types'; @@ -45,6 +46,7 @@ interface BuildDataProps { node: dNode; nodeExecutionsById: any; onNodeSelectionChanged: any; + onPhaseSelectionChanged: any; onAddNestedView: any; onRemoveNestedView: any; rootParentNode: dNode; @@ -55,6 +57,7 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { node, nodeExecutionsById, onNodeSelectionChanged, + onPhaseSelectionChanged, onAddNestedView, onRemoveNestedView, rootParentNode, @@ -77,9 +80,10 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { }; const nodeExecutionStatus = mapNodeExecutionStatus(); - // nodeExecutionsById null check is required as on first render it can be undefined + const nodeLogsByPhase: LogsByPhase = nodeExecutionsById?.[node.scopedId]?.logsByPhase; + const cacheStatus: CatalogCacheStatus = - nodeExecutionsById?.[scopedId]?.closure.taskNodeMetadata?.cacheStatus ?? + nodeExecutionsById?.[scopedId]?.closure.taskNodeMetadata?.cacheStatus ?? CatalogCacheStatus.CACHE_DISABLED; const dataProps = { @@ -89,12 +93,18 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { nodeType, scopedId, taskType, + nodeLogsByPhase, cacheStatus, onNodeSelectionChanged: () => { if (onNodeSelectionChanged) { onNodeSelectionChanged([scopedId]); } }, + onPhaseSelectionChanged: (phase: TaskExecutionPhase) => { + if (onPhaseSelectionChanged) { + onPhaseSelectionChanged(phase); + } + }, onAddNestedView: () => { onAddNestedView({ parent: rootParentNode.scopedId, @@ -188,6 +198,7 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { const { nodeExecutionsById, onNodeSelectionChanged, + onPhaseSelectionChanged, onAddNestedView, onRemoveNestedView, currentNestedView, @@ -196,6 +207,7 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { const nodeDataProps = { nodeExecutionsById, onNodeSelectionChanged, + onPhaseSelectionChanged, onAddNestedView, onRemoveNestedView, currentNestedView, @@ -215,54 +227,56 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { const parse = (props: ParseProps) => { const { contextNode, contextParent, rootParentNode, nodeDataProps } = props; let context: ReactFlowGraph | null = null; - contextNode.nodes.filter(n => !!n).map((node: dNode) => { - /* Case: node has children => recurse */ - if (nodeHasChildren(node)) { + contextNode.nodes + .filter((n) => !!n) + .map((node: dNode) => { + /* Case: node has children => recurse */ + if (nodeHasChildren(node)) { + if (rootParentNode) { + parse({ + contextNode: node, + contextParent: node, + rootParentNode: rootParentNode, + nodeDataProps: nodeDataProps, + }); + } else { + parse({ + contextNode: node, + contextParent: node, + rootParentNode: node, + nodeDataProps: nodeDataProps, + }); + } + } + if (rootParentNode) { - parse({ - contextNode: node, - contextParent: node, + const rootParentId = rootParentNode.scopedId; + const contextParentId = contextParent?.scopedId; + rootParentMap[rootParentId] = rootParentMap[rootParentId] || {}; + rootParentMap[rootParentId][contextParentId] = rootParentMap[rootParentId][ + contextParentId + ] || { + nodes: {}, + edges: {}, + }; + context = rootParentMap[rootParentId][contextParentId] as ReactFlowGraph; + const reactFlowNode = buildReactFlowNode({ + node: node, + dataProps: nodeDataProps, rootParentNode: rootParentNode, - nodeDataProps: nodeDataProps, + parentNode: contextParent, + typeOverride: isStaticGraph === true ? dTypes.staticNode : undefined, }); + context.nodes[reactFlowNode.id] = reactFlowNode; } else { - parse({ - contextNode: node, - contextParent: node, - rootParentNode: node, - nodeDataProps: nodeDataProps, + const reactFlowNode = buildReactFlowNode({ + node: node, + dataProps: nodeDataProps, + typeOverride: isStaticGraph === true ? dTypes.staticNode : undefined, }); + root.nodes[reactFlowNode.id] = reactFlowNode; } - } - - if (rootParentNode) { - const rootParentId = rootParentNode.scopedId; - const contextParentId = contextParent?.scopedId; - rootParentMap[rootParentId] = rootParentMap[rootParentId] || {}; - rootParentMap[rootParentId][contextParentId] = rootParentMap[rootParentId][ - contextParentId - ] || { - nodes: {}, - edges: {}, - }; - context = rootParentMap[rootParentId][contextParentId] as ReactFlowGraph; - const reactFlowNode = buildReactFlowNode({ - node: node, - dataProps: nodeDataProps, - rootParentNode: rootParentNode, - parentNode: contextParent, - typeOverride: isStaticGraph === true ? dTypes.staticNode : undefined, - }); - context.nodes[reactFlowNode.id] = reactFlowNode; - } else { - const reactFlowNode = buildReactFlowNode({ - node: node, - dataProps: nodeDataProps, - typeOverride: isStaticGraph === true ? dTypes.staticNode : undefined, - }); - root.nodes[reactFlowNode.id] = reactFlowNode; - } - }); + }); contextNode.edges.map((edge: dEdge) => { const reactFlowEdge = buildReactFlowEdge({ edge, rootParentNode }); if (rootParentNode && context) { diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx index 025c8acdd..ac74ef775 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { NodeExecutionPhase } from 'models/Execution/enums'; +import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums'; import { dTypes } from 'models/Graph/types'; import { CSSProperties } from 'react'; import { RFBackgroundProps } from './types'; @@ -103,7 +103,9 @@ export const nodePhaseColorMapping = { * @param nodeExecutionStatus * @returns */ -export const getStatusColor = (nodeExecutionStatus: NodeExecutionPhase): string => { +export const getStatusColor = ( + nodeExecutionStatus: NodeExecutionPhase | TaskExecutionPhase, +): string => { if (nodePhaseColorMapping[nodeExecutionStatus]) { return nodePhaseColorMapping[nodeExecutionStatus].color; } else { diff --git a/packages/zapp/console/src/models/Execution/types.ts b/packages/zapp/console/src/models/Execution/types.ts index 4628d266b..96998afbc 100644 --- a/packages/zapp/console/src/models/Execution/types.ts +++ b/packages/zapp/console/src/models/Execution/types.ts @@ -1,4 +1,4 @@ -import { Admin, Core, Protobuf } from 'flyteidl'; +import { Admin, Core, Event, Protobuf } from 'flyteidl'; import { Identifier, LiteralMap, LiteralMapBlob, TaskLog, UrlBlob } from 'models/Common/types'; import { CompiledWorkflow } from 'models/Workflow/types'; import { @@ -10,6 +10,8 @@ import { export type WorkflowExecutionIdentifier = RequiredNonNullable; export type ExecutionError = RequiredNonNullable; +export type ExternalResource = Event.IExternalResourceInfo; +export type LogsByPhase = Map; export interface BaseExecutionClosure { createdAt: Protobuf.ITimestamp; @@ -124,6 +126,7 @@ export interface TaskExecutionClosure extends Admin.ITaskExecutionClosure { outputUri: string; phase: TaskExecutionPhase; startedAt?: Protobuf.ITimestamp; + eventVersion?: number; } /** Execution data */