diff --git a/jest.config.js b/jest.config.js index e2bd7b7fb..1878b0f10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,9 +24,10 @@ module.exports = { '/assetsTransformer.js', }, coverageDirectory: '.coverage', - collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*/*.stories.tsx'], + collectCoverageFrom: ['**/*.{ts,tsx}', '!**/*/*.stories.{ts,tsx}', '!**/*/*.mocks.{ts,tsx}'], coveragePathIgnorePatterns: [ '__stories__', + '__mocks__', '/.storybook', '/node_modules', '/dist', diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index 5abb530bf..c5984096e 100644 --- a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -1,8 +1,7 @@ +import * as React from 'react'; import { useEffect, useRef } from 'react'; -import { IconButton, Typography } from '@material-ui/core'; +import { IconButton, Typography, Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; -import Tab from '@material-ui/core/Tab'; -import Tabs from '@material-ui/core/Tabs'; import Close from '@material-ui/icons/Close'; import classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; @@ -15,27 +14,25 @@ import { LocationDescriptor } from 'history'; import { PaginatedEntityResponse } from 'models/AdminEntity/types'; import { Workflow } from 'models/Workflow/types'; import { NodeExecution, NodeExecutionIdentifier, TaskExecution } from 'models/Execution/types'; -import { TaskTemplate } from 'models/Task/types'; -import * as React from 'react'; import Skeleton from 'react-loading-skeleton'; import { useQuery, useQueryClient } from 'react-query'; import { Link as RouterLink } from 'react-router-dom'; import { Routes } from 'routes/routes'; import { RemoteLiteralMapViewer } from 'components/Literals/RemoteLiteralMapViewer'; 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 { transformWorkflowToKeyedDag, getNodeNameFromDag } from 'components/WorkflowGraph/utils'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; import { makeListTaskExecutionsQuery, makeNodeExecutionQuery } from '../nodeExecutionQueries'; -import { TaskExecutionsList } from '../TaskExecutionsList/TaskExecutionsList'; import { NodeExecutionDetails } from '../types'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionInputs } from './NodeExecutionInputs'; -import { NodeExecutionOutputs } from './NodeExecutionOutputs'; -import { NodeExecutionTaskDetails } from './NodeExecutionTaskDetails'; import { getTaskExecutionDetailReasons } from './utils'; import { ExpandableMonospaceText } from '../../common/ExpandableMonospaceText'; import { fetchWorkflowExecution } from '../useWorkflowExecution'; +import { NodeExecutionTabs } from './NodeExecutionTabs'; const useStyles = makeStyles((theme: Theme) => { const paddingVertical = `${theme.spacing(2)}px`; @@ -127,8 +124,6 @@ const tabIds = { task: 'task', }; -const defaultTab = tabIds.executions; - interface NodeExecutionDetailsProps { nodeExecutionId: NodeExecutionIdentifier; onClose?: () => void; @@ -176,60 +171,13 @@ const ExecutionTypeDetails: React.FC<{ ); }; -const NodeExecutionTabs: React.FC<{ - nodeExecution: NodeExecution; - taskTemplate?: TaskTemplate | null; -}> = ({ nodeExecution, taskTemplate }) => { - const styles = useStyles(); - const tabState = useTabState(tabIds, defaultTab); - - if (tabState.value === tabIds.task && !taskTemplate) { - // Reset tab value, if task tab is selected, while no taskTemplate is avaible - // can happen when user switches between nodeExecutions without closing the drawer - tabState.onChange(() => { - /* */ - }, defaultTab); - } - - let tabContent: JSX.Element | null = null; - switch (tabState.value) { - case tabIds.executions: { - tabContent = ; - break; - } - case tabIds.inputs: { - tabContent = ; - break; - } - case tabIds.outputs: { - tabContent = ; - break; - } - case tabIds.task: { - tabContent = taskTemplate ? : null; - break; - } - } - return ( - <> - - - - - {!!taskTemplate && } - -
{tabContent}
- - ); -}; - const WorkflowTabs: React.FC<{ dagData: dNode; nodeId: string; }> = ({ dagData, nodeId }) => { const styles = useStyles(); const tabState = useTabState(tabIds, tabIds.inputs); - const commonStyles = useCommonStyles(); + let tabContent: JSX.Element | null = null; const id = nodeId.slice(nodeId.lastIndexOf('-') + 1); const taskTemplate = dagData[id].value.template; @@ -237,16 +185,18 @@ const WorkflowTabs: React.FC<{ switch (tabState.value) { case tabIds.inputs: { tabContent = taskTemplate ? ( -
-
- -
-
+ + + ) : null; break; } case tabIds.task: { - tabContent = taskTemplate ? : null; + tabContent = taskTemplate ? ( + + + + ) : null; break; } } @@ -340,7 +290,10 @@ export const NodeExecutionDetailsPanelContent: React.FC; const isRunningPhase = React.useMemo(() => { - return nodeExecution?.closure.phase === 1 || nodeExecution?.closure.phase === 2; + return ( + nodeExecution?.closure.phase === NodeExecutionPhase.QUEUED || + nodeExecution?.closure.phase === NodeExecutionPhase.RUNNING + ); }, [nodeExecution]); const handleReasonsVisibility = React.useCallback(() => { diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionInputs.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionTabs/NodeExecutionInputs.tsx similarity index 56% rename from src/components/Executions/ExecutionDetails/NodeExecutionInputs.tsx rename to src/components/Executions/ExecutionDetails/NodeExecutionTabs/NodeExecutionInputs.tsx index 7d6cea201..c83c53a9a 100644 --- a/src/components/Executions/ExecutionDetails/NodeExecutionInputs.tsx +++ b/src/components/Executions/ExecutionDetails/NodeExecutionTabs/NodeExecutionInputs.tsx @@ -1,4 +1,4 @@ -import { useCommonStyles } from 'components/common/styles'; +import { PanelSection } from 'components/common/PanelSection'; import { WaitForData } from 'components/common/WaitForData'; import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; import { RemoteLiteralMapViewer } from 'components/Literals/RemoteLiteralMapViewer'; @@ -7,22 +7,15 @@ import * as React from 'react'; /** Fetches and renders the input data for a given `NodeExecution` */ export const NodeExecutionInputs: React.FC<{ execution: NodeExecution }> = ({ execution }) => { - const commonStyles = useCommonStyles(); const executionData = useNodeExecutionData(execution.id); return ( - {() => ( - <> -
-
- -
-
- - )} + + +
); }; diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionOutputs.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionTabs/NodeExecutionOutputs.tsx similarity index 56% rename from src/components/Executions/ExecutionDetails/NodeExecutionOutputs.tsx rename to src/components/Executions/ExecutionDetails/NodeExecutionTabs/NodeExecutionOutputs.tsx index ede48700d..1948a6575 100644 --- a/src/components/Executions/ExecutionDetails/NodeExecutionOutputs.tsx +++ b/src/components/Executions/ExecutionDetails/NodeExecutionTabs/NodeExecutionOutputs.tsx @@ -1,4 +1,4 @@ -import { useCommonStyles } from 'components/common/styles'; +import { PanelSection } from 'components/common/PanelSection'; import { WaitForData } from 'components/common/WaitForData'; import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; import { RemoteLiteralMapViewer } from 'components/Literals/RemoteLiteralMapViewer'; @@ -7,22 +7,15 @@ import * as React from 'react'; /** Fetches and renders the output data for a given `NodeExecution` */ export const NodeExecutionOutputs: React.FC<{ execution: NodeExecution }> = ({ execution }) => { - const commonStyles = useCommonStyles(); const executionData = useNodeExecutionData(execution.id); return ( - {() => ( - <> -
-
- -
-
- - )} + + +
); }; diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx new file mode 100644 index 000000000..c3c763845 --- /dev/null +++ b/src/components/Executions/ExecutionDetails/NodeExecutionTabs/index.tsx @@ -0,0 +1,82 @@ +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 { TaskTemplate } from 'models/Task/types'; +import { useTabState } from 'components/hooks/useTabState'; +import { PanelSection } from 'components/common/PanelSection'; +import { DumpJSON } from 'components/common/DumpJSON'; +import { TaskExecutionsList } from '../../TaskExecutionsList/TaskExecutionsList'; +import { NodeExecutionInputs } from './NodeExecutionInputs'; +import { NodeExecutionOutputs } from './NodeExecutionOutputs'; + +const useStyles = makeStyles((theme) => { + return { + content: { + overflowY: 'auto', + }, + tabs: { + borderBottom: `1px solid ${theme.palette.divider}`, + }, + }; +}); + +const tabIds = { + executions: 'executions', + inputs: 'inputs', + outputs: 'outputs', + task: 'task', +}; + +const defaultTab = tabIds.executions; + +export const NodeExecutionTabs: React.FC<{ + nodeExecution: NodeExecution; + taskTemplate?: TaskTemplate | null; +}> = ({ nodeExecution, taskTemplate }) => { + const styles = useStyles(); + const tabState = useTabState(tabIds, defaultTab); + + if (tabState.value === tabIds.task && !taskTemplate) { + // Reset tab value, if task tab is selected, while no taskTemplate is avaible + // can happen when user switches between nodeExecutions without closing the drawer + tabState.onChange(() => { + /* */ + }, defaultTab); + } + + let tabContent: JSX.Element | null = null; + switch (tabState.value) { + case tabIds.executions: { + tabContent = ; + break; + } + case tabIds.inputs: { + tabContent = ; + break; + } + case tabIds.outputs: { + tabContent = ; + break; + } + case tabIds.task: { + tabContent = taskTemplate ? ( + + + + ) : null; + break; + } + } + return ( + <> + + + + + {!!taskTemplate && } + +
{tabContent}
+ + ); +}; diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionTaskDetails.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionTaskDetails.tsx deleted file mode 100644 index 043c0d60a..000000000 --- a/src/components/Executions/ExecutionDetails/NodeExecutionTaskDetails.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { DumpJSON } from 'components/common/DumpJSON'; -import { useCommonStyles } from 'components/common/styles'; -import { DataError } from 'components/Errors/DataError'; -import { TaskTemplate } from 'models/Task/types'; -import * as React from 'react'; - -/** Render the task template for a given NodeExecution */ -export const NodeExecutionTaskDetails: React.FC<{ - taskTemplate: TaskTemplate; -}> = ({ taskTemplate }) => { - const commonStyles = useCommonStyles(); - const content = taskTemplate ? ( - - ) : ( - - ); - return ( -
-
{content}
-
- ); -}; diff --git a/src/components/Executions/TaskExecutionsList/TaskExecutions.mocks.ts b/src/components/Executions/TaskExecutionsList/TaskExecutions.mocks.ts new file mode 100644 index 000000000..8fc2432f3 --- /dev/null +++ b/src/components/Executions/TaskExecutionsList/TaskExecutions.mocks.ts @@ -0,0 +1,53 @@ +import { Protobuf } from 'flyteidl'; +import { MessageFormat, ResourceType } from 'models/Common/types'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { TaskExecution } from 'models/Execution/types'; + +import * as Long from 'long'; + +// we probably will create a new helper function in future, to make testing/storybooks closer to what we see in API Json responses +const getProtobufTimestampFromIsoTime = (isoDateTime: string): Protobuf.ITimestamp => { + const timeMs = Date.parse(isoDateTime); + const timestamp = new Protobuf.Timestamp(); + timestamp.seconds = Long.fromInt(Math.floor(timeMs / 1000)); + timestamp.nanos = (timeMs % 1000) * 1e6; + return timestamp; +}; + +export const MockPythonTaskExecution: TaskExecution = { + id: { + taskId: { + resourceType: ResourceType.TASK, + project: 'flytesnacks', + domain: 'development', + name: 'athena.workflows.example.say_hello', + version: 'v13', + }, + nodeExecutionId: { + nodeId: 'ff65vi3y', + executionId: { + project: 'flytesnacks', + domain: 'development', + name: 'ogaayir2e3', + }, + }, + }, + inputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-ogaayir2e3/athenaworkflowsexamplesayhello/data/inputs.pb', + closure: { + outputUri: + 's3://flyte-demo/metadata/propeller/flytesnacks-development-ogaayir2e3/athenaworkflowsexamplesayhello/data/0/outputs.pb', + phase: TaskExecutionPhase.SUCCEEDED, + logs: [ + { + uri: 'https://console.aws.amazon.com/cloudwatch/home?region=us-east-2#logEventViewer:group=/aws/containerinsights/flyte-demo-2/application;stream=var.log.containers.ogaayir2e3-ff65vi3y-0_flytesnacks-development_ogaayir2e3-ff65vi3y-0-380d210ccaac45a6e2314a155822b36a67e044914069d01323bc18832487ac4a.log', + name: 'Cloudwatch Logs (User)', + messageFormat: MessageFormat.JSON, + }, + ], + createdAt: getProtobufTimestampFromIsoTime('2022-03-17T21:30:53.469624134Z'), + updatedAt: getProtobufTimestampFromIsoTime('2022-03-17T21:31:04.011303736Z'), + reason: 'task submitted to K8s', + taskType: 'python-task', + }, +}; diff --git a/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx b/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx index 16452e788..0a991672b 100644 --- a/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx +++ b/src/components/Executions/TaskExecutionsList/TaskExecutionsList.tsx @@ -18,7 +18,7 @@ interface TaskExecutionsListProps { nodeExecution: NodeExecution; } -const TaskExecutionsListContent: React.FC<{ +export const TaskExecutionsListContent: React.FC<{ taskExecutions: TaskExecution[]; }> = ({ taskExecutions }) => { const styles = useStyles(); diff --git a/src/components/Executions/TaskExecutionsList/TaskExecutionsListContent.stories.tsx b/src/components/Executions/TaskExecutionsList/TaskExecutionsListContent.stories.tsx new file mode 100644 index 000000000..4b7823ecd --- /dev/null +++ b/src/components/Executions/TaskExecutionsList/TaskExecutionsListContent.stories.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { TaskExecutionPhase } from 'models/Execution/enums'; +import { PanelViewDecorator } from 'components/common/__stories__/Decorators'; +import { TaskExecutionsListContent } from './TaskExecutionsList'; +import { MockPythonTaskExecution } from './TaskExecutions.mocks'; + +export default { + title: 'Task/NodeExecutionTabs', + component: TaskExecutionsListContent, +} as ComponentMeta; + +// 👇 We create a “template” of how args map to rendering +const Template: ComponentStory = (args) => ( + +); + +export const PythonTaskExecution = Template.bind({}); +PythonTaskExecution.args = { taskExecutions: [MockPythonTaskExecution] }; + +export const PythonTaskWithRetry = Template.bind({}); +PythonTaskWithRetry.decorators = [(Story) => PanelViewDecorator(Story)]; +PythonTaskWithRetry.args = { + taskExecutions: [ + { + ...MockPythonTaskExecution, + closure: { ...MockPythonTaskExecution.closure, phase: TaskExecutionPhase.FAILED }, + }, + { ...MockPythonTaskExecution, id: { ...MockPythonTaskExecution.id, retryAttempt: 1 } }, + ], +}; diff --git a/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx b/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx index c6f73c5f7..ac899606e 100644 --- a/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx +++ b/src/components/Executions/TaskExecutionsList/TaskExecutionsListItem.tsx @@ -2,6 +2,7 @@ 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'; @@ -45,32 +46,30 @@ export const TaskExecutionsListItem: React.FC = ({ const taskHasStarted = closure.phase >= TaskExecutionPhase.QUEUED; return ( -
-
+ +
+
+ + {headerText} + +
+ +
+ {!!error && (
-
- - {headerText} - -
- +
- {!!error && ( + )} + {taskHasStarted && ( + <>
- +
- )} - {taskHasStarted && ( - <> -
- -
-
- -
- - )} -
-
+
+ +
+ + )} + ); }; diff --git a/src/components/Executions/TaskExecutionsList/utils.ts b/src/components/Executions/TaskExecutionsList/utils.ts index aad1ce3fc..8d71d2809 100644 --- a/src/components/Executions/TaskExecutionsList/utils.ts +++ b/src/components/Executions/TaskExecutionsList/utils.ts @@ -9,7 +9,7 @@ import { TaskExecution } from 'models/Execution/types'; export function getUniqueTaskExecutionName({ id }: TaskExecution) { const { name } = id.taskId; const { retryAttempt } = id; - const suffix = retryAttempt > 0 ? ` (${retryAttempt + 1})` : ''; + const suffix = retryAttempt && retryAttempt > 0 ? ` (${retryAttempt + 1})` : ''; return `${name}${suffix}`; } diff --git a/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx b/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx index 4ea05d997..1927630cf 100644 --- a/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx +++ b/src/components/common/MapTaskExecutionsList/MapTaskStatusInfo.stories.tsx @@ -5,7 +5,7 @@ import { MapTaskStatusInfo } from './MapTaskStatusInfo'; import { PanelViewDecorator } from '../__stories__/Decorators'; export default { - title: 'Common/MapTaskExecutionList/MapTaskStatusInfo', + title: 'Task/MapTaskExecutionList/MapTaskStatusInfo', component: MapTaskStatusInfo, parameters: { actions: { argTypesRegex: 'toggleExpanded' } }, } as ComponentMeta; diff --git a/src/components/common/PanelSection/index.tsx b/src/components/common/PanelSection/index.tsx new file mode 100644 index 000000000..23b229699 --- /dev/null +++ b/src/components/common/PanelSection/index.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyle = makeStyles((theme) => ({ + detailsPanelCard: { + borderBottom: `1px solid ${theme.palette.divider}`, + }, + detailsPanelCardContent: { + padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, + }, +})); + +interface PanelSectionProps { + children: React.ReactNode; +} + +export const PanelSection = (props: PanelSectionProps) => { + const commonStyles = useStyle(); + return ( +
+
{props.children}
+
+ ); +}; diff --git a/src/components/common/__stories__/Decorators.tsx b/src/components/common/__stories__/Decorators.tsx index 362dccd34..9a2e9de1c 100644 --- a/src/components/common/__stories__/Decorators.tsx +++ b/src/components/common/__stories__/Decorators.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -const PANEL_VIEW = '320px'; +const PANEL_VIEW = '420px'; export function PanelViewDecorator(Story: any) { return ( diff --git a/src/components/common/styles.ts b/src/components/common/styles.ts index 4f2922fea..2d9df5bdb 100644 --- a/src/components/common/styles.ts +++ b/src/components/common/styles.ts @@ -48,12 +48,6 @@ export const useCommonStyles = makeStyles((theme: Theme) => ({ color: dangerousButtonHoverColor, }, }, - detailsPanelCard: { - borderBottom: `1px solid ${theme.palette.divider}`, - }, - detailsPanelCardContent: { - padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`, - }, errorText: { color: theme.palette.error.main, }, diff --git a/src/models/Common/types.ts b/src/models/Common/types.ts index 1d179d611..e93e63c22 100644 --- a/src/models/Common/types.ts +++ b/src/models/Common/types.ts @@ -19,6 +19,8 @@ export type BlobDimensionality = Core.BlobType.BlobDimensionality; export const BlobDimensionality = Core.BlobType.BlobDimensionality; export type SchemaColumnType = Core.SchemaType.SchemaColumn.SchemaColumnType; export const SchemaColumnType = Core.SchemaType.SchemaColumn.SchemaColumnType; +export type MessageFormat = Core.TaskLog.MessageFormat; +export const MessageFormat = Core.TaskLog.MessageFormat; /* eslint-enable @typescript-eslint/no-redeclare */ export type Alias = Core.IAlias; @@ -49,7 +51,7 @@ export type Notification = Admin.INotification; export type RetryStrategy = Core.IRetryStrategy; export type RuntimeMetadata = Core.IRuntimeMetadata; export type Schedule = Admin.ISchedule; -export type MessageFormat = Core.TaskLog.MessageFormat; + export interface TaskLog extends Core.ITaskLog { name: string; uri: string; diff --git a/src/models/Execution/types.ts b/src/models/Execution/types.ts index 9d981ff22..e0a4c9e8f 100644 --- a/src/models/Execution/types.ts +++ b/src/models/Execution/types.ts @@ -107,13 +107,13 @@ export interface NodeExecutionClosure extends Admin.INodeExecutionClosure { export interface TaskExecutionIdentifier extends Core.ITaskExecutionIdentifier { taskId: Identifier; nodeExecutionId: NodeExecutionIdentifier; - retryAttempt: number; + retryAttempt?: number; } export interface TaskExecution extends Admin.ITaskExecution { id: TaskExecutionIdentifier; inputUri: string; - isParent: boolean; + isParent?: boolean; closure: TaskExecutionClosure; } export interface TaskExecutionClosure extends Admin.ITaskExecutionClosure {