From 9b10e5f58363e3ece29748677c65bfab7f7812ed Mon Sep 17 00:00:00 2001
From: olga-union <101579322+olga-union@users.noreply.github.com>
Date: Wed, 17 Aug 2022 14:41:30 -0500
Subject: [PATCH] fix: update timeline view to show dynamic wf internals on
 first render (#562)

* fix: update timeline view to show dynamic wf internals on first render

Signed-off-by: Olga Nad <olga@union.ai>

* fix: update tests and clean up code

Signed-off-by: Olga Nad <olga@union.ai>

* fix: test

Signed-off-by: Olga Nad <olga@union.ai>

Signed-off-by: Olga Nad <olga@union.ai>
---
 .../ExecutionChildrenLoader.tsx               |  33 ----
 .../ExecutionDetails/ExecutionNodeViews.tsx   | 163 ++++++++++++++----
 .../ExecutionWorkflowGraph.tsx                |  79 +--------
 .../TaskExecutionNode.tsx                     |   7 +-
 .../Timeline/ExecutionTimeline.tsx            |  52 ++++--
 .../ExecutionDetails/Timeline/index.tsx       |  48 +-----
 .../ExecutionDetails/test/Timeline.test.tsx   |   9 +-
 .../src/components/Executions/contexts.ts     |   3 +-
 .../Executions/nodeExecutionQueries.ts        | 102 ++---------
 .../WorkflowGraph/WorkflowGraph.tsx           |  39 +----
 .../WorkflowGraph/test/WorkflowGraph.test.tsx |   2 -
 .../console/src/components/common/utils.ts    |  31 ++++
 .../ReactFlow/ReactFlowGraphComponent.tsx     |   5 +-
 .../console/src/models/Execution/types.ts     |   1 +
 14 files changed, 250 insertions(+), 324 deletions(-)
 delete mode 100644 packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionChildrenLoader.tsx

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 ? (
-      <ExecutionWorkflowGraph nodeExecutions={executions} workflowId={workflowId} />
-    ) : null;
-  };
-
-  return (
-    <WaitForQuery errorComponent={DataError} query={childGroupsQuery}>
-      {renderGraphComponent}
-    </WaitForQuery>
-  );
-};
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<ExecutionNodeViewsProps> = ({ 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<NodeExecution[]>([]);
+  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<ExecutionNodeViewsProps> = ({ 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[]) => (
     <NodeExecutionsRequestConfigContext.Provider value={nodeExecutionsRequestConfig}>
       <NodeExecutionsTable
@@ -67,19 +148,35 @@ export const ExecutionNodeViews: React.FC<ExecutionNodeViewsProps> = ({ executio
     </NodeExecutionsRequestConfigContext.Provider>
   );
 
-  const renderExecutionLoader = (nodeExecutions: NodeExecution[]) => {
+  const renderExecutionChildrenLoader = () =>
+    nodeExecutions.length > 0 ? <ExecutionWorkflowGraph workflowId={workflowId} /> : null;
+
+  const renderExecutionLoader = () => {
     return (
-      <ExecutionChildrenLoader
-        nodeExecutions={nodeExecutions}
-        workflowId={execution.closure.workflowId}
-      />
+      <WaitForQuery errorComponent={DataError} query={childGroupsQuery}>
+        {renderExecutionChildrenLoader}
+      </WaitForQuery>
     );
   };
 
-  const renderExecutionsTimeline = (nodeExecutions: NodeExecution[]) => (
-    <ExecutionNodesTimeline nodeExecutions={nodeExecutions} />
+  const renderExecutionsTimeline = () => (
+    <WaitForQuery
+      errorComponent={DataError}
+      query={childGroupsQuery}
+      loadingComponent={TimelineLoading}
+    >
+      {() => <ExecutionNodesTimeline />}
+    </WaitForQuery>
   );
 
+  const TimelineLoading = () => {
+    return (
+      <div className={styles.loading}>
+        <LargeLoadingSpinner />
+      </div>
+    );
+  };
+
   return (
     <>
       <Tabs className={styles.tabs} {...tabState}>
@@ -87,29 +184,31 @@ export const ExecutionNodeViews: React.FC<ExecutionNodeViewsProps> = ({ executio
         <Tab value={tabs.graph.id} label={tabs.graph.label} />
         <Tab value={tabs.timeline.id} label={tabs.timeline.label} />
       </Tabs>
-      <NodeExecutionDetailsContextProvider workflowId={execution.closure.workflowId}>
-        <div className={styles.nodesContainer}>
-          {tabState.value === tabs.nodes.id && (
-            <>
-              <div className={styles.filters}>
-                <ExecutionFilters {...filterState} />
-              </div>
+      <NodeExecutionDetailsContextProvider workflowId={workflowId}>
+        <NodeExecutionsByIdContext.Provider value={nodeExecutionsById}>
+          <div className={styles.nodesContainer}>
+            {tabState.value === tabs.nodes.id && (
+              <>
+                <div className={styles.filters}>
+                  <ExecutionFilters {...filterState} />
+                </div>
+                <WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
+                  {renderNodeExecutionsTable}
+                </WaitForQuery>
+              </>
+            )}
+            {tabState.value === tabs.graph.id && (
+              <WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
+                {renderExecutionLoader}
+              </WaitForQuery>
+            )}
+            {tabState.value === tabs.timeline.id && (
               <WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
-                {renderNodeExecutionsTable}
+                {renderExecutionsTimeline}
               </WaitForQuery>
-            </>
-          )}
-          {tabState.value === tabs.graph.id && (
-            <WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
-              {renderExecutionLoader}
-            </WaitForQuery>
-          )}
-          {tabState.value === tabs.timeline.id && (
-            <WaitForQuery errorComponent={DataError} query={nodeExecutionsQuery}>
-              {renderExecutionsTimeline}
-            </WaitForQuery>
-          )}
-        </div>
+            )}
+          </div>
+        </NodeExecutionsByIdContext.Provider>
       </NodeExecutionDetailsContextProvider>
     </>
   );
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<ExecutionWorkflowGraphProps> = ({
-  nodeExecutions,
-  workflowId,
-}) => {
+export const ExecutionWorkflowGraph: React.FC<ExecutionWorkflowGraphProps> = ({ workflowId }) => {
   const queryClient = useQueryClient();
   const workflowQuery = useQuery<Workflow, Error>(makeWorkflowQuery(queryClient, workflowId));
 
-  const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState<
-    WorkflowNodeExecution[]
-  >([]);
   const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
+  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<ExecutionWorkflowGraphProps> = ({
   const [selectedPhase, setSelectedPhase] = useState<TaskExecutionPhase | undefined>(undefined);
   const [isDetailsTabClosed, setIsDetailsTabClosed] = useState<boolean>(!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<ExecutionWorkflowGraphProps> = ({
       selectedPhase={selectedPhase}
       onPhaseSelectionChanged={setSelectedPhase}
       isDetailsTabClosed={isDetailsTabClosed}
-      nodeExecutionsById={nodeExecutionsById}
       workflow={workflow}
     />
   );
 
   return (
     <>
-      <NodeExecutionsContext.Provider value={nodeExecutionsById}>
-        <WaitForQuery errorComponent={DataError} query={workflowQuery}>
-          {renderGraph}
-        </WaitForQuery>
-      </NodeExecutionsContext.Provider>
+      <WaitForQuery errorComponent={DataError} query={workflowQuery}>
+        {renderGraph}
+      </WaitForQuery>
       <DetailsPanel open={!!selectedExecution} onClose={onCloseDetailsPanel}>
         {selectedExecution && (
           <NodeExecutionDetailsPanelContent
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx
index d9268edff..afbfcf39d 100644
--- a/packages/zapp/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx
+++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx
@@ -4,7 +4,8 @@ import { TaskNodeRenderer } from 'components/WorkflowGraph/TaskNodeRenderer';
 import { NodeExecutionPhase } from 'models/Execution/enums';
 import { DAGNode } from 'models/Graph/types';
 import * as React from 'react';
-import { NodeExecutionsContext } from '../../contexts';
+import { useContext } from 'react';
+import { NodeExecutionsByIdContext } from '../../contexts';
 import { StatusIndicator } from './StatusIndicator';
 
 /** Renders DAGNodes with colors based on their node type, as well as dots to
@@ -12,8 +13,8 @@ import { StatusIndicator } from './StatusIndicator';
  */
 export const TaskExecutionNode: React.FC<NodeRendererProps<DAGNode>> = (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<ExProps> = ({ nodeExecutions, chartTimezone }) => {
-  const [chartWidth, setChartWidth] = React.useState(0);
-  const [labelInterval, setLabelInterval] = React.useState(INTERVAL_LENGTH);
-  const durationsRef = React.useRef<HTMLDivElement>(null);
-  const durationsLabelsRef = React.useRef<HTMLDivElement>(null);
-  const taskNamesRef = React.createRef<HTMLDivElement>();
+export const ExecutionTimeline: React.FC<ExProps> = ({ chartTimezone }) => {
+  const [chartWidth, setChartWidth] = useState(0);
+  const [labelInterval, setLabelInterval] = useState(INTERVAL_LENGTH);
+  const durationsRef = useRef<HTMLDivElement>(null);
+  const durationsLabelsRef = useRef<HTMLDivElement>(null);
+  const taskNamesRef = createRef<HTMLDivElement>();
 
-  const [originalNodes, setOriginalNodes] = React.useState<dNode[]>([]);
-  const [showNodes, setShowNodes] = React.useState<dNode[]>([]);
-  const [startedAt, setStartedAt] = React.useState<Date>(new Date());
+  const [originalNodes, setOriginalNodes] = useState<dNode[]>([]);
+  const [showNodes, setShowNodes] = useState<dNode[]>([]);
+  const [startedAt, setStartedAt] = useState<Date>(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<ExProps> = ({ 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<NodeExecutionIdentifier | null>(
-    null,
-  );
-  const [chartTimezone, setChartTimezone] = React.useState(TimeZone.Local);
+  const [selectedExecution, setSelectedExecution] = useState<NodeExecutionIdentifier | null>(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 <ExecutionTimeline nodeExecutions={nodeExecutions} chartTimezone={chartTimezone} />;
-  };
-
-  const TimelineLoading = () => {
-    return (
-      <div className={styles.loading}>
-        <LargeLoadingSpinner />
-      </div>
-    );
-  };
-
   return (
     <ScaleProvider>
       <div className={styles.wrapper}>
         <div className={styles.container}>
           <NodeExecutionsTimelineContext.Provider value={timelineContext}>
-            <WaitForQuery
-              errorComponent={DataError}
-              query={childGroupsQuery}
-              loadingComponent={TimelineLoading}
-            >
-              {renderExecutionsTimeline}
-            </WaitForQuery>
+            <ExecutionTimeline chartTimezone={chartTimezone} />;
           </NodeExecutionsTimelineContext.Provider>
         </div>
         <ExecutionTimelineFooter onTimezoneChange={handleTimezoneChange} />
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(
       <ThemeProvider theme={muiTheme}>
         <QueryClientProvider client={queryClient}>
-          <ExecutionNodesTimeline nodeExecutions={[]} />
+          <NodeExecutionsByIdContext.Provider value={{}}>
+            <ExecutionNodesTimeline />
+          </NodeExecutionsByIdContext.Provider>
         </QueryClientProvider>
       </ThemeProvider>,
     );
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<ExecutionContextData>(
   {} as ExecutionContextData,
 );
-export const NodeExecutionsContext = React.createContext<Dictionary<NodeExecution>>({});
+
+export const NodeExecutionsByIdContext = React.createContext<Dictionary<NodeExecution>>({});
 
 export const NodeExecutionsRequestConfigContext = React.createContext<RequestConfig>({});
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<Array<NodeExecutionGroup[]>> {
-  const executionGroups: Array<NodeExecutionGroup[]> = 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<Array<NodeExecutionGroup[]>, 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<Array<NodeExecutionGroup[]>>(
-    {
-      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<NodeExecution[], Error> {
   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<NodeExecution[]>(
     {
-      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<WorkflowGraphProps> = (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<WorkflowGraphProps> = (props) => {
     return (
       <ReactFlowGraphComponent
         dynamicWorkflows={dynamicWorkflows}
-        nodeExecutionsById={nodeExecutionsById}
         data={merged}
         onNodeSelectionChanged={onNodeSelectionChanged}
         onPhaseSelectionChanged={onPhaseSelectionChanged}
diff --git a/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx b/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx
index bcbb7fb5e..93817bb9d 100644
--- a/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx
+++ b/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx
@@ -4,7 +4,6 @@ import { createTestQueryClient } from 'test/utils';
 import { QueryClient, QueryClientProvider } from 'react-query';
 import { WorkflowGraph } from '../WorkflowGraph';
 import { workflow } from './workflow.mock';
-import { nodeExecutionsById } from './nodeExecutionsById.mock';
 
 jest.mock('../../flytegraph/ReactFlow/ReactFlowWrapper.tsx', () => ({
   ReactFlowWrapper: jest.fn(({ children }) => (
@@ -27,7 +26,6 @@ describe('WorkflowGraph', () => {
             onNodeSelectionChanged={jest.fn}
             onPhaseSelectionChanged={jest.fn}
             workflow={workflow}
-            nodeExecutionsById={nodeExecutionsById}
             isDetailsTabClosed={true}
           />
         </QueryClientProvider>,
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 {