From 747b10047ef65fa5ff7abfa199720880ef588fba Mon Sep 17 00:00:00 2001 From: olga-union <101579322+olga-union@users.noreply.github.com> Date: Wed, 18 May 2022 15:47:05 -0500 Subject: [PATCH] [Mapping][TaskInfo] V.2 - Update Task details to allow check information for child task execution (#467) --- .../ExecutionMetadataExtra.tsx | 14 ++- .../ExecutionWorkflowGraph.tsx | 17 +++- .../NodeExecutionDetailsPanelContent.tsx | 33 +++---- .../NodeExecutionTabs/index.tsx | 25 ++++-- .../NodeExecutionTabs/test/index.test.tsx | 21 +++-- .../Executions/ExecutionStatusBadge.tsx | 2 +- .../MapTaskExecutionDetails.tsx | 45 ++++++++++ .../MapTaskExecutionListItem.tsx | 31 ++++--- .../TaskExecutionDetails.tsx | 24 +++--- .../TaskExecutionLogsCard.tsx | 80 +++++++++++++++++ .../TaskExecutionsList/TaskExecutionsList.tsx | 19 ++++- .../TaskExecutionsListItem.tsx | 65 +++----------- .../test/MapTaskExecutionDetails.test.tsx | 32 +++++++ .../test/TaskExecutionDetails.test.tsx | 52 ++++++++++++ .../test/TaskExecutionLogsCard.test.tsx | 47 ++++++++++ .../test/TaskExecutionsList.test.tsx | 2 +- .../test/TaskExecutionsListItem.test.tsx | 17 ++++ .../TaskExecutionsList/test/utils.spec.ts | 85 ++++++++++++++++++- .../Executions/TaskExecutionsList/utils.ts | 63 +++++++++++++- .../WorkflowGraph/WorkflowGraph.tsx | 13 ++- .../MapTaskStatusInfo.stories.tsx | 16 ++-- .../MapTaskStatusInfo.tsx | 32 +++++-- .../MapTaskExecutionsList/TaskNameList.tsx | 62 ++++++++++++++ .../{ => test}/MapTaskStatusInfo.test.tsx | 26 +++++- .../test/TaskNameList.test.tsx | 59 +++++++++++++ .../components/common/PanelSection/index.tsx | 2 +- .../src/components/common/SectionHeader.tsx | 5 +- .../ReactFlow/ReactFlowGraphComponent.tsx | 9 +- .../ReactFlow/customNodeComponents.tsx | 10 ++- .../ReactFlow/transformDAGToReactFlowV2.tsx | 7 +- .../components/flytegraph/ReactFlow/utils.tsx | 4 +- .../console/src/models/Execution/types.ts | 3 + 32 files changed, 759 insertions(+), 163 deletions(-) create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionDetails.tsx create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionLogsCard.tsx create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/test/MapTaskExecutionDetails.test.tsx create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionDetails.test.tsx create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionLogsCard.test.tsx create mode 100644 packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsListItem.test.tsx create mode 100644 packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx rename packages/zapp/console/src/components/common/MapTaskExecutionsList/{ => test}/MapTaskStatusInfo.test.tsx (64%) create mode 100644 packages/zapp/console/src/components/common/MapTaskExecutionsList/test/TaskNameList.test.tsx diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx index 6d2d847b8..9c0d72054 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx @@ -9,14 +9,12 @@ import { LaunchPlanSpec } from 'models/Launch/types'; import { dashedValueString } from 'common/constants'; import { ExecutionMetadataLabels } from './constants'; -const useStyles = makeStyles((theme: Theme) => { - return { - detailItem: { - flexShrink: 0, - marginLeft: theme.spacing(4), - }, - }; -}); +const useStyles = makeStyles((theme: Theme) => ({ + detailItem: { + flexShrink: 0, + marginLeft: theme.spacing(4), + }, +})); interface DetailItem { className?: string; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx index 379d850ce..4d1502028 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx @@ -9,7 +9,7 @@ import { ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/t 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 { useEffect, useMemo, useState } from 'react'; import { useQuery, useQueryClient } from 'react-query'; import { NodeExecutionsContext } from '../contexts'; import { getGroupedLogs } from '../TaskExecutionsList/utils'; @@ -72,14 +72,25 @@ export const ExecutionWorkflowGraph: React.FC = ({ } : null; - const onCloseDetailsPanel = () => setSelectedNodes([]); + const onCloseDetailsPanel = () => { + setSelectedPhase(undefined); + setIsDetailsTabClosed(true); + setSelectedNodes([]); + }; const [selectedPhase, setSelectedPhase] = useState(undefined); + const [isDetailsTabClosed, setIsDetailsTabClosed] = useState(!selectedExecution); + + useEffect(() => { + setIsDetailsTabClosed(!selectedExecution); + }, [selectedExecution]); const renderGraph = (workflow: Workflow) => ( @@ -92,7 +103,7 @@ export const ExecutionWorkflowGraph: React.FC = ({ {renderGraph} - + {selectedExecution && ( (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 [selectedTaskExecution, setSelectedTaskExecution] = useState(null); const isMounted = useRef(false); useEffect(() => { @@ -267,6 +272,10 @@ export const NodeExecutionDetailsPanelContent: React.FC { + setSelectedTaskExecution(null); + }, [nodeExecutionId, phase]); + const nodeExecution = nodeExecutionQuery.data; const getWorkflowDag = async () => { @@ -297,24 +306,17 @@ export const NodeExecutionDetailsPanelContent: React.FC { - setShouldShowTaskDetails(false); + setSelectedTaskExecution(null); }; 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 mapTaskHeader = `${selectedTaskExecution?.taskIndex} of ${nodeExecutionId.nodeId}`; + const header = selectedTaskExecution ? mapTaskHeader : nodeExecutionId.nodeId; return (
- {shouldShowTaskDetails && ( + {!!selectedTaskExecution && ( @@ -326,7 +328,7 @@ export const NodeExecutionDetailsPanelContent: React.FC ); - }, [nodeExecutionId, shouldShowTaskDetails]); + }, [nodeExecutionId, selectedTaskExecution]); const isRunningPhase = useMemo(() => { return ( @@ -370,9 +372,10 @@ export const NodeExecutionDetailsPanelContent: React.FC ) : null; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx index 0fceb1331..91d6e119d 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx @@ -1,13 +1,14 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Tab, Tabs } from '@material-ui/core'; -import { NodeExecution } from 'models/Execution/types'; +import { MapTaskExecution, NodeExecution } from 'models/Execution/types'; import { TaskTemplate } from 'models/Task/types'; import { useTabState } from 'components/hooks/useTabState'; import { PanelSection } from 'components/common/PanelSection'; import { DumpJSON } from 'components/common/DumpJSON'; import { isMapTaskType } from 'models/Task/utils'; import { TaskExecutionPhase } from 'models/Execution/enums'; +import { MapTaskExecutionDetails } from 'components/Executions/TaskExecutionsList/MapTaskExecutionDetails'; import { TaskVersionDetailsLink } from 'components/Entities/VersionDetails/VersionDetailsLink'; import { Identifier } from 'models/Common/types'; import { TaskExecutionsList } from '../../TaskExecutionsList/TaskExecutionsList'; @@ -42,10 +43,11 @@ const defaultTab = tabIds.executions; export const NodeExecutionTabs: React.FC<{ nodeExecution: NodeExecution; - shouldShowTaskDetails: boolean; + selectedTaskExecution: MapTaskExecution | null; + onTaskSelected: (val: MapTaskExecution) => void; phase?: TaskExecutionPhase; taskTemplate?: TaskTemplate | null; -}> = ({ nodeExecution, shouldShowTaskDetails, taskTemplate, phase }) => { +}> = ({ nodeExecution, selectedTaskExecution, onTaskSelected, taskTemplate, phase }) => { const styles = useStyles(); const tabState = useTabState(tabIds, defaultTab); @@ -60,7 +62,15 @@ export const NodeExecutionTabs: React.FC<{ let tabContent: JSX.Element | null = null; switch (tabState.value) { case tabIds.executions: { - tabContent = ; + tabContent = selectedTaskExecution ? ( + + ) : ( + + ); break; } case tabIds.inputs: { @@ -82,11 +92,8 @@ export const NodeExecutionTabs: React.FC<{ } } - const executionLabel = isMapTaskType(taskTemplate?.type) - ? shouldShowTaskDetails - ? 'Execution' - : 'Map Execution' - : 'Executions'; + const executionLabel = + isMapTaskType(taskTemplate?.type) && !selectedTaskExecution ? '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 index 1e5de265f..944fa6f26 100644 --- 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 @@ -6,6 +6,7 @@ import { createMockNodeExecutions } from 'models/Execution/__mocks__/mockNodeExe import { TaskType } from 'models/Task/constants'; import { createMockWorkflow } from 'models/__mocks__/workflowData'; import * as React from 'react'; +import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData'; import { NodeExecutionTabs } from '../index'; const getMockNodeExecution = () => createMockNodeExecutions(1).executions[0]; @@ -20,26 +21,28 @@ 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 () => { + it('should display proper tab name when it was provided and shouldShow is TRUE', () => { const { queryByText, queryAllByRole } = render( , ); expect(queryAllByRole('tab')).toHaveLength(4); - expect(queryByText('Execution')).toBeInTheDocument(); + expect(queryByText('Executions')).toBeInTheDocument(); }); - it('should display proper tab name when it was provided and shouldShow is FALSE', async () => { + it('should display proper tab name when it was provided and shouldShow is FALSE', () => { const { queryByText, queryAllByRole } = render( , ); @@ -49,9 +52,13 @@ describe('NodeExecutionTabs', () => { }); describe('without map tasks', () => { - it('should display proper tab name when mapTask was not provided', async () => { + it('should display proper tab name when mapTask was not provided', () => { const { queryAllByRole, queryByText } = render( - , + , ); expect(queryAllByRole('tab')).toHaveLength(3); diff --git a/packages/zapp/console/src/components/Executions/ExecutionStatusBadge.tsx b/packages/zapp/console/src/components/Executions/ExecutionStatusBadge.tsx index f33e3b0aa..f6725371f 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionStatusBadge.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionStatusBadge.tsx @@ -64,7 +64,7 @@ type ExecutionStatusBadgeProps = | NodeExecutionStatusBadgeProps | TaskExecutionStatusBadgeProps; -function getPhaseConstants( +export function getPhaseConstants( type: 'workflow' | 'node' | 'task', phase: WorkflowExecutionPhase | NodeExecutionPhase | TaskExecutionPhase, ) { diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionDetails.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionDetails.tsx new file mode 100644 index 000000000..cd83a6317 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionDetails.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { MapTaskExecution } from 'models/Execution/types'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { PanelSection } from 'components/common/PanelSection'; +import { formatRetryAttempt, getTaskRetryAtemptsForIndex } from './utils'; +import { TaskExecutionLogsCard } from './TaskExecutionLogsCard'; + +interface MapTaskExecutionDetailsProps { + taskExecution: MapTaskExecution; +} + +/** Renders an individual map task execution attempts as part of a list */ +export const MapTaskExecutionDetails: React.FC = ({ + taskExecution, +}) => { + const { + closure: { metadata }, + taskIndex, + } = taskExecution; + + const filteredResources = getTaskRetryAtemptsForIndex( + metadata?.externalResources ?? [], + taskIndex, + ); + + return ( + + {filteredResources.map((item) => { + const attempt = item.retryAttempt ?? 0; + const headerText = formatRetryAttempt(attempt); + + return ( +
+ +
+ ); + })} +
+ ); +}; diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx index 3fdf143c2..679f3f5d8 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/MapTaskExecutionListItem.tsx @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { PanelSection } from 'components/common/PanelSection'; import { useCommonStyles } from 'components/common/styles'; import { TaskExecutionPhase } from 'models/Execution/enums'; -import { TaskExecution } from 'models/Execution/types'; +import { MapTaskExecution, TaskExecution } from 'models/Execution/types'; import { MapTaskStatusInfo } from 'components/common/MapTaskExecutionsList/MapTaskStatusInfo'; import { TaskExecutionDetails } from './TaskExecutionDetails'; import { TaskExecutionError } from './TaskExecutionError'; @@ -34,6 +34,7 @@ const useStyles = makeStyles((theme: Theme) => ({ interface MapTaskExecutionsListItemProps { taskExecution: TaskExecution; showAttempts: boolean; + onTaskSelected: (val: MapTaskExecution) => void; selectedPhase?: TaskExecutionPhase; } @@ -41,19 +42,23 @@ interface MapTaskExecutionsListItemProps { export const MapTaskExecutionsListItem: React.FC = ({ taskExecution, showAttempts, + onTaskSelected, selectedPhase, }) => { const commonStyles = useCommonStyles(); const styles = useStyles(); - const { closure } = taskExecution; - const taskHasStarted = closure.phase >= TaskExecutionPhase.QUEUED; - const headerText = formatRetryAttempt(taskExecution.id.retryAttempt); - const logsByPhase = getGroupedLogs(closure.metadata?.externalResources ?? []); + const { + closure: { error, startedAt, updatedAt, duration, phase, logs, metadata }, + id: { retryAttempt }, + } = taskExecution; + const taskHasStarted = phase >= TaskExecutionPhase.QUEUED; + const headerText = formatRetryAttempt(retryAttempt); + const logsByPhase = getGroupedLogs(metadata?.externalResources ?? []); return ( - {/* Attempts header is ahown only if there is more than one attempt */} + {/* Attempts header is shown only if there is more than one attempt */} {showAttempts ? (
@@ -64,16 +69,16 @@ export const MapTaskExecutionsListItem: React.FC
) : null} {/* Error info is shown only if there is an error present for this map task */} - {closure.error ? ( + {error ? (
- +
) : null} {/* If main map task has log attached - show it here */} - {closure.logs && closure.logs.length > 0 ? ( + {logs && logs.length > 0 ? (
- +
) : null} {/* child/array logs separated by subtasks phase */} @@ -85,10 +90,12 @@ export const MapTaskExecutionsListItem: React.FC const key = `${id}-${phase}`; return ( ); })} @@ -96,7 +103,7 @@ export const MapTaskExecutionsListItem: React.FC {/* If map task is actively started - show 'started' and 'run time' details */} {taskHasStarted && (
- +
)}
diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionDetails.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionDetails.tsx index 76b22c71f..80d323388 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionDetails.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionDetails.tsx @@ -2,40 +2,38 @@ import { unknownValueString } from 'common/constants'; import { dateWithFromNow, protobufDurationToHMS } from 'common/formatters'; import { timestampToDate } from 'common/utils'; import { DetailsGroup } from 'components/common/DetailsGroup'; -import { TaskExecution } from 'models/Execution/types'; import * as React from 'react'; +import { Protobuf } from 'flyteidl'; /** Renders the less important details for a `TaskExecution` as a `DetailsGroup` */ export const TaskExecutionDetails: React.FC<{ - taskExecution: TaskExecution; -}> = ({ taskExecution }) => { - const labelWidthGridUnits = taskExecution.closure.startedAt ? 7 : 10; + startedAt?: Protobuf.ITimestamp; + updatedAt?: Protobuf.ITimestamp | null; + duration?: Protobuf.Duration; +}> = ({ startedAt, duration, updatedAt }) => { + const labelWidthGridUnits = startedAt ? 7 : 10; const detailItems = React.useMemo(() => { - if (taskExecution.closure.startedAt) { + if (startedAt) { return [ { name: 'started', - content: dateWithFromNow(timestampToDate(taskExecution.closure.startedAt)), + content: dateWithFromNow(timestampToDate(startedAt)), }, { name: 'run time', - content: taskExecution.closure.duration - ? protobufDurationToHMS(taskExecution.closure.duration) - : unknownValueString, + content: duration ? protobufDurationToHMS(duration) : unknownValueString, }, ]; } else { return [ { name: 'last updated', - content: taskExecution.closure.updatedAt - ? dateWithFromNow(timestampToDate(taskExecution.closure.updatedAt)) - : unknownValueString, + content: updatedAt ? dateWithFromNow(timestampToDate(updatedAt)) : unknownValueString, }, ]; } - }, [taskExecution]); + }, [startedAt, duration, updatedAt]); return (
diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionLogsCard.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionLogsCard.tsx new file mode 100644 index 000000000..3f4f14b5f --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionLogsCard.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import classnames from 'classnames'; +import { useCommonStyles } from 'components/common/styles'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { TaskExecution } from 'models/Execution/types'; +import { Core } from 'flyteidl'; +import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; +import { TaskExecutionDetails } from './TaskExecutionDetails'; +import { TaskExecutionError } from './TaskExecutionError'; +import { TaskExecutionLogs } from './TaskExecutionLogs'; + +const useStyles = makeStyles((theme: Theme) => ({ + detailsLink: { + fontWeight: 'normal', + }, + header: { + marginBottom: theme.spacing(1), + }, + title: { + marginBottom: theme.spacing(1), + }, + showDetailsButton: { + marginTop: theme.spacing(1), + }, + section: { + marginBottom: theme.spacing(2), + }, +})); + +interface TaskExecutionLogsCardProps { + taskExecution: TaskExecution; + headerText: string; + phase: TaskExecutionPhase; + logs: Core.ITaskLog[]; +} + +export const TaskExecutionLogsCard: React.FC = ({ + taskExecution, + headerText, + phase, + logs, +}) => { + const commonStyles = useCommonStyles(); + const styles = useStyles(); + + const { + closure: { error, startedAt, updatedAt, duration }, + } = taskExecution; + const taskHasStarted = phase >= TaskExecutionPhase.QUEUED; + + return ( + <> +
+
+ + {headerText} + +
+ +
+ {!!error && ( +
+ +
+ )} + {taskHasStarted && ( + <> +
+ +
+
+ +
+ + )} + + ); +}; diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx index ef354206c..f59c27f06 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx @@ -3,7 +3,7 @@ import { makeStyles, Theme } from '@material-ui/core/styles'; import { noExecutionsFoundString } from 'common/constants'; import { NonIdealState } from 'components/common/NonIdealState'; import { WaitForData } from 'components/common/WaitForData'; -import { NodeExecution, TaskExecution } from 'models/Execution/types'; +import { MapTaskExecution, NodeExecution, TaskExecution } from 'models/Execution/types'; import { isMapTaskType } from 'models/Task/utils'; import { TaskExecutionPhase } from 'models/Execution/enums'; import { useTaskExecutions, useTaskExecutionsRefresher } from '../useTaskExecutions'; @@ -19,13 +19,15 @@ const useStyles = makeStyles((theme: Theme) => ({ interface TaskExecutionsListProps { nodeExecution: NodeExecution; + onTaskSelected: (val: MapTaskExecution) => void; phase?: TaskExecutionPhase; } export const TaskExecutionsListContent: React.FC<{ taskExecutions: TaskExecution[]; + onTaskSelected: (val: MapTaskExecution) => void; phase?: TaskExecutionPhase; -}> = ({ taskExecutions, phase }) => { +}> = ({ taskExecutions, onTaskSelected, phase }) => { const styles = useStyles(); if (!taskExecutions.length) { return ( @@ -49,6 +51,7 @@ export const TaskExecutionsListContent: React.FC<{ taskExecution={taskExecution} showAttempts={taskExecutions.length > 1} selectedPhase={phase} + onTaskSelected={onTaskSelected} /> ) : ( = ({ nodeExecution, phase }) => { +export const TaskExecutionsList: React.FC = ({ + nodeExecution, + onTaskSelected, + phase, +}) => { const taskExecutions = useTaskExecutions(nodeExecution.id); useTaskExecutionsRefresher(nodeExecution, taskExecutions); return ( - + ); }; diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx index ac899606e..3ddcc3639 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx @@ -1,34 +1,8 @@ import * as React from 'react'; -import { makeStyles, Theme } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; -import classnames from 'classnames'; import { PanelSection } from 'components/common/PanelSection'; -import { useCommonStyles } from 'components/common/styles'; -import { TaskExecutionPhase } from 'models/Execution/enums'; import { TaskExecution } from 'models/Execution/types'; -import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; -import { TaskExecutionDetails } from './TaskExecutionDetails'; -import { TaskExecutionError } from './TaskExecutionError'; -import { TaskExecutionLogs } from './TaskExecutionLogs'; import { formatRetryAttempt } from './utils'; - -const useStyles = makeStyles((theme: Theme) => ({ - detailsLink: { - fontWeight: 'normal', - }, - header: { - marginBottom: theme.spacing(1), - }, - title: { - marginBottom: theme.spacing(1), - }, - showDetailsButton: { - marginTop: theme.spacing(1), - }, - section: { - marginBottom: theme.spacing(2), - }, -})); +import { TaskExecutionLogsCard } from './TaskExecutionLogsCard'; interface TaskExecutionsListItemProps { taskExecution: TaskExecution; @@ -38,38 +12,19 @@ interface TaskExecutionsListItemProps { export const TaskExecutionsListItem: React.FC = ({ taskExecution, }) => { - const commonStyles = useCommonStyles(); - const styles = useStyles(); - const { closure } = taskExecution; - const { error } = closure; + const { + closure: { phase, logs }, + } = taskExecution; const headerText = formatRetryAttempt(taskExecution.id.retryAttempt); - const taskHasStarted = closure.phase >= TaskExecutionPhase.QUEUED; return ( -
-
- - {headerText} - -
- -
- {!!error && ( -
- -
- )} - {taskHasStarted && ( - <> -
- -
-
- -
- - )} +
); }; diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/MapTaskExecutionDetails.test.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/MapTaskExecutionDetails.test.tsx new file mode 100644 index 000000000..4a51832a5 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/MapTaskExecutionDetails.test.tsx @@ -0,0 +1,32 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { MapTaskExecutionDetails } from '../MapTaskExecutionDetails'; +import { MockMapTaskExecution } from '../TaskExecutions.mocks'; + +jest.mock('../TaskExecutionLogsCard.tsx', () => ({ + TaskExecutionLogsCard: jest.fn(({ children }) =>
{children}
), +})); + +describe('MapTaskExecutionDetails', () => { + it('should render list with 1 execution attempt', () => { + const { queryAllByTestId } = render( + , + ); + const logsCards = queryAllByTestId('logs-card'); + expect(logsCards).toHaveLength(1); + logsCards.forEach((card) => { + expect(card).toBeInTheDocument(); + }); + }); + + it('should render list with 2 execution attempts', () => { + const { queryAllByTestId } = render( + , + ); + const logsCards = queryAllByTestId('logs-card'); + expect(logsCards).toHaveLength(2); + logsCards.forEach((card) => { + expect(card).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionDetails.test.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionDetails.test.tsx new file mode 100644 index 000000000..594032831 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionDetails.test.tsx @@ -0,0 +1,52 @@ +import { render } from '@testing-library/react'; +import { unknownValueString } from 'common/constants'; +import * as React from 'react'; +import { long } from 'test/utils'; +import { TaskExecutionDetails } from '../TaskExecutionDetails'; + +const date = { seconds: long(5), nanos: 0 }; +const duration = { seconds: long(0), nanos: 0 }; + +const dateContent = '1/1/1970 12:00:05 AM UTC (52 years ago)'; + +describe('TaskExecutionDetails', () => { + it('should render details with task started info and duration', () => { + const { queryByText } = render(); + + expect(queryByText('started')).toBeInTheDocument(); + expect(queryByText('last updated')).not.toBeInTheDocument(); + expect(queryByText(dateContent)).toBeInTheDocument(); + expect(queryByText('run time')).toBeInTheDocument(); + expect(queryByText('0s')).toBeInTheDocument(); + }); + + it('should render details with task started info without duration', () => { + const { queryByText } = render(); + + expect(queryByText('started')).toBeInTheDocument(); + expect(queryByText('last updated')).not.toBeInTheDocument(); + expect(queryByText(dateContent)).toBeInTheDocument(); + expect(queryByText('run time')).toBeInTheDocument(); + expect(queryByText(unknownValueString)).toBeInTheDocument(); + }); + + it('should render details with task updated info and duration', () => { + const { queryByText } = render(); + + expect(queryByText('started')).not.toBeInTheDocument(); + expect(queryByText('last updated')).toBeInTheDocument(); + expect(queryByText(dateContent)).toBeInTheDocument(); + expect(queryByText('run time')).not.toBeInTheDocument(); + expect(queryByText('0s')).not.toBeInTheDocument(); + }); + + it('should render details with task updated info without duration', () => { + const { queryByText } = render(); + + expect(queryByText('started')).not.toBeInTheDocument(); + expect(queryByText('last updated')).toBeInTheDocument(); + expect(queryByText(dateContent)).toBeInTheDocument(); + expect(queryByText('run time')).not.toBeInTheDocument(); + expect(queryByText(unknownValueString)).not.toBeInTheDocument(); + }); +}); diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionLogsCard.test.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionLogsCard.test.tsx new file mode 100644 index 000000000..8d5e7b6a5 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionLogsCard.test.tsx @@ -0,0 +1,47 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { noLogsFoundString } from 'components/Executions/constants'; +import { getPhaseConstants } from 'components/Executions/ExecutionStatusBadge'; +import { TaskExecutionLogsCard } from '../TaskExecutionLogsCard'; +import { formatRetryAttempt } from '../utils'; + +const headerText = formatRetryAttempt(0); +const taskLogs = [{ uri: '#', name: 'Kubernetes Logs #0-0' }]; +const phase = TaskExecutionPhase.SUCCEEDED; + +describe('TaskExecutionLogsCard', () => { + it('should render card with logs provided', () => { + const { queryByText } = render( + , + ); + const { text } = getPhaseConstants('task', phase); + + expect(queryByText(headerText)).toBeInTheDocument(); + expect(queryByText(text)).toBeInTheDocument(); + expect(queryByText(taskLogs[0].name)).toBeInTheDocument(); + }); + + it('should render card with no logs found string', () => { + const { queryByText } = render( + , + ); + + const { text } = getPhaseConstants('task', phase); + + expect(queryByText(headerText)).toBeInTheDocument(); + expect(queryByText(text)).toBeInTheDocument(); + expect(queryByText(noLogsFoundString)).toBeInTheDocument(); + }); +}); diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsList.test.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsList.test.tsx index d95c1407a..a805b1832 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsList.test.tsx +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsList.test.tsx @@ -21,7 +21,7 @@ describe('TaskExecutionsList', () => { listTaskExecutions: mockListTaskExecutions, })} > - + , ); beforeEach(() => { diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsListItem.test.tsx b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsListItem.test.tsx new file mode 100644 index 000000000..c977f5690 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/TaskExecutionsListItem.test.tsx @@ -0,0 +1,17 @@ +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { TaskExecutionsListItem } from '../TaskExecutionsListItem'; +import { MockMapTaskExecution } from '../TaskExecutions.mocks'; + +jest.mock('../TaskExecutionLogsCard.tsx', () => ({ + TaskExecutionLogsCard: jest.fn(({ children }) =>
{children}
), +})); + +describe('TaskExecutionsListItem', () => { + it('should render execution logs card', () => { + const { queryByTestId } = render( + , + ); + expect(queryByTestId('logs-card')).toBeInTheDocument(); + }); +}); diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts index 53b0a4599..1990f4890 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts @@ -1,8 +1,14 @@ +import { getTaskLogName, getTaskIndex } from 'components/Executions/TaskExecutionsList/utils'; import { Event } from 'flyteidl'; import { TaskExecutionPhase } from 'models/Execution/enums'; import { obj } from 'test/utils'; -import { getMockMapTaskLogItem } from '../TaskExecutions.mocks'; -import { formatRetryAttempt, getGroupedLogs, getUniqueTaskExecutionName } from '../utils'; +import { + getTaskRetryAtemptsForIndex, + formatRetryAttempt, + getGroupedLogs, + getUniqueTaskExecutionName, +} from '../utils'; +import { getMockMapTaskLogItem, MockMapTaskExecution } from '../TaskExecutions.mocks'; describe('getUniqueTaskExecutionName', () => { const cases: [{ name: string; retryAttempt: number }, string][] = [ @@ -46,7 +52,7 @@ describe('getGroupedLogs', () => { getMockMapTaskLogItem(TaskExecutionPhase.FAILED, false, 2), ]; - it(`Should properly group to Success and Failed`, () => { + it('should properly group to Success and Failed', () => { const logs = getGroupedLogs(resources); // Do not have key which was not in the logs expect(logs.get(TaskExecutionPhase.QUEUED)).toBeUndefined(); @@ -62,3 +68,76 @@ describe('getGroupedLogs', () => { expect(logs.get(TaskExecutionPhase.FAILED)?.length).toEqual(1); }); }); + +describe('getTaskRetryAttemptsForIndex', () => { + it('should return 2 filtered attempts for provided index', () => { + const index = 3; + // '?? []' -> TS check, mock contains externalResources + const result = getTaskRetryAtemptsForIndex( + MockMapTaskExecution.closure.metadata?.externalResources ?? [], + index, + ); + expect(result).toHaveLength(2); + }); + + it('should return 1 filtered attempt for provided index', () => { + const index = 0; + // '?? []' -> TS check, mock contains externalResources + const result = getTaskRetryAtemptsForIndex( + MockMapTaskExecution.closure.metadata?.externalResources ?? [], + index, + ); + expect(result).toHaveLength(1); + }); + + it('should return empty array when null index provided', () => { + const index = null; + // '?? []' -> TS check, mock contains externalResources + const result = getTaskRetryAtemptsForIndex( + MockMapTaskExecution.closure.metadata?.externalResources ?? [], + index, + ); + expect(result).toHaveLength(0); + }); +}); + +describe('getTaskIndex', () => { + it('should return index if selected log has a match in externalResources list', () => { + const index = 3; + const log = getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true, index, 1).logs?.[0]; + + // TS check + if (log) { + const result1 = getTaskIndex(MockMapTaskExecution, log); + expect(result1).toStrictEqual(index); + } + }); +}); + +describe('getTaskLogName', () => { + it('should return correct names', () => { + const taskName1 = 'task_name_1'; + const taskName2 = 'task.task_name_1'; + const taskLogName1 = 'abc'; + const taskLogName2 = 'abc-1'; + const taskLogName3 = 'abc-1-1'; + + const result1 = getTaskLogName(taskName1, taskLogName1); + expect(result1).toStrictEqual(taskName1); + + const result2 = getTaskLogName(taskName1, taskLogName2); + expect(result2).toStrictEqual('task_name_1-1'); + + const result3 = getTaskLogName(taskName1, taskLogName3); + expect(result3).toStrictEqual('task_name_1-1-1'); + + const result4 = getTaskLogName(taskName2, taskLogName1); + expect(result4).toStrictEqual(taskName1); + + const result5 = getTaskLogName(taskName2, taskLogName2); + expect(result5).toStrictEqual('task_name_1-1'); + + const result6 = getTaskLogName(taskName2, taskLogName3); + expect(result6).toStrictEqual('task_name_1-1-1'); + }); +}); diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts b/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts index e307f7ec1..7beb824ee 100644 --- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts +++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts @@ -1,6 +1,6 @@ -import { LogsByPhase, TaskExecution } from 'models/Execution/types'; +import { ExternalResource, LogsByPhase, TaskExecution } from 'models/Execution/types'; import { leftPaddedNumber } from 'common/formatters'; -import { Event } from 'flyteidl'; +import { Core, Event } from 'flyteidl'; import { TaskExecutionPhase } from 'models/Execution/enums'; /** Generates a unique name for a task execution, suitable for display in a @@ -61,3 +61,62 @@ export const getGroupedLogs = (resources: Event.IExternalResourceInfo[]): LogsBy return logsByPhase; }; + +export const getTaskRetryAtemptsForIndex = ( + resources: ExternalResource[], + taskIndex: number | null, +): ExternalResource[] => { + // check spesifically for null values, to make sure we're not skipping logs for 0 index + if (taskIndex === null) { + return []; + } + + const filtered = resources.filter((a) => { + const index = a.index ?? 0; + return index === taskIndex; + }); + + // sort output sample [0-2, 0-1, 0, 1, 2], where 0-1 means index = 0 retry = 1 + filtered.sort((a, b) => { + const aIndex = a.index ?? 0; + const bIndex = b.index ?? 0; + if (aIndex !== bIndex) { + // return smaller index first + return aIndex - bIndex; + } + + const aRetry = a.retryAttempt ?? 0; + const bRetry = b.retryAttempt ?? 0; + return bRetry - aRetry; + }); + return filtered; +}; + +export function getTaskIndex( + taskExecution: TaskExecution, + selectedLog: Core.ITaskLog, +): number | null { + const externalResources = taskExecution.closure.metadata?.externalResources ?? []; + for (const item of externalResources) { + const logs = item.logs ?? []; + for (const log of logs) { + if (log.uri) { + if (log.name === selectedLog.name && log.uri === selectedLog.uri) { + return item.index ?? 0; + } + } else if (log.name === selectedLog.name) { + return item.index ?? 0; + } + } + } + + return null; +} + +export function getTaskLogName(taskName: string, taskLogName: string): string { + const lastDotIndex = taskName.lastIndexOf('.'); + const prefix = lastDotIndex !== -1 ? taskName.slice(lastDotIndex + 1) : taskName; + const firstDahIndex = taskLogName.indexOf('-'); + const suffix = firstDahIndex !== -1 ? taskLogName.slice(firstDahIndex) : ''; + return `${prefix}${suffix}`; +} diff --git a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx index 6b2a6bfe1..ff0035bb4 100644 --- a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -16,6 +16,8 @@ import { transformerWorkflowToDag } from './transformerWorkflowToDag'; export interface WorkflowGraphProps { onNodeSelectionChanged: (selectedNodes: string[]) => void; onPhaseSelectionChanged: (phase: TaskExecutionPhase) => void; + selectedPhase?: TaskExecutionPhase; + isDetailsTabClosed: boolean; workflow: Workflow; nodeExecutionsById?: any; } @@ -56,7 +58,14 @@ export interface DynamicWorkflowMapping { dynamicExecutions: any[]; } export const WorkflowGraph: React.FC = (props) => { - const { onNodeSelectionChanged, onPhaseSelectionChanged, nodeExecutionsById, workflow } = props; + const { + onNodeSelectionChanged, + onPhaseSelectionChanged, + selectedPhase, + isDetailsTabClosed, + nodeExecutionsById, + workflow, + } = props; const { dag, staticExecutionIdsMap, error } = workflowToDag(workflow); /** * Note: @@ -113,6 +122,8 @@ export const WorkflowGraph: React.FC = (props) => { data={merged} onNodeSelectionChanged={onNodeSelectionChanged} onPhaseSelectionChanged={onPhaseSelectionChanged} + selectedPhase={selectedPhase} + isDetailsTabClosed={isDetailsTabClosed} /> ); }; diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx index f5fdfc86b..199c6f869 100644 --- a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { TaskExecutionPhase } from 'models/Execution/enums'; +import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData'; import { MapTaskStatusInfo } from './MapTaskStatusInfo'; import { PanelViewDecorator } from '../__stories__/Decorators'; @@ -17,19 +18,24 @@ const Template: ComponentStory = (args) => ( export const Default = Template.bind({}); Default.decorators = [(Story) => PanelViewDecorator(Story)]; Default.args = { + taskExecution: mockTaskExecution, taskLogs: [ + // logs without URI should be black and not clickable { uri: '#', name: 'Kubernetes Logs #0-0' }, { uri: '#', name: 'Kubernetes Logs #0-1' }, - { uri: '#', name: 'Kubernetes Logs #0-2' }, - { uri: '#', name: 'Kubernetes Logs #0-3' }, + { name: 'Kubernetes Logs #0-2' }, + { name: 'Kubernetes Logs #0-3' }, { uri: '#', name: 'Kubernetes Logs #0-4' }, ], - status: TaskExecutionPhase.QUEUED, - expanded: true, + phase: TaskExecutionPhase.QUEUED, + selectedPhase: TaskExecutionPhase.QUEUED, + onTaskSelected: () => {}, }; export const AllSpace = Template.bind({}); AllSpace.args = { + taskExecution: mockTaskExecution, taskLogs: [], - status: TaskExecutionPhase.SUCCEEDED, + phase: TaskExecutionPhase.SUCCEEDED, + onTaskSelected: () => {}, }; diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx index cef9b2be6..2aa314f36 100644 --- a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { RowExpander } from 'components/Executions/Tables/RowExpander'; import { TaskExecutionPhase } from 'models/Execution/enums'; import { getTaskExecutionPhaseConstants } from 'components/Executions/utils'; -import { TaskLogList } from 'components/Executions/TaskExecutionsList/TaskExecutionLogs'; import { Core } from 'flyteidl'; +import { MapTaskExecution, TaskExecution } from 'models/Execution/types'; +import { TaskNameList } from './TaskNameList'; const useStyles = makeStyles((_theme: Theme) => ({ mainWrapper: { @@ -36,20 +37,33 @@ const useStyles = makeStyles((_theme: Theme) => ({ })); interface MapTaskStatusInfoProps { + taskExecution: TaskExecution; taskLogs: Core.ITaskLog[]; phase: TaskExecutionPhase; - isExpanded: boolean; + selectedPhase?: TaskExecutionPhase; + onTaskSelected: (val: MapTaskExecution) => void; } -export const MapTaskStatusInfo = ({ taskLogs, phase, isExpanded }: MapTaskStatusInfoProps) => { - const [expanded, setExpanded] = useState(isExpanded); +export const MapTaskStatusInfo = ({ + taskExecution, + taskLogs, + phase, + selectedPhase, + onTaskSelected, +}: MapTaskStatusInfoProps) => { + const [expanded, setExpanded] = useState(selectedPhase === phase); const styles = useStyles(); + const phaseData = getTaskExecutionPhaseConstants(phase); + + useEffect(() => { + setExpanded(selectedPhase === phase); + }, [selectedPhase, phase]); + const toggleExpanded = () => { setExpanded(!expanded); }; - const phaseData = getTaskExecutionPhaseConstants(phase); return (
@@ -62,7 +76,11 @@ export const MapTaskStatusInfo = ({ taskLogs, phase, isExpanded }: MapTaskStatus
{expanded && (
- +
)}
diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx new file mode 100644 index 000000000..41d7d1c79 --- /dev/null +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Typography } from '@material-ui/core'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import { Core } from 'flyteidl'; +import { getTaskIndex, getTaskLogName } from 'components/Executions/TaskExecutionsList/utils'; +import { MapTaskExecution, TaskExecution } from 'models/Execution/types'; +import { noLogsFoundString } from 'components/Executions/constants'; +import { useCommonStyles } from '../styles'; + +interface StyleProps { + isLink: boolean; +} + +const useStyles = makeStyles((_theme: Theme) => ({ + taskTitle: ({ isLink }: StyleProps) => ({ + cursor: isLink ? 'pointer' : 'default', + '&:hover': { + textDecoration: isLink ? 'underline' : 'none', + }, + }), +})); + +interface TaskNameListProps { + taskExecution: TaskExecution; + logs: Core.ITaskLog[]; + onTaskSelected: (val: MapTaskExecution) => void; +} + +export const TaskNameList = ({ taskExecution, logs, onTaskSelected }: TaskNameListProps) => { + const commonStyles = useCommonStyles(); + + if (logs.length === 0) { + return {noLogsFoundString}; + } + + return ( + <> + {logs.map((log) => { + const styles = useStyles({ isLink: !!log.uri }); + const taskLogName = getTaskLogName(taskExecution.id.taskId.name, log.name ?? ''); + const taskIndex = getTaskIndex(taskExecution, log); + + const handleClick = () => { + onTaskSelected({ ...taskExecution, taskIndex }); + }; + + return ( + + {taskLogName} + + ); + })} + + ); +}; diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/test/MapTaskStatusInfo.test.tsx similarity index 64% rename from packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx rename to packages/zapp/console/src/components/common/MapTaskExecutionsList/test/MapTaskStatusInfo.test.tsx index 399a92309..29885c99c 100644 --- a/packages/zapp/console/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.test.tsx +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/test/MapTaskStatusInfo.test.tsx @@ -3,8 +3,13 @@ import { noLogsFoundString } from 'components/Executions/constants'; import { getTaskExecutionPhaseConstants } from 'components/Executions/utils'; import { TaskExecutionPhase } from 'models/Execution/enums'; import * as React from 'react'; +import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData'; -import { MapTaskStatusInfo } from './MapTaskStatusInfo'; +import { + getTaskLogName, + getUniqueTaskExecutionName, +} from 'components/Executions/TaskExecutionsList/utils'; +import { MapTaskStatusInfo } from '../MapTaskStatusInfo'; const taskLogs = [ { uri: '#', name: 'Kubernetes Logs #0-0' }, @@ -18,7 +23,12 @@ describe('MapTaskStatusInfo', () => { const phaseData = getTaskExecutionPhaseConstants(phase); const { queryByText, getByTitle } = render( - , + , ); expect(queryByText(phaseData.text)).toBeInTheDocument(); @@ -29,7 +39,9 @@ describe('MapTaskStatusInfo', () => { const buttonEl = getByTitle('Expand row'); fireEvent.click(buttonEl); await waitFor(() => { - expect(queryByText(taskLogs[0].name)).toBeInTheDocument(); + const taskName = getUniqueTaskExecutionName(mockTaskExecution); + const taskLogName = getTaskLogName(taskName, taskLogs[0].name); + expect(queryByText(taskLogName)).toBeInTheDocument(); }); }); @@ -38,7 +50,13 @@ describe('MapTaskStatusInfo', () => { const phaseData = getTaskExecutionPhaseConstants(phase); const { queryByText } = render( - , + , ); expect(queryByText(phaseData.text)).toBeInTheDocument(); diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/test/TaskNameList.test.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/test/TaskNameList.test.tsx new file mode 100644 index 000000000..59a620df0 --- /dev/null +++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/test/TaskNameList.test.tsx @@ -0,0 +1,59 @@ +import ThemeProvider from '@material-ui/styles/ThemeProvider'; +import { render } from '@testing-library/react'; +import { muiTheme } from 'components/Theme/muiTheme'; +import * as React from 'react'; +import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData'; + +import { TaskNameList } from '../TaskNameList'; + +const taskLogs = [ + { uri: '#', name: 'Kubernetes Logs #0-0' }, + { uri: '#', name: 'Kubernetes Logs #0-1' }, + { uri: '#', name: 'Kubernetes Logs #0-2' }, +]; + +const taskLogsWithoutUri = [ + { name: 'Kubernetes Logs #0-0' }, + { name: 'Kubernetes Logs #0-1' }, + { name: 'Kubernetes Logs #0-2' }, +]; + +describe('TaskNameList', () => { + it('should render log names in color if they have URI', async () => { + const { queryAllByTestId } = render( + + + , + ); + + const logs = queryAllByTestId('map-task-log'); + expect(logs).toHaveLength(3); + logs.forEach((log) => { + expect(log).toBeInTheDocument(); + expect(log).toHaveStyle({ color: '#8B37FF' }); + }); + }); + + it('should render log names in black if they have URI', () => { + const { queryAllByTestId } = render( + + + , + ); + + const logs = queryAllByTestId('map-task-log'); + expect(logs).toHaveLength(3); + logs.forEach((log) => { + expect(log).toBeInTheDocument(); + expect(log).toHaveStyle({ color: '#292936' }); + }); + }); +}); diff --git a/packages/zapp/console/src/components/common/PanelSection/index.tsx b/packages/zapp/console/src/components/common/PanelSection/index.tsx index ace1ab503..cb6dfd91e 100644 --- a/packages/zapp/console/src/components/common/PanelSection/index.tsx +++ b/packages/zapp/console/src/components/common/PanelSection/index.tsx @@ -3,11 +3,11 @@ import { makeStyles } from '@material-ui/core/styles'; const useStyle = makeStyles((theme) => ({ detailsPanelCard: { - borderBottom: `1px solid ${theme.palette.divider}`, paddingBottom: '150px', // TODO @FC 454 temporary fix for panel height issue }, detailsPanelCardContent: { padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, + borderBottom: `1px solid ${theme.palette.divider}`, }, })); diff --git a/packages/zapp/console/src/components/common/SectionHeader.tsx b/packages/zapp/console/src/components/common/SectionHeader.tsx index 4bb112752..acb722171 100644 --- a/packages/zapp/console/src/components/common/SectionHeader.tsx +++ b/packages/zapp/console/src/components/common/SectionHeader.tsx @@ -6,7 +6,6 @@ const useStyles = makeStyles((theme: Theme) => ({ container: { marginBottom: theme.spacing(1), }, - title: {}, })); export interface SectionHeaderProps { @@ -17,9 +16,7 @@ export const SectionHeader: React.FC = ({ title, subtitle }) const styles = useStyles(); return (
- - {title} - + {title} {!!subtitle && {subtitle}}
); diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index a3c4079f5..7735be04d 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -48,6 +48,8 @@ const ReactFlowGraphComponent = (props) => { data, onNodeSelectionChanged, onPhaseSelectionChanged, + selectedPhase, + isDetailsTabClosed, nodeExecutionsById, dynamicWorkflows, } = props; @@ -56,6 +58,7 @@ const ReactFlowGraphComponent = (props) => { dynamicWorkflows, currentNestedView: {}, nodeExecutionsById, + selectedPhase, onNodeSelectionChanged, onPhaseSelectionChanged, rfGraphJson: null, @@ -92,6 +95,7 @@ const ReactFlowGraphComponent = (props) => { nodeExecutionsById: state.nodeExecutionsById, onNodeSelectionChanged: state.onNodeSelectionChanged, onPhaseSelectionChanged: state.onPhaseSelectionChanged, + selectedPhase, onAddNestedView, onRemoveNestedView, currentNestedView: state.currentNestedView, @@ -105,7 +109,7 @@ const ReactFlowGraphComponent = (props) => { ...state, rfGraphJson: newRFGraphData, })); - }, [state.currentNestedView, state.nodeExecutionsById]); + }, [state.currentNestedView, state.nodeExecutionsById, isDetailsTabClosed]); useEffect(() => { if (graphNodeCountChanged(state.data, data)) { @@ -130,8 +134,9 @@ const ReactFlowGraphComponent = (props) => { ...state, onNodeSelectionChanged, onPhaseSelectionChanged, + selectedPhase, })); - }, [onNodeSelectionChanged, onPhaseSelectionChanged]); + }, [onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase]); const backgroundStyle = getRFBackground().nested; diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 791bcd8b3..27383b4ee 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -262,7 +262,9 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { const onNodeSelectionChanged = data.onNodeSelectionChanged; const onPhaseSelectionChanged = data.onPhaseSelectionChanged; const [selectedNode, setSelectedNode] = useState(false); - const [selectedPhase, setSelectedPhase] = useState(undefined); + const [selectedPhase, setSelectedPhase] = useState( + data.selectedPhase, + ); useEffect(() => { if (selectedNode === true) { @@ -349,12 +351,14 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => { if (!logsByPhase.has(phase)) { return null; } - + const defaultColor = getStatusColor(); + const phaseColor = getStatusColor(phase); + const color = !selectedPhase || phase === selectedPhase ? phaseColor : defaultColor; const key = `${id}-${phase}`; return ( void; + selectedPhase: TaskExecutionPhase; onAddNestedView: any; onRemoveNestedView: any; rootParentNode: dNode; @@ -58,6 +59,7 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, + selectedPhase, onAddNestedView, onRemoveNestedView, rootParentNode, @@ -95,6 +97,7 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { taskType, nodeLogsByPhase, cacheStatus, + selectedPhase, onNodeSelectionChanged: () => { if (onNodeSelectionChanged) { onNodeSelectionChanged([scopedId]); @@ -199,6 +202,7 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, + selectedPhase, onAddNestedView, onRemoveNestedView, currentNestedView, @@ -208,6 +212,7 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, + selectedPhase, onAddNestedView, onRemoveNestedView, currentNestedView, diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx index ac74ef775..a62901298 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx @@ -104,9 +104,9 @@ export const nodePhaseColorMapping = { * @returns */ export const getStatusColor = ( - nodeExecutionStatus: NodeExecutionPhase | TaskExecutionPhase, + nodeExecutionStatus?: NodeExecutionPhase | TaskExecutionPhase, ): string => { - if (nodePhaseColorMapping[nodeExecutionStatus]) { + if (nodeExecutionStatus && nodePhaseColorMapping[nodeExecutionStatus]) { return nodePhaseColorMapping[nodeExecutionStatus].color; } else { /** @TODO decide what we want default color to be */ diff --git a/packages/zapp/console/src/models/Execution/types.ts b/packages/zapp/console/src/models/Execution/types.ts index 96998afbc..f03d5a4a4 100644 --- a/packages/zapp/console/src/models/Execution/types.ts +++ b/packages/zapp/console/src/models/Execution/types.ts @@ -111,6 +111,9 @@ export interface TaskExecutionIdentifier extends Core.ITaskExecutionIdentifier { nodeExecutionId: NodeExecutionIdentifier; retryAttempt?: number; } +export interface MapTaskExecution extends TaskExecution { + taskIndex: number | null; +} export interface TaskExecution extends Admin.ITaskExecution { id: TaskExecutionIdentifier;