diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionChildrenLoader.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionChildrenLoader.tsx deleted file mode 100644 index 9da436efe..000000000 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionChildrenLoader.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { WaitForQuery } from 'components/common/WaitForQuery'; -import { DataError } from 'components/Errors/DataError'; -import * as React from 'react'; -import { NodeExecution } from 'models/Execution/types'; -import { useAllChildNodeExecutionGroupsQuery } from '../nodeExecutionQueries'; -import { NodeExecutionsRequestConfigContext } from '../contexts'; -import { ExecutionWorkflowGraph } from './ExecutionWorkflowGraph'; - -export const ExecutionChildrenLoader = ({ nodeExecutions, workflowId }) => { - const requestConfig = React.useContext(NodeExecutionsRequestConfigContext); - const childGroupsQuery = useAllChildNodeExecutionGroupsQuery(nodeExecutions, requestConfig); - - const renderGraphComponent = (childGroups) => { - const output: any[] = []; - for (let i = 0; i < childGroups.length; i++) { - for (let j = 0; j < childGroups[i].length; j++) { - for (let k = 0; k < childGroups[i][j].nodeExecutions.length; k++) { - output.push(childGroups[i][j].nodeExecutions[k] as NodeExecution); - } - } - } - const executions: NodeExecution[] = output.concat(nodeExecutions); - return nodeExecutions.length > 0 ? ( - - ) : null; - }; - - return ( - - {renderGraphComponent} - - ); -}; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 2a8afdd49..006db91d3 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -5,16 +5,24 @@ import { WaitForQuery } from 'components/common/WaitForQuery'; import { DataError } from 'components/Errors/DataError'; import { useTabState } from 'components/hooks/useTabState'; import { secondaryBackgroundColor } from 'components/Theme/constants'; -import { Execution, NodeExecution } from 'models/Execution/types'; +import { Execution, ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types'; +import { useContext, useEffect, useMemo, useState } from 'react'; +import { keyBy } from 'lodash'; +import { isMapTaskV1 } from 'models/Task/utils'; +import { useQueryClient } from 'react-query'; +import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionsRequestConfigContext } from '../contexts'; +import { NodeExecutionsByIdContext, NodeExecutionsRequestConfigContext } from '../contexts'; import { ExecutionFilters } from '../ExecutionFilters'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { tabs } from './constants'; -import { ExecutionChildrenLoader } from './ExecutionChildrenLoader'; import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; import { ExecutionNodesTimeline } from './Timeline'; +import { fetchTaskExecutionList } from '../taskExecutionQueries'; +import { getGroupedLogs } from '../TaskExecutionsList/utils'; +import { useAllTreeNodeExecutionGroupsQuery } from '../nodeExecutionQueries'; +import { ExecutionWorkflowGraph } from './ExecutionWorkflowGraph'; const useStyles = makeStyles((theme: Theme) => ({ filters: { @@ -31,8 +39,15 @@ const useStyles = makeStyles((theme: Theme) => ({ background: secondaryBackgroundColor, paddingLeft: theme.spacing(3.5), }, + loading: { + margin: 'auto', + }, })); +interface WorkflowNodeExecution extends NodeExecution { + logsByPhase?: LogsByPhase; +} + export interface ExecutionNodeViewsProps { execution: Execution; } @@ -43,11 +58,22 @@ export const ExecutionNodeViews: React.FC = ({ executio const styles = useStyles(); const filterState = useNodeExecutionFiltersState(); const tabState = useTabState(tabs, defaultTab); + const queryClient = useQueryClient(); + const requestConfig = useContext(NodeExecutionsRequestConfigContext); const { - closure: { abortMetadata }, + closure: { abortMetadata, workflowId }, } = execution; + const [nodeExecutions, setNodeExecutions] = useState([]); + const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState< + WorkflowNodeExecution[] + >([]); + + const nodeExecutionsById = useMemo(() => { + return keyBy(nodeExecutionsWithResources, 'scopedId'); + }, [nodeExecutionsWithResources]); + /* We want to maintain the filter selection when switching away from the Nodes tab and back, but do not want to filter the nodes when viewing the graph. So, we will only pass filters to the execution state when on the nodes tab. */ @@ -58,6 +84,61 @@ export const ExecutionNodeViews: React.FC = ({ executio appliedFilters, ); + useEffect(() => { + let isCurrent = true; + async function fetchData(baseNodeExecutions, queryClient) { + const newValue = await Promise.all( + baseNodeExecutions.map(async (baseNodeExecution) => { + const taskExecutions = await fetchTaskExecutionList(queryClient, baseNodeExecution.id); + + const useNewMapTaskView = taskExecutions.every((taskExecution) => { + const { + closure: { taskType, metadata, eventVersion = 0 }, + } = taskExecution; + return isMapTaskV1( + eventVersion, + metadata?.externalResources?.length ?? 0, + taskType ?? undefined, + ); + }); + const externalResources: ExternalResource[] = taskExecutions + .map((taskExecution) => taskExecution.closure.metadata?.externalResources) + .flat() + .filter((resource): resource is ExternalResource => !!resource); + + const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); + + return { + ...baseNodeExecution, + ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), + }; + }), + ); + + if (isCurrent) { + setNodeExecutionsWithResources(newValue); + } + } + + if (nodeExecutions.length > 0) { + fetchData(nodeExecutions, queryClient); + } + return () => { + isCurrent = false; + }; + }, [nodeExecutions]); + + const childGroupsQuery = useAllTreeNodeExecutionGroupsQuery( + nodeExecutionsQuery.data ?? [], + requestConfig, + ); + + useEffect(() => { + if (!childGroupsQuery.isLoading && childGroupsQuery.data) { + setNodeExecutions(childGroupsQuery.data); + } + }, [childGroupsQuery.data]); + const renderNodeExecutionsTable = (nodeExecutions: NodeExecution[]) => ( = ({ executio ); - const renderExecutionLoader = (nodeExecutions: NodeExecution[]) => { + const renderExecutionChildrenLoader = () => + nodeExecutions.length > 0 ? : null; + + const renderExecutionLoader = () => { return ( - + + {renderExecutionChildrenLoader} + ); }; - const renderExecutionsTimeline = (nodeExecutions: NodeExecution[]) => ( - + const renderExecutionsTimeline = () => ( + + {() => } + ); + const TimelineLoading = () => { + return ( +
+ +
+ ); + }; + return ( <> @@ -87,29 +184,31 @@ export const ExecutionNodeViews: React.FC = ({ executio - -
- {tabState.value === tabs.nodes.id && ( - <> -
- -
+ + +
+ {tabState.value === tabs.nodes.id && ( + <> +
+ +
+ + {renderNodeExecutionsTable} + + + )} + {tabState.value === tabs.graph.id && ( + + {renderExecutionLoader} + + )} + {tabState.value === tabs.timeline.id && ( - {renderNodeExecutionsTable} + {renderExecutionsTimeline} - - )} - {tabState.value === tabs.graph.id && ( - - {renderExecutionLoader} - - )} - {tabState.value === tabs.timeline.id && ( - - {renderExecutionsTimeline} - - )} -
+ )} +
+
); diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx index b0181cdec..72165b735 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx @@ -3,46 +3,27 @@ import { WaitForQuery } from 'components/common/WaitForQuery'; import { DataError } from 'components/Errors/DataError'; import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; -import { keyBy } from 'lodash'; import { TaskExecutionPhase } from 'models/Execution/enums'; -import { ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types'; import { endNodeId, startNodeId } from 'models/Node/constants'; -import { isMapTaskV1 } from 'models/Task/utils'; import { Workflow, WorkflowId } from 'models/Workflow/types'; import * as React from 'react'; -import { useEffect, useMemo, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useQuery, useQueryClient } from 'react-query'; -import { NodeExecutionsContext } from '../contexts'; -import { fetchTaskExecutionList } from '../taskExecutionQueries'; -import { getGroupedLogs } from '../TaskExecutionsList/utils'; +import { NodeExecutionsByIdContext } from '../contexts'; import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; export interface ExecutionWorkflowGraphProps { - nodeExecutions: NodeExecution[]; workflowId: WorkflowId; } -interface WorkflowNodeExecution extends NodeExecution { - logsByPhase?: LogsByPhase; -} - /** Wraps a WorkflowGraph, customizing it to also show execution statuses */ -export const ExecutionWorkflowGraph: React.FC = ({ - nodeExecutions, - workflowId, -}) => { +export const ExecutionWorkflowGraph: React.FC = ({ workflowId }) => { const queryClient = useQueryClient(); const workflowQuery = useQuery(makeWorkflowQuery(queryClient, workflowId)); - const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState< - WorkflowNodeExecution[] - >([]); const [selectedNodes, setSelectedNodes] = useState([]); + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); - const nodeExecutionsById = useMemo( - () => keyBy(nodeExecutionsWithResources, 'scopedId'), - [nodeExecutionsWithResources], - ); // Note: flytegraph allows multiple selection, but we only support showing // a single item in the details panel const selectedExecution = selectedNodes.length @@ -57,49 +38,6 @@ export const ExecutionWorkflowGraph: React.FC = ({ const [selectedPhase, setSelectedPhase] = useState(undefined); const [isDetailsTabClosed, setIsDetailsTabClosed] = useState(!selectedExecution); - useEffect(() => { - let isCurrent = true; - async function fetchData(nodeExecutions, queryClient) { - const newValue = await Promise.all( - nodeExecutions.map(async (nodeExecution) => { - const taskExecutions = await fetchTaskExecutionList(queryClient, nodeExecution.id); - - const useNewMapTaskView = taskExecutions.every((taskExecution) => { - const { - closure: { taskType, metadata, eventVersion = 0 }, - } = taskExecution; - return isMapTaskV1( - eventVersion, - metadata?.externalResources?.length ?? 0, - taskType ?? undefined, - ); - }); - const externalResources: ExternalResource[] = taskExecutions - .map((taskExecution) => taskExecution.closure.metadata?.externalResources) - .flat() - .filter((resource): resource is ExternalResource => !!resource); - - const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); - - return { - ...nodeExecution, - ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), - }; - }), - ); - - if (isCurrent) { - setNodeExecutionsWithResources(newValue); - } - } - - fetchData(nodeExecutions, queryClient); - - return () => { - isCurrent = false; - }; - }, [nodeExecutions]); - useEffect(() => { setIsDetailsTabClosed(!selectedExecution); }, [selectedExecution]); @@ -126,18 +64,15 @@ export const ExecutionWorkflowGraph: React.FC = ({ selectedPhase={selectedPhase} onPhaseSelectionChanged={setSelectedPhase} isDetailsTabClosed={isDetailsTabClosed} - nodeExecutionsById={nodeExecutionsById} workflow={workflow} /> ); return ( <> - - - {renderGraph} - - + + {renderGraph} + {selectedExecution && ( > = (props) => { const { node, config, selected } = props; - const nodeExecutions = React.useContext(NodeExecutionsContext); - const nodeExecution = nodeExecutions[node.id]; + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const nodeExecution = nodeExecutionsById[node.id]; const phase = nodeExecution ? nodeExecution.closure.phase : NodeExecutionPhase.UNDEFINED; const { badgeColor: color } = getNodeExecutionPhaseConstants(phase); diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 677cf7e2a..e15b6c31f 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -6,8 +6,12 @@ import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWo import { isEndNode, isStartNode, isExpanded } from 'components/WorkflowGraph/utils'; import { tableHeaderColor } from 'components/Theme/constants'; import { timestampToDate } from 'common/utils'; -import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; +import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; +import { useQuery } from 'react-query'; +import { createRef, useContext, useEffect, useRef, useState } from 'react'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { checkForDynamicExecutions } from 'components/common/utils'; import { convertToPlainNodes } from './helpers'; import { ChartHeader } from './chartHeader'; import { useScaleContext } from './scaleContext'; @@ -67,40 +71,50 @@ const useStyles = makeStyles((theme) => ({ const INTERVAL_LENGTH = 110; interface ExProps { - nodeExecutions: NodeExecution[]; chartTimezone: string; } -export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTimezone }) => { - const [chartWidth, setChartWidth] = React.useState(0); - const [labelInterval, setLabelInterval] = React.useState(INTERVAL_LENGTH); - const durationsRef = React.useRef(null); - const durationsLabelsRef = React.useRef(null); - const taskNamesRef = React.createRef(); +export const ExecutionTimeline: React.FC = ({ chartTimezone }) => { + const [chartWidth, setChartWidth] = useState(0); + const [labelInterval, setLabelInterval] = useState(INTERVAL_LENGTH); + const durationsRef = useRef(null); + const durationsLabelsRef = useRef(null); + const taskNamesRef = createRef(); - const [originalNodes, setOriginalNodes] = React.useState([]); - const [showNodes, setShowNodes] = React.useState([]); - const [startedAt, setStartedAt] = React.useState(new Date()); + const [originalNodes, setOriginalNodes] = useState([]); + const [showNodes, setShowNodes] = useState([]); + const [startedAt, setStartedAt] = useState(new Date()); const { compiledWorkflowClosure } = useNodeExecutionContext(); const { chartInterval: chartTimeInterval } = useScaleContext(); + const { staticExecutionIdsMap } = compiledWorkflowClosure + ? transformerWorkflowToDag(compiledWorkflowClosure) + : []; - React.useEffect(() => { + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + + const dynamicParents = checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap); + + const { data: dynamicWorkflows } = useQuery( + makeNodeExecutionDynamicWorkflowQuery(dynamicParents), + ); + + useEffect(() => { const nodes: dNode[] = compiledWorkflowClosure - ? transformerWorkflowToDag(compiledWorkflowClosure).dag.nodes + ? transformerWorkflowToDag(compiledWorkflowClosure, dynamicWorkflows).dag.nodes : []; // we remove start/end node info in the root dNode list during first assignment const initializeNodes = convertToPlainNodes(nodes); setOriginalNodes(initializeNodes); - }, [compiledWorkflowClosure]); + }, [dynamicWorkflows, compiledWorkflowClosure]); - React.useEffect(() => { + useEffect(() => { const initializeNodes = convertToPlainNodes(originalNodes); const updatedShownNodesMap = initializeNodes.map((node) => { - const index = nodeExecutions.findIndex((exe) => exe.scopedId === node.scopedId); + const execution = nodeExecutionsById[node.scopedId]; return { ...node, - execution: index >= 0 ? nodeExecutions[index] : undefined, + execution, }; }); setShowNodes(updatedShownNodesMap); @@ -110,12 +124,12 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime if (firstStartedAt) { setStartedAt(timestampToDate(firstStartedAt)); } - }, [originalNodes, nodeExecutions]); + }, [originalNodes, nodeExecutionsById]); const { items: barItemsData, totalDurationSec } = getChartDurationData(showNodes, startedAt); const styles = useStyles({ chartWidth: chartWidth, itemsShown: showNodes.length }); - React.useEffect(() => { + useEffect(() => { // Sync width of elements and intervals of ChartHeader (time labels) and TimelineChart const calcWidth = Math.ceil(totalDurationSec / chartTimeInterval) * INTERVAL_LENGTH; if (durationsRef.current && calcWidth < durationsRef.current.clientWidth) { diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx index e0349ddab..e0180b7a1 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx @@ -1,12 +1,8 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core'; -import { NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; -import { WaitForQuery } from 'components/common/WaitForQuery'; -import { NodeExecutionsRequestConfigContext } from 'components/Executions/contexts'; -import { useAllTreeNodeExecutionGroupsQuery } from 'components/Executions/nodeExecutionQueries'; -import { DataError } from 'components/Errors/DataError'; +import { NodeExecutionIdentifier } from 'models/Execution/types'; import { DetailsPanel } from 'components/common/DetailsPanel'; -import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; +import { useMemo, useState } from 'react'; import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent'; import { NodeExecutionsTimelineContext } from './context'; import { ExecutionTimelineFooter } from './ExecutionTimelineFooter'; @@ -25,58 +21,28 @@ const useStyles = makeStyles(() => ({ flex: '1 1 0', overflowY: 'auto', }, - loading: { - margin: 'auto', - }, })); -interface TimelineProps { - nodeExecutions: NodeExecution[]; -} - -export const ExecutionNodesTimeline = (props: TimelineProps) => { +export const ExecutionNodesTimeline = () => { const styles = useStyles(); - const [selectedExecution, setSelectedExecution] = React.useState( - null, - ); - const [chartTimezone, setChartTimezone] = React.useState(TimeZone.Local); + const [selectedExecution, setSelectedExecution] = useState(null); + const [chartTimezone, setChartTimezone] = useState(TimeZone.Local); const onCloseDetailsPanel = () => setSelectedExecution(null); const handleTimezoneChange = (tz) => setChartTimezone(tz); - const requestConfig = React.useContext(NodeExecutionsRequestConfigContext); - const childGroupsQuery = useAllTreeNodeExecutionGroupsQuery(props.nodeExecutions, requestConfig); - - const timelineContext = React.useMemo( + const timelineContext = useMemo( () => ({ selectedExecution, setSelectedExecution }), [selectedExecution, setSelectedExecution], ); - const renderExecutionsTimeline = (nodeExecutions: NodeExecution[]) => { - return ; - }; - - const TimelineLoading = () => { - return ( -
- -
- ); - }; - return (
- - {renderExecutionsTimeline} - + ;
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx index d1e68dba2..f99d01f8d 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx @@ -1,5 +1,6 @@ import ThemeProvider from '@material-ui/styles/ThemeProvider'; import { render, waitFor } from '@testing-library/react'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { muiTheme } from 'components/Theme/muiTheme'; import { oneFailedTaskWorkflow } from 'mocks/data/fixtures/oneFailedTaskWorkflow'; import { insertFixture } from 'mocks/data/insertFixture'; @@ -16,6 +17,10 @@ jest.mock('../ExecutionWorkflowGraph.tsx', () => ({ ExecutionWorkflowGraph: () => null, })); +jest.mock('../Timeline/ExecutionTimeline.tsx', () => ({ + ExecutionTimeline: () => null, +})); + jest.mock('chart.js', () => ({ Chart: { register: () => null }, Tooltip: { positioners: { cursor: () => null } }, @@ -59,7 +64,9 @@ describe('ExecutionDetails > Timeline', () => { render( - + + + , ); diff --git a/packages/zapp/console/src/components/Executions/contexts.ts b/packages/zapp/console/src/components/Executions/contexts.ts index 691d464ee..faeb47b6d 100644 --- a/packages/zapp/console/src/components/Executions/contexts.ts +++ b/packages/zapp/console/src/components/Executions/contexts.ts @@ -9,6 +9,7 @@ export interface ExecutionContextData { export const ExecutionContext = React.createContext( {} as ExecutionContextData, ); -export const NodeExecutionsContext = React.createContext>({}); + +export const NodeExecutionsByIdContext = React.createContext>({}); export const NodeExecutionsRequestConfigContext = React.createContext({}); diff --git a/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts b/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts index f31766060..556832414 100644 --- a/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts @@ -280,86 +280,6 @@ function fetchChildNodeExecutionGroups( return fetchGroupsForTaskExecutionNode(queryClient, nodeExecution, config); } -/** - * Query returns all children for a list of `nodeExecutions` - * Will recursively gather all children for anyone that isParent() - */ -async function fetchAllChildNodeExecutions( - queryClient: QueryClient, - nodeExecutions: NodeExecution[], - config: RequestConfig, -): Promise> { - const executionGroups: Array = await Promise.all( - nodeExecutions.map((exe) => fetchChildNodeExecutionGroups(queryClient, exe, config)), - ); - - /** Recursive check for nested/dynamic nodes */ - const childrenFromChildrenNodes: NodeExecution[] = []; - executionGroups.map((group) => - group.map((attempt) => { - attempt.nodeExecutions.map((execution) => { - if (isParentNode(execution)) { - childrenFromChildrenNodes.push(execution); - } - }); - }), - ); - - /** Request and concact data from children */ - if (childrenFromChildrenNodes.length > 0) { - const childGroups = await fetchAllChildNodeExecutions( - queryClient, - childrenFromChildrenNodes, - config, - ); - for (const group in childGroups) { - executionGroups.push(childGroups[group]); - } - } - return executionGroups; -} - -/** - * - * @param nodeExecutions list of parent node executionId's - * @param config - * @returns - */ -export function useAllChildNodeExecutionGroupsQuery( - nodeExecutions: NodeExecution[], - config: RequestConfig, -): QueryObserverResult, Error> { - const queryClient = useQueryClient(); - const shouldEnableFn = (groups) => { - if (groups.length > 0) { - return groups.some((group) => { - // non-empty groups are wrapped in array - const unwrappedGroup = Array.isArray(group) ? group[0] : group; - if (unwrappedGroup?.nodeExecutions?.length > 0) { - /* Return true is any executions are not yet terminal (ie, they can change) */ - return unwrappedGroup.nodeExecutions.some((ne) => { - return !nodeExecutionIsTerminal(ne); - }); - } else { - return false; - } - }); - } else { - return false; - } - }; - - const key = `${nodeExecutions?.[0]?.scopedId}-${nodeExecutions?.[0]?.closure?.phase}`; - - return useConditionalQuery>( - { - queryKey: [QueryType.NodeExecutionChildList, key, config], - queryFn: () => fetchAllChildNodeExecutions(queryClient, nodeExecutions, config), - }, - shouldEnableFn, - ); -} - /** Fetches and groups `NodeExecution`s which are direct children of the given * `NodeExecution`. */ @@ -438,15 +358,29 @@ export function useAllTreeNodeExecutionGroupsQuery( ): QueryObserverResult { const queryClient = useQueryClient(); const shouldEnableFn = (groups) => { - if (nodeExecutions.some((ne) => !nodeExecutionIsTerminal(ne))) { - return true; + if (groups.length > 0) { + return groups.some((group) => { + // non-empty groups are wrapped in array + const unwrappedGroup = Array.isArray(group) ? group[0] : group; + if (unwrappedGroup?.nodeExecutions?.length > 0) { + /* Return true is any executions are not yet terminal (ie, they can change) */ + return unwrappedGroup.nodeExecutions.some((ne) => { + return !nodeExecutionIsTerminal(ne); + }); + } else { + return false; + } + }); + } else { + return false; } - return groups.some((group) => !nodeExecutionIsTerminal(group)); }; + const key = `${nodeExecutions?.[0]?.scopedId}-${nodeExecutions?.[0]?.closure?.phase}`; + return useConditionalQuery( { - queryKey: [QueryType.NodeExecutionTreeList, nodeExecutions[0]?.id, config], + queryKey: [QueryType.NodeExecutionTreeList, key, config], queryFn: () => fetchAllTreeNodeExecutions(queryClient, nodeExecutions, config), refetchInterval: executionRefreshIntervalMs, }, diff --git a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx index ff0035bb4..fad6e0902 100644 --- a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -11,6 +11,9 @@ import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workf import { createDebugLogger } from 'common/log'; import { CompiledNode } from 'models/Node/types'; import { TaskExecutionPhase } from 'models/Execution/enums'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { useContext } from 'react'; +import { checkForDynamicExecutions } from 'components/common/utils'; import { transformerWorkflowToDag } from './transformerWorkflowToDag'; export interface WorkflowGraphProps { @@ -19,7 +22,6 @@ export interface WorkflowGraphProps { selectedPhase?: TaskExecutionPhase; isDetailsTabClosed: boolean; workflow: Workflow; - nodeExecutionsById?: any; } interface PrepareDAGResult { @@ -63,42 +65,12 @@ export const WorkflowGraph: React.FC = (props) => { onPhaseSelectionChanged, selectedPhase, isDetailsTabClosed, - nodeExecutionsById, workflow, } = props; + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); const { dag, staticExecutionIdsMap, error } = workflowToDag(workflow); - /** - * Note: - * Dynamic nodes are deteremined at runtime and thus do not come - * down as part of the workflow closure. We can detect and place - * dynamic nodes by finding orphan execution id's and then mapping - * those executions into the dag by using the executions 'uniqueParentId' - * to render that node as a subworkflow - */ - const checkForDynamicExeuctions = (allExecutions, staticExecutions) => { - const parentsToFetch = {}; - for (const executionId in allExecutions) { - if (!staticExecutions[executionId]) { - const dynamicExecution = allExecutions[executionId]; - const dynamicExecutionId = dynamicExecution.metadata.specNodeId || dynamicExecution.id; - const uniqueParentId = dynamicExecution.fromUniqueParentId; - if (uniqueParentId) { - if (parentsToFetch[uniqueParentId]) { - parentsToFetch[uniqueParentId].push(dynamicExecutionId); - } else { - parentsToFetch[uniqueParentId] = [dynamicExecutionId]; - } - } - } - } - const result = {}; - for (const parentId in parentsToFetch) { - result[parentId] = allExecutions[parentId]; - } - return result; - }; - const dynamicParents = checkForDynamicExeuctions(nodeExecutionsById, staticExecutionIdsMap); + const dynamicParents = checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap); const dynamicWorkflowQuery = useQuery(makeNodeExecutionDynamicWorkflowQuery(dynamicParents)); const renderReactFlowGraph = (dynamicWorkflows) => { debug('DynamicWorkflows:', dynamicWorkflows); @@ -118,7 +90,6 @@ export const WorkflowGraph: React.FC = (props) => { return ( ({ ReactFlowWrapper: jest.fn(({ children }) => ( @@ -27,7 +26,6 @@ describe('WorkflowGraph', () => { onNodeSelectionChanged={jest.fn} onPhaseSelectionChanged={jest.fn} workflow={workflow} - nodeExecutionsById={nodeExecutionsById} isDetailsTabClosed={true} /> , diff --git a/packages/zapp/console/src/components/common/utils.ts b/packages/zapp/console/src/components/common/utils.ts index c6c521874..4612bddbe 100644 --- a/packages/zapp/console/src/components/common/utils.ts +++ b/packages/zapp/console/src/components/common/utils.ts @@ -16,3 +16,34 @@ export function measureText(fontDefinition: string, text: string) { context.font = fontDefinition; return context.measureText(text); } + +/** + * Note: + * Dynamic nodes are deteremined at runtime and thus do not come + * down as part of the workflow closure. We can detect and place + * dynamic nodes by finding orphan execution id's and then mapping + * those executions into the dag by using the executions 'uniqueParentId' + * to render that node as a subworkflow + */ +export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { + const parentsToFetch = {}; + for (const executionId in allExecutions) { + if (!staticExecutions[executionId]) { + const dynamicExecution = allExecutions[executionId]; + const dynamicExecutionId = dynamicExecution.metadata.specNodeId || dynamicExecution.id; + const uniqueParentId = dynamicExecution.fromUniqueParentId; + if (uniqueParentId) { + if (parentsToFetch[uniqueParentId]) { + parentsToFetch[uniqueParentId].push(dynamicExecutionId); + } else { + parentsToFetch[uniqueParentId] = [dynamicExecutionId]; + } + } + } + } + const result = {}; + for (const parentId in parentsToFetch) { + result[parentId] = allExecutions[parentId]; + } + return result; +}; diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 2fb41b801..b84d2f8bc 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useContext } from 'react'; import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; @@ -50,9 +51,9 @@ const ReactFlowGraphComponent = (props) => { onPhaseSelectionChanged, selectedPhase, isDetailsTabClosed, - nodeExecutionsById, dynamicWorkflows, } = props; + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); const [state, setState] = useState({ data, dynamicWorkflows, diff --git a/packages/zapp/console/src/models/Execution/types.ts b/packages/zapp/console/src/models/Execution/types.ts index 0ac356e58..b2990fc75 100644 --- a/packages/zapp/console/src/models/Execution/types.ts +++ b/packages/zapp/console/src/models/Execution/types.ts @@ -92,6 +92,7 @@ export interface NodeExecution extends Admin.INodeExecution { closure: NodeExecutionClosure; metadata?: NodeExecutionMetadata; scopedId?: string; + fromUniqueParentId?: string; } export interface NodeExecutionsById {