diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index 9411dfadd..f2f72028e 100644 --- a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -5,11 +5,17 @@ import Tabs from '@material-ui/core/Tabs'; import Close from '@material-ui/icons/Close'; import * as classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; +import { InfoIcon } from 'components/common/Icons/InfoIcon'; import { ExecutionStatusBadge } from 'components/Executions/ExecutionStatusBadge'; import { LocationState } from 'components/hooks/useLocationState'; import { useTabState } from 'components/hooks/useTabState'; import { LocationDescriptor } from 'history'; -import { NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; +import { PaginatedEntityResponse } from 'models/AdminEntity/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'; @@ -17,13 +23,18 @@ import { useQuery } from 'react-query'; import { Link as RouterLink } from 'react-router-dom'; import { Routes } from 'routes/routes'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; -import { makeNodeExecutionQuery } from '../nodeExecutionQueries'; +import { + makeListTaskExecutionsQuery, + makeNodeExecutionQuery +} from '../nodeExecutionQueries'; import { TaskExecutionsList } from '../TaskExecutionsList/TaskExecutionsList'; import { NodeExecutionDetails } from '../types'; import { useNodeExecutionDetails } from '../useNodeExecutionDetails'; import { NodeExecutionInputs } from './NodeExecutionInputs'; import { NodeExecutionOutputs } from './NodeExecutionOutputs'; import { NodeExecutionTaskDetails } from './NodeExecutionTaskDetails'; +import { getTaskExecutionDetailReasons } from './utils'; +import { ExpandableMonospaceText } from '../../common/ExpandableMonospaceText'; const useStyles = makeStyles((theme: Theme) => { const paddingVertical = `${theme.spacing(2)}px`; @@ -74,6 +85,21 @@ const useStyles = makeStyles((theme: Theme) => { alignItems: 'flex-start', display: 'flex', justifyContent: 'space-between' + }, + statusContainer: { + display: 'flex', + flexDirection: 'column' + }, + statusHeaderContainer: { + display: 'flex', + alignItems: 'center' + }, + reasonsIcon: { + marginLeft: theme.spacing(1), + cursor: 'pointer' + }, + statusBody: { + marginTop: theme.spacing(2) } }; }); @@ -198,6 +224,7 @@ export const NodeExecutionDetailsPanelContent: React.FC { + const [isReasonsVisible, setReasonsVisible] = React.useState(false); const nodeExecutionQuery = useQuery({ ...makeNodeExecutionQuery(nodeExecutionId), // The selected NodeExecution has been fetched at this point, we don't want to @@ -205,8 +232,22 @@ export const NodeExecutionDetailsPanelContent: React.FC { + setReasonsVisible(false); + }, [nodeExecutionId]); + const nodeExecution = nodeExecutionQuery.data; + const listTaskExecutionsQuery = useQuery< + PaginatedEntityResponse, + Error + >({ + ...makeListTaskExecutionsQuery(nodeExecutionId), + staleTime: Infinity + }); + + const reasons = getTaskExecutionDetailReasons(listTaskExecutionsQuery.data); + const commonStyles = useCommonStyles(); const styles = useStyles(); const detailsQuery = useNodeExecutionDetails(nodeExecution); @@ -219,8 +260,40 @@ export const NodeExecutionDetailsPanelContent: React.FC { + return ( + nodeExecution?.closure.phase === 1 || + nodeExecution?.closure.phase === 2 + ); + }, [nodeExecution]); + + const handleReasonsVisibility = React.useCallback(() => { + setReasonsVisible(prevVisibility => !prevVisibility); + }, []); + const statusContent = nodeExecution ? ( - +
+
+ + {isRunningPhase && ( + + )} +
+ {isRunningPhase && isReasonsVisible && ( +
+ +
+ )} +
) : null; const detailsContent = nodeExecution ? ( diff --git a/src/components/Executions/ExecutionDetails/utils.ts b/src/components/Executions/ExecutionDetails/utils.ts index 2aa5ffd57..fad47d26d 100644 --- a/src/components/Executions/ExecutionDetails/utils.ts +++ b/src/components/Executions/ExecutionDetails/utils.ts @@ -1,6 +1,7 @@ import { Identifier, ResourceType } from 'models/Common/types'; -import { Execution } from 'models/Execution/types'; +import { Execution, TaskExecution } from 'models/Execution/types'; import { Routes } from 'routes/routes'; +import { PaginatedEntityResponse } from 'models/AdminEntity/types'; export function isSingleTaskExecution(execution: Execution) { return execution.spec.launchPlan.resourceType === ResourceType.TASK; @@ -18,3 +19,13 @@ export function getExecutionBackLink(execution: Execution): string { ? Routes.TaskDetails.makeUrl(project, domain, name) : Routes.WorkflowDetails.makeUrl(project, domain, name); } + +export function getTaskExecutionDetailReasons( + taskExecutionDetails?: PaginatedEntityResponse +): (string | null | undefined)[] { + return ( + taskExecutionDetails?.entities.map( + taskExecution => taskExecution.closure.reason + ) || [] + ); +} diff --git a/src/components/Executions/nodeExecutionQueries.ts b/src/components/Executions/nodeExecutionQueries.ts index 0cb4e5df5..14c34ed4e 100644 --- a/src/components/Executions/nodeExecutionQueries.ts +++ b/src/components/Executions/nodeExecutionQueries.ts @@ -1,16 +1,21 @@ import { QueryInput, QueryType } from 'components/data/types'; import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; import { isEqual } from 'lodash'; -import { RequestConfig } from 'models/AdminEntity/types'; +import { + PaginatedEntityResponse, + RequestConfig +} from 'models/AdminEntity/types'; import { getNodeExecution, listNodeExecutions, - listTaskExecutionChildren + listTaskExecutionChildren, + listTaskExecutions } from 'models/Execution/api'; import { nodeExecutionQueryParams } from 'models/Execution/constants'; import { NodeExecution, NodeExecutionIdentifier, + TaskExecution, TaskExecutionIdentifier, WorkflowExecutionIdentifier } from 'models/Execution/types'; @@ -45,6 +50,15 @@ export function makeNodeExecutionQuery( }; } +export function makeListTaskExecutionsQuery( + id: NodeExecutionIdentifier +): QueryInput> { + return { + queryKey: [QueryType.TaskExecutionList, id], + queryFn: () => listTaskExecutions(id) + }; +} + /** Composable fetch function which wraps `makeNodeExecutionQuery` */ export function fetchNodeExecution( queryClient: QueryClient, diff --git a/src/components/common/Icons/InfoIcon.tsx b/src/components/common/Icons/InfoIcon.tsx new file mode 100644 index 000000000..edc8e594f --- /dev/null +++ b/src/components/common/Icons/InfoIcon.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { IconProps } from './interface'; + +export const InfoIcon: React.FC = ({ + size = 14, + className, + onClick +}) => { + return ( + + + + + + ); +}; diff --git a/src/components/common/Icons/interface.ts b/src/components/common/Icons/interface.ts new file mode 100644 index 000000000..2f6e7e98a --- /dev/null +++ b/src/components/common/Icons/interface.ts @@ -0,0 +1,5 @@ +export interface IconProps { + size?: number; + className?: string; + onClick?: () => void; +}