Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update node executions to display map tasks #455

Merged
merged 10 commits into from
May 13, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ 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 { ExternalResource, 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 { useTaskExecutions, useTaskExecutionsRefresher } from '../useTaskExecutions';
import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent';

export interface ExecutionWorkflowGraphProps {
Expand All @@ -23,12 +25,34 @@ export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({
workflowId,
}) => {
const workflowQuery = useQuery<Workflow, Error>(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 = taskExecutions.value
anrusina marked this conversation as resolved.
Show resolved Hide resolved
.map((taskExecution) => taskExecution.closure.metadata?.externalResources)
.filter((resources) => resources?.length);

const externalResourcesByPhase = new Map();
externalResources.forEach((resource) => {
if (resource) {
externalResourcesByPhase.set(resource[0].phase, resource);
}
});

return {
...nodeExecution,
...(externalResourcesByPhase.size > 0 && { externalResourcesByPhase }),
};
});

const nodeExecutionsById = useMemo(
() => keyBy(nodeExecutionsWithResources, 'scopedId'),
[nodeExecutionsWithResources],
);

const [selectedNodes, setSelectedNodes] = React.useState<string[]>([]);
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const onNodeSelectionChanged = (newSelection: string[]) => {
const validSelection = newSelection.filter((nodeId) => {
if (nodeId === startNodeId || nodeId === endNodeId) {
Expand All @@ -52,9 +76,15 @@ export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({

const onCloseDetailsPanel = () => setSelectedNodes([]);

const [selectedMapTask, setSelectedMapTask] = useState<ExternalResource[] | null>(null);
const onMapTaskSelectionChanged = (newSelection: ExternalResource[] | null) => {
setSelectedMapTask(newSelection);
olga-union marked this conversation as resolved.
Show resolved Hide resolved
};

const renderGraph = (workflow: Workflow) => (
<WorkflowGraph
onNodeSelectionChanged={onNodeSelectionChanged}
onMapTaskSelectionChanged={onMapTaskSelectionChanged}
nodeExecutionsById={nodeExecutionsById}
workflow={workflow}
/>
Expand All @@ -71,6 +101,7 @@ export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({
{selectedExecution && (
<NodeExecutionDetailsPanelContent
onClose={onCloseDetailsPanel}
mapTask={selectedMapTask}
nodeExecutionId={selectedExecution}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,7 +14,12 @@ import { useTabState } from 'components/hooks/useTabState';
import { LocationDescriptor } from 'history';
import { PaginatedEntityResponse } from 'models/AdminEntity/types';
import { Workflow } from 'models/Workflow/types';
import { NodeExecution, NodeExecutionIdentifier, TaskExecution } from 'models/Execution/types';
import {
ExternalResource,
NodeExecution,
NodeExecutionIdentifier,
TaskExecution,
} from 'models/Execution/types';
import Skeleton from 'react-loading-skeleton';
import { useQuery, useQueryClient } from 'react-query';
import { Link as RouterLink } from 'react-router-dom';
Expand Down Expand Up @@ -126,6 +132,7 @@ const tabIds = {

interface NodeExecutionDetailsProps {
nodeExecutionId: NodeExecutionIdentifier;
mapTask?: ExternalResource[] | null;
onClose?: () => void;
}

Expand Down Expand Up @@ -218,8 +225,21 @@ const WorkflowTabs: React.FC<{
*/
export const NodeExecutionDetailsPanelContent: React.FC<NodeExecutionDetailsProps> = ({
nodeExecutionId,
mapTask,
onClose,
}) => {
const commonStyles = useCommonStyles();
const styles = useStyles();
const queryClient = useQueryClient();
const detailsContext = useNodeExecutionContext();

const [isReasonsVisible, setReasonsVisible] = useState<boolean>(false);
const [dag, setDag] = useState<any>(null);
const [details, setDetails] = useState<NodeExecutionDetails | undefined>();
const [shouldShowMapTaskInfo, setShouldShowMapTaskInfo] = useState<boolean>(
mapTask ? true : false,
);

const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
Expand All @@ -228,21 +248,14 @@ export const NodeExecutionDetailsPanelContent: React.FC<NodeExecutionDetailsProp
};
}, []);

const queryClient = useQueryClient();
const detailsContext = useNodeExecutionContext();

const [isReasonsVisible, setReasonsVisible] = React.useState(false);
const [dag, setDag] = React.useState<any>(null);
const [details, setDetails] = React.useState<NodeExecutionDetails | undefined>();

const nodeExecutionQuery = useQuery<NodeExecution, Error>({
...makeNodeExecutionQuery(nodeExecutionId),
// The selected NodeExecution has been fetched at this point, we don't want to
// issue an additional fetch.
staleTime: Infinity,
});

React.useEffect(() => {
useEffect(() => {
let isCurrent = true;
detailsContext.getNodeExecutionDetails(nodeExecution).then((res) => {
if (isCurrent) {
Expand All @@ -255,10 +268,14 @@ export const NodeExecutionDetailsPanelContent: React.FC<NodeExecutionDetailsProp
};
});

React.useEffect(() => {
useEffect(() => {
setReasonsVisible(false);
}, [nodeExecutionId]);

useEffect(() => {
setShouldShowMapTaskInfo(mapTask ? true : false);
}, [mapTask, nodeExecutionId]);

const nodeExecution = nodeExecutionQuery.data;

const getWorkflowDag = async () => {
Expand Down Expand Up @@ -288,25 +305,51 @@ export const NodeExecutionDetailsPanelContent: React.FC<NodeExecutionDetailsProp

const reasons = getTaskExecutionDetailReasons(listTaskExecutionsQuery.data);

const commonStyles = useCommonStyles();
const styles = useStyles();
const displayName = details?.displayName ?? <Skeleton />;
const onBackClick = () => {
setShouldShowMapTaskInfo(false);
};

const isRunningPhase = React.useMemo(() => {
const headerTitle = useMemo(() => {
// 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 = shouldShowMapTaskInfo ? mapTaskHeader : nodeExecutionId.nodeId;

return (
<Typography className={classnames(commonStyles.textWrapped, styles.title)} variant="h3">
<div>
{shouldShowMapTaskInfo && (
<IconButton onClick={onBackClick} size="small">
<ArrowBackIos />
</IconButton>
)}
{header}
</div>
<IconButton className={styles.closeButton} onClick={onClose} size="small">
<Close />
</IconButton>
</Typography>
);
}, [mapTask, nodeExecutionId, shouldShowMapTaskInfo]);

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 ? (
<div className={styles.statusContainer}>
<div className={styles.statusHeaderContainer}>
<ExecutionStatusBadge phase={nodeExecution.closure.phase} type="node" />
<ExecutionStatusBadge phase={nodeExecution?.closure.phase} type="node" />
{isRunningPhase && (
<InfoIcon className={styles.reasonsIcon} onClick={handleReasonsVisibility} />
)}
Expand All @@ -321,26 +364,32 @@ export const NodeExecutionDetailsPanelContent: React.FC<NodeExecutionDetailsProp
<div className={styles.notRunStatus}>NOT RUN</div>
);

const detailsContent = nodeExecution ? (
<>
<NodeExecutionCacheStatus taskNodeMetadata={nodeExecution.closure.taskNodeMetadata} />
<ExecutionTypeDetails details={details} execution={nodeExecution} />
</>
) : null;
let detailsContent: JSX.Element | null = null;
if (nodeExecution) {
detailsContent = (
<>
<NodeExecutionCacheStatus taskNodeMetadata={nodeExecution.closure.taskNodeMetadata} />
<ExecutionTypeDetails details={details} execution={nodeExecution} />
</>
);
}

const tabsContent = nodeExecution ? (
<NodeExecutionTabs nodeExecution={nodeExecution} taskTemplate={details?.taskTemplate} />
const tabsContent: JSX.Element | null = nodeExecution ? (
<NodeExecutionTabs
nodeExecution={nodeExecution}
shouldShowMapTaskInfo={shouldShowMapTaskInfo}
mapTask={mapTask}
taskTemplate={details?.taskTemplate}
/>
) : null;

const displayName = details?.displayName ?? <Skeleton />;

return (
<section className={styles.container}>
<header className={styles.header}>
<div className={styles.headerContent}>
<Typography className={classnames(commonStyles.textWrapped, styles.title)} variant="h3">
{nodeExecutionId.nodeId}
<IconButton className={styles.closeButton} onClick={onClose} size="small">
<Close />
</IconButton>
</Typography>
{headerTitle}
<Typography
className={classnames(commonStyles.textWrapped, styles.displayId)}
variant="subtitle1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { ExternalResource } from 'models/Execution/types';
import { Core } from 'flyteidl';
import { Typography } from '@material-ui/core';
import classnames from 'classnames';
import { useCommonStyles } from 'components/common/styles';
import { TaskExecutionPhase } from 'models/Execution/enums';
import { ExecutionStatusBadge } from '../../ExecutionStatusBadge';
import { TaskExecutionLogs } from '../../TaskExecutionsList/TaskExecutionLogs';
import { formatRetryAttempt } from '../../TaskExecutionsList/utils';

const useStyles = makeStyles((theme) => {
return {
detailsPanelCardContent: {
padding: `${theme.spacing(2)}px ${theme.spacing(3)}px`,
borderBottom: `1px solid ${theme.palette.divider}`,
},
section: {
marginBottom: theme.spacing(2),
},
header: {
marginBottom: theme.spacing(1),
},
title: {
marginBottom: theme.spacing(1),
},
logLink: {
margin: `${theme.spacing(0.5)} 0`,
},
logName: {
fontWeight: 'lighter',
},
};
});

const MapTaskLogs: React.FC<{
phase?: TaskExecutionPhase | null;
retryAttempt?: number | null;
logs?: Core.ITaskLog[] | null;
}> = ({ phase, retryAttempt, logs }) => {
const commonStyles = useCommonStyles();
const styles = useStyles();
const headerText = formatRetryAttempt(retryAttempt ?? 0);

return (
<div className={styles.detailsPanelCardContent}>
<section className={styles.section}>
<header className={styles.header}>
<Typography variant="h6" className={classnames(styles.title, commonStyles.textWrapped)}>
{headerText}
</Typography>
</header>
{phase && <ExecutionStatusBadge phase={phase} type="task" variant="text" />}
</section>
<section className={styles.section}>
<TaskExecutionLogs taskLogs={logs || []} />
</section>
</div>
);
};

export const MapTaskExecutionsList: React.FC<{
mapTask: ExternalResource[];
}> = ({ mapTask }) => {
return (
<>
{mapTask.map((task) => {
return (
<MapTaskLogs
phase={task.phase}
retryAttempt={task.retryAttempt}
logs={task.logs}
key={task.index}
/>
);
})}
</>
);
};
Loading