From eff11c51d1588636fe3a94daa111e4e63b3c5a80 Mon Sep 17 00:00:00 2001 From: Olga Nad Date: Mon, 30 Jan 2023 14:24:31 +0100 Subject: [PATCH 01/14] fix: execution view children fetch on demand refactor Signed-off-by: Olga Nad Signed-off-by: Jason Porter --- .../ExecutionDetails/ExecutionNodeViews.tsx | 137 +++-------------- .../ExecutionDetails/ExecutionTabContent.tsx | 71 ++++++--- .../NodeExecutionDetailsPanelContent.tsx | 80 +++++++++- .../TaskExecutionNode.tsx | 5 +- .../Timeline/ExecutionTimeline.tsx | 30 +++- .../Timeline/NodeExecutionName.tsx | 5 +- .../ExecutionDetails/Timeline/TaskNames.tsx | 14 +- .../Executions/ExecutionDetails/utils.ts | 17 ++- .../Executions/Tables/NodeExecutionRow.tsx | 6 +- .../Executions/Tables/NodeExecutionsTable.tsx | 24 ++- .../Tables/nodeExecutionColumns.tsx | 4 +- .../NodeExecutionDetails/index.tsx | 19 ++- .../src/components/Executions/contexts.ts | 22 ++- .../Executions/nodeExecutionQueries.ts | 103 +------------ .../Executions/useNodeExecutionsById.ts | 23 +++ .../src/components/Executions/utils.ts | 46 ++++++ .../Launch/LaunchForm/ResumeSignalForm.tsx | 5 +- .../WorkflowGraph/WorkflowGraph.tsx | 10 +- .../transformerWorkflowToDag.tsx | 14 +- .../console/src/components/common/utils.ts | 18 ++- .../ReactFlow/ReactFlowGraphComponent.tsx | 143 ++++++++++++++---- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 27 +++- .../ReactFlow/customNodeComponents.tsx | 9 +- .../ReactFlow/transformDAGToReactFlowV2.tsx | 26 ++-- .../components/flytegraph/ReactFlow/types.ts | 3 +- packages/console/src/models/Graph/types.ts | 1 + 26 files changed, 520 insertions(+), 342 deletions(-) create mode 100644 packages/console/src/components/Executions/useNodeExecutionsById.ts diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 4cca481af..e88e90e2b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -1,20 +1,12 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; import { Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; 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, - ExternalResource, - LogsByPhase, - NodeExecution, -} from 'models/Execution/types'; -import { useEffect, useMemo, useState } from 'react'; +import { Execution } from 'models/Execution/types'; import { keyBy } from 'lodash'; -import { isMapTaskV1 } from 'models/Task/utils'; -import { useQueryClient } from 'react-query'; import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { FilterOperation } from 'models/AdminEntity/types'; import { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails'; @@ -23,10 +15,8 @@ import { ExecutionFilters } from '../ExecutionFilters'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { tabs } from './constants'; import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; -import { fetchTaskExecutionList } from '../taskExecutionQueries'; -import { getGroupedLogs } from '../TaskExecutionsList/utils'; -import { useAllTreeNodeExecutionGroupsQuery } from '../nodeExecutionQueries'; import { ExecutionTab } from './ExecutionTab'; +import { useNodeExecutionsById } from '../useNodeExecutionsById'; const useStyles = makeStyles((theme: Theme) => ({ filters: { @@ -55,10 +45,6 @@ const isPhaseFilter = (appliedFilters: FilterOperation[]) => { return false; }; -interface WorkflowNodeExecution extends NodeExecution { - logsByPhase?: LogsByPhase; -} - interface ExecutionNodeViewsProps { execution: Execution; } @@ -71,22 +57,11 @@ export const ExecutionNodeViews: React.FC = ({ const styles = useStyles(); const filterState = useNodeExecutionFiltersState(); const tabState = useTabState(tabs, defaultTab); - const queryClient = useQueryClient(); - const [nodeExecutionsLoading, setNodeExecutionsLoading] = - useState(true); const { closure: { workflowId }, } = execution; - const [nodeExecutions, setNodeExecutions] = useState([]); - const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = - useState([]); - - const nodeExecutionsById = useMemo(() => { - return keyBy(nodeExecutionsWithResources, 'scopedId'); - }, [nodeExecutionsWithResources]); - // query to get all data to build Graph and Timeline const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); // query to get filtered data to narrow down Table outputs @@ -94,73 +69,16 @@ export const ExecutionNodeViews: React.FC = ({ nodeExecutionsQuery: { data: filteredNodeExecutions }, } = useExecutionNodeViewsState(execution, filterState.appliedFilters); - useEffect(() => { - let isCurrent = true; - - async function fetchData(baseNodeExecutions, queryClient) { - setNodeExecutionsLoading(true); - 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); - setNodeExecutionsLoading(false); - } - } - - if (nodeExecutions.length > 0) { - fetchData(nodeExecutions, queryClient); - } else { - if (isCurrent) { - setNodeExecutionsLoading(false); - } - } - return () => { - isCurrent = false; - }; - }, [nodeExecutions]); - - const childGroupsQuery = useAllTreeNodeExecutionGroupsQuery( - nodeExecutionsQuery.data ?? [], - {}, - ); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = + useNodeExecutionsById(); useEffect(() => { - if (!childGroupsQuery.isLoading && childGroupsQuery.data) { - setNodeExecutions(childGroupsQuery.data); - } - }, [childGroupsQuery.data]); + const currentNodeExecutionsById = keyBy( + nodeExecutionsQuery.data, + 'scopedId', + ); + setCurrentNodeExecutionsById(currentNodeExecutionsById); + }, [nodeExecutionsQuery.data]); const LoadingComponent = () => { return ( @@ -171,27 +89,16 @@ export const ExecutionNodeViews: React.FC = ({ }; const renderTab = tabType => { - if (nodeExecutionsLoading) { - return ; - } return ( - - {() => ( - - )} - + ); }; @@ -203,7 +110,9 @@ export const ExecutionNodeViews: React.FC = ({ - +
{tabState.value === tabs.nodes.id && (
diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx index de52522e8..5bd36ec01 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx @@ -5,11 +5,10 @@ import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; import { TaskExecutionPhase } from 'models/Execution/enums'; import { NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; import { startNodeId, endNodeId } from 'models/Node/constants'; -import * as React from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { checkForDynamicExecutions } from 'components/common/utils'; import { dNode } from 'models/Graph/types'; -import { useContext, useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { FilterOperation, @@ -54,8 +53,8 @@ const executionMatchesPhaseFilter = ( if (key === 'phase' && operation === FilterOperationName.VALUE_IN) { // default to UNKNOWN phase if the field does not exist on a closure const itemValue = - nodeExecutionPhaseConstants()[nodeExecution?.closure[key]]?.value ?? - nodeExecutionPhaseConstants()[0].value; + nodeExecutionPhaseConstants[nodeExecution?.closure[key]]?.value ?? + nodeExecutionPhaseConstants[0].value; // phase check filters always return values in an array const valuesArray = value as FilterOperationValueList; return valuesArray.includes(itemValue); @@ -70,31 +69,50 @@ export const ExecutionTabContent: React.FC = ({ const styles = useStyles(); const { compiledWorkflowClosure } = useNodeExecutionContext(); const { appliedFilters } = useNodeExecutionFiltersState(); - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); - - const { dag, staticExecutionIdsMap, error } = compiledWorkflowClosure + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { staticExecutionIdsMap } = compiledWorkflowClosure ? transformerWorkflowToDag(compiledWorkflowClosure) - : { dag: {}, staticExecutionIdsMap: {}, error: null }; - const dynamicParents = checkForDynamicExecutions( - nodeExecutionsById, - staticExecutionIdsMap, + : { staticExecutionIdsMap: {} }; + const [dynamicParents, setDynamicParents] = useState( + checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap), ); - const { data: dynamicWorkflows } = useQuery( + const { data: dynamicWorkflows, refetch } = useQuery( makeNodeExecutionDynamicWorkflowQuery(dynamicParents), ); + const [initialNodes, setInitialNodes] = useState([]); const [initialFilteredNodes, setInitialFilteredNodes] = useState< dNode[] | undefined >(undefined); + const [dagError, setDagError] = useState(null); const [mergedDag, setMergedDag] = useState(null); const [filters, setFilters] = useState(appliedFilters); const [isFiltersChanged, setIsFiltersChanged] = useState(false); + const [shouldUpdate, setShouldUpdate] = useState(false); + + useEffect(() => { + if (shouldUpdate) { + const newDynamicParents = checkForDynamicExecutions( + nodeExecutionsById, + staticExecutionIdsMap, + ); + setDynamicParents(newDynamicParents); + refetch(); + setShouldUpdate(false); + } + }, [shouldUpdate]); useEffect(() => { - const nodes: dNode[] = compiledWorkflowClosure - ? transformerWorkflowToDag(compiledWorkflowClosure, dynamicWorkflows).dag - .nodes - : []; + const { dag, staticExecutionIdsMap, error } = compiledWorkflowClosure + ? transformerWorkflowToDag( + compiledWorkflowClosure, + dynamicWorkflows, + nodeExecutionsById, + ) + : { dag: {}, staticExecutionIdsMap: {}, error: null }; + + const nodes = dag.nodes ?? []; + // we remove start/end node info in the root dNode list during first assignment const plainNodes = convertToPlainNodes(nodes); @@ -106,14 +124,27 @@ export const ExecutionTabContent: React.FC = ({ const dynamicWorkflow = transformerWorkflowToDag( compiledWorkflowClosure, dynamicWorkflows, + nodeExecutionsById, ); newMergedDag = dynamicWorkflow.dag; } } } + setDagError(error); setMergedDag(newMergedDag); + plainNodes.map(node => { + const initialNode = initialNodes.find(n => n.scopedId === node.scopedId); + if (initialNode) { + node.expanded = initialNode.expanded; + } + }); setInitialNodes(plainNodes); - }, [compiledWorkflowClosure, dynamicWorkflows]); + }, [ + compiledWorkflowClosure, + dynamicWorkflows, + dynamicParents, + nodeExecutionsById, + ]); useEffect(() => { if (!isEqual(filters, appliedFilters)) { @@ -219,19 +250,22 @@ export const ExecutionTabContent: React.FC = ({ ); case tabs.graph.id: return ( ); case tabs.timeline.id: @@ -241,6 +275,7 @@ export const ExecutionTabContent: React.FC = ({
diff --git a/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index 9038587a7..40cb0a87b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -1,9 +1,7 @@ -import * as React from 'react'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useContext, 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 { ArrowBackIos, Close } from '@material-ui/icons'; import classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; import { InfoIcon } from 'components/common/Icons/InfoIcon'; @@ -15,6 +13,8 @@ import { LocationDescriptor } from 'history'; import { PaginatedEntityResponse } from 'models/AdminEntity/types'; import { Workflow } from 'models/Workflow/types'; import { + ExternalResource, + LogsByPhase, MapTaskExecution, NodeExecution, NodeExecutionIdentifier, @@ -36,6 +36,8 @@ import { } from 'components/WorkflowGraph/utils'; import { TaskVersionDetailsLink } from 'components/Entities/VersionDetails/VersionDetailsLink'; import { Identifier } from 'models/Common/types'; +import { isMapTaskV1 } from 'models/Task/utils'; +import { merge } from 'lodash'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; import { makeListTaskExecutionsQuery, @@ -49,6 +51,9 @@ import { fetchWorkflowExecution } from '../useWorkflowExecution'; import { NodeExecutionTabs } from './NodeExecutionTabs'; import { ExecutionDetailsActions } from './ExecutionDetailsActions'; import { getNodeFrontendPhase, isNodeGateNode } from '../utils'; +import { fetchTaskExecutionList } from '../taskExecutionQueries'; +import { getGroupedLogs } from '../TaskExecutionsList/utils'; +import { NodeExecutionsByIdContext } from '../contexts'; const useStyles = makeStyles((theme: Theme) => { const paddingVertical = `${theme.spacing(2)}px`; @@ -255,19 +260,80 @@ export const NodeExecutionDetailsPanelContent: React.FC< const queryClient = useQueryClient(); const { getNodeExecutionDetails, compiledWorkflowClosure } = useNodeExecutionContext(); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( + NodeExecutionsByIdContext, + ); const isGateNode = isNodeGateNode( compiledWorkflowClosure?.primary.template.nodes ?? [], nodeExecutionId, ); + const [nodeExecutionLoading, setNodeExecutionLoading] = + useState(false); - const nodeExecutionQuery = useQuery({ + const { data: nodeExecution } = useQuery({ ...makeNodeExecutionQuery(nodeExecutionId), // The selected NodeExecution has been fetched at this point, we don't want to // issue an additional fetch. staleTime: Infinity, }); - const nodeExecution = nodeExecutionQuery.data; + useEffect(() => { + let isCurrent = true; + + async function fetchTasksData(exe, queryClient) { + setNodeExecutionLoading(true); + const taskExecutions = await fetchTaskExecutionList(queryClient, exe.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); + + const exeWithResources = { + [exe.scopedId]: { + ...exe, + ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), + tasksFetched: true, + }, + }; + + if (isCurrent) { + const newNodeExecutionsById = merge( + nodeExecutionsById, + exeWithResources, + ); + setCurrentNodeExecutionsById(newNodeExecutionsById); + setNodeExecutionLoading(false); + } + } + + if (nodeExecution) { + if ( + nodeExecution.scopedId && + !nodeExecutionsById[nodeExecution.scopedId].tasksFetched + ) + fetchTasksData(nodeExecution, queryClient); + } else { + if (isCurrent) { + setNodeExecutionLoading(false); + } + } + return () => { + isCurrent = false; + }; + }, [nodeExecution]); const [isReasonsVisible, setReasonsVisible] = useState(false); const [dag, setDag] = useState(null); @@ -458,7 +524,7 @@ export const NodeExecutionDetailsPanelContent: React.FC< />
- {dag ? ( + {!nodeExecutionLoading && dag ? ( ) : ( tabsContent diff --git a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx index 630e42c3a..620d58a6e 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx @@ -3,8 +3,7 @@ import { NodeRendererProps, Point } from 'components/flytegraph/types'; import { TaskNodeRenderer } from 'components/WorkflowGraph/TaskNodeRenderer'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { DAGNode } from 'models/Graph/types'; -import * as React from 'react'; -import { useContext } from 'react'; +import React, { useContext } from 'react'; import { NodeExecutionsByIdContext } from '../../contexts'; import { StatusIndicator } from './StatusIndicator'; @@ -15,7 +14,7 @@ export const TaskExecutionNode: React.FC< NodeRendererProps > = props => { const { node, config, selected } = props; - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); const nodeExecution = nodeExecutionsById[node.id]; const phase = nodeExecution diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 0b2676968..8f634c0cf 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -1,4 +1,10 @@ -import * as React from 'react'; +import React, { + createRef, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { makeStyles, Typography } from '@material-ui/core'; import { isEndNode, @@ -8,8 +14,9 @@ import { import { tableHeaderColor } from 'components/Theme/constants'; import { timestampToDate } from 'common/utils'; import { dNode } from 'models/Graph/types'; -import { createRef, useContext, useEffect, useRef, useState } from 'react'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { fetchChildrenExecutions } from 'components/Executions/utils'; +import { useQueryClient } from 'react-query'; import { convertToPlainNodes } from './helpers'; import { ChartHeader } from './ChartHeader'; import { useScaleContext } from './scaleContext'; @@ -72,11 +79,13 @@ const INTERVAL_LENGTH = 110; interface ExProps { chartTimezone: string; initialNodes: dNode[]; + setShouldUpdate: (val: boolean) => void; } export const ExecutionTimeline: React.FC = ({ chartTimezone, initialNodes, + setShouldUpdate, }) => { const [chartWidth, setChartWidth] = useState(0); const [labelInterval, setLabelInterval] = useState(INTERVAL_LENGTH); @@ -87,7 +96,10 @@ export const ExecutionTimeline: React.FC = ({ const [originalNodes, setOriginalNodes] = useState(initialNodes); const [showNodes, setShowNodes] = useState([]); const [startedAt, setStartedAt] = useState(new Date()); - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const queryClient = useQueryClient(); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( + NodeExecutionsByIdContext, + ); const { chartInterval: chartTimeInterval } = useScaleContext(); useEffect(() => { @@ -159,7 +171,15 @@ export const ExecutionTimeline: React.FC = ({ } }; - const toggleNode = (id: string, scopeId: string, level: number) => { + const toggleNode = async (id: string, scopedId: string, level: number) => { + fetchChildrenExecutions( + queryClient, + scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + const searchNode = (nodes: dNode[], nodeLevel: number) => { if (!nodes || nodes.length === 0) { return; @@ -171,7 +191,7 @@ export const ExecutionTimeline: React.FC = ({ } if ( node.id === id && - node.scopedId === scopeId && + node.scopedId === scopedId && nodeLevel === level ) { nodes[i].expanded = !nodes[i].expanded; diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx index c533df046..4db08a68a 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx @@ -6,8 +6,7 @@ import { SelectNodeExecutionLink } from 'components/Executions/Tables/SelectNode import { isEqual } from 'lodash'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { NodeExecution } from 'models/Execution/types'; -import * as React from 'react'; -import { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { DetailsPanelContext } from '../DetailsPanelContext'; interface NodeExecutionTimelineNameData { @@ -45,7 +44,7 @@ export const NodeExecutionName: React.FC = ({ let isCurrent = true; getNodeExecutionDetails(execution).then(res => { if (isCurrent) { - setDisplayName(res.displayName); + setDisplayName(res?.displayName); } }); return () => { diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx index cf939e3d4..7a58f9fc5 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -1,10 +1,11 @@ -import * as React from 'react'; +import React, { useContext } from 'react'; import { IconButton, makeStyles, Theme, Tooltip } from '@material-ui/core'; - import { RowExpander } from 'components/Executions/Tables/RowExpander'; import { getNodeTemplateName } from 'components/WorkflowGraph/utils'; import { dNode } from 'models/Graph/types'; -import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; +import { PlayCircleOutline } from '@material-ui/icons'; +import { isParentNode } from 'components/Executions/utils'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { NodeExecutionName } from './NodeExecutionName'; import t from '../strings'; @@ -53,11 +54,14 @@ interface TaskNamesProps { export const TaskNames = React.forwardRef( ({ nodes, onScroll, onToggle, onAction }, ref) => { const styles = useStyles(); + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); return (
{nodes.map(node => { const nodeLevel = node?.level ?? 0; + const nodeExecution = nodeExecutionsById[node.scopedId]; + return (
( }} >
- {node.nodes?.length ? ( + {nodeExecution && isParentNode(nodeExecution) ? ( @@ -104,7 +108,7 @@ export const TaskNames = React.forwardRef( onClick={() => onAction(node.id)} data-testid={`resume-gate-node-${node.id}`} > - + )} diff --git a/packages/console/src/components/Executions/ExecutionDetails/utils.ts b/packages/console/src/components/Executions/ExecutionDetails/utils.ts index ca5adc21a..7d6c84c7a 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/utils.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/utils.ts @@ -1,5 +1,9 @@ import { Identifier, ResourceType } from 'models/Common/types'; -import { Execution, TaskExecution } from 'models/Execution/types'; +import { + Execution, + NodeExecution, + TaskExecution, +} from 'models/Execution/types'; import { Routes } from 'routes/routes'; import { PaginatedEntityResponse } from 'models/AdminEntity/types'; @@ -29,3 +33,14 @@ export function getTaskExecutionDetailReasons( ) || [] ); } + +export function isChildGroupsFetched( + scopedId: string, + nodeExecutionsById: Dictionary, +): boolean { + return Object.values(nodeExecutionsById).find( + exe => exe?.fromUniqueParentId === scopedId, + ) + ? true + : false; +} diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index ad8f3fc48..26107716f 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -2,8 +2,7 @@ import classnames from 'classnames'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; -import * as React from 'react'; -import { useContext } from 'react'; +import React, { useContext } from 'react'; import { isExpanded } from 'components/WorkflowGraph/utils'; import { isEqual } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; @@ -13,6 +12,7 @@ import { NodeExecutionColumnDefinition } from './types'; import { DetailsPanelContext } from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; +import { isParentNode } from '../utils'; const useStyles = makeStyles(() => ({ namesContainerExpander: { @@ -68,7 +68,7 @@ export const NodeExecutionRow: React.FC = ({ const expanderContent = (
- {node.nodes?.length ? ( + {isParentNode(nodeExecution) ? ( onToggle(node.id, node.scopedId, nodeLevel)} diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index ce51b7243..14bac78ed 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -6,13 +6,13 @@ import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { dateToTimestamp } from 'common/utils'; -import * as React from 'react'; -import { useMemo, useEffect, useState, useContext } from 'react'; +import React, { useMemo, useEffect, useState, useContext } from 'react'; import { isEndNode, isExpanded, isStartNode, } from 'components/WorkflowGraph/utils'; +import { useQueryClient } from 'react-query'; import { ExecutionsTableHeader } from './ExecutionsTableHeader'; import { generateColumns } from './nodeExecutionColumns'; import { NoExecutionsContent } from './NoExecutionsContent'; @@ -22,10 +22,12 @@ import { convertToPlainNodes } from '../ExecutionDetails/Timeline/helpers'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionRow } from './NodeExecutionRow'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; +import { fetchChildrenExecutions } from '../utils'; interface NodeExecutionsTableProps { initialNodes: dNode[]; filteredNodes?: dNode[]; + setShouldUpdate: (val: boolean) => void; } const scrollbarPadding = scrollbarSize(); @@ -43,10 +45,14 @@ const scrollbarPadding = scrollbarSize(); export const NodeExecutionsTable: React.FC = ({ initialNodes, filteredNodes, + setShouldUpdate, }) => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const queryClient = useQueryClient(); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( + NodeExecutionsByIdContext, + ); const { appliedFilters } = useNodeExecutionFiltersState(); const [originalNodes, setOriginalNodes] = useState( appliedFilters.length > 0 && filteredNodes ? filteredNodes : initialNodes, @@ -81,7 +87,15 @@ export const NodeExecutionsTable: React.FC = ({ setShowNodes(updatedShownNodesMap); }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); - const toggleNode = (id: string, scopeId: string, level: number) => { + const toggleNode = async (id: string, scopedId: string, level: number) => { + fetchChildrenExecutions( + queryClient, + scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + const searchNode = (nodes: dNode[], nodeLevel: number) => { if (!nodes || nodes.length === 0) { return; @@ -93,7 +107,7 @@ export const NodeExecutionsTable: React.FC = ({ } if ( node.id === id && - node.scopedId === scopeId && + node.scopedId === scopedId && nodeLevel === level ) { nodes[i].expanded = !nodes[i].expanded; diff --git a/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx b/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx index e6ebb9149..47419f4c2 100644 --- a/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -37,7 +37,7 @@ const DisplayId: React.FC = ({ execution }) => { let isCurrent = true; getNodeExecutionDetails(execution).then(res => { if (isCurrent) { - setDisplayId(res.displayId); + setDisplayId(res?.displayId); } }); return () => { @@ -63,7 +63,7 @@ const DisplayType: React.FC = ({ let isCurrent = true; getNodeExecutionDetails(execution).then(res => { if (isCurrent) { - setType(res.displayType); + setType(res?.displayType); } }); return () => { diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx index 58528219b..427ae987e 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx @@ -1,5 +1,10 @@ -import * as React from 'react'; -import { createContext, useContext, useEffect, useRef, useState } from 'react'; +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from 'react'; import { log } from 'common/log'; import { Identifier } from 'models/Common/types'; import { NodeExecution } from 'models/Execution/types'; @@ -17,7 +22,7 @@ import { getTaskThroughExecution } from './getTaskThroughExecution'; interface NodeExecutionDetailsState { getNodeExecutionDetails: ( nodeExecution?: NodeExecution, - ) => Promise; + ) => Promise; workflowId: Identifier; compiledWorkflowClosure: CompiledWorkflowClosure | null; } @@ -113,7 +118,7 @@ export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { }; }, [queryClient, resourceType, project, domain, name, version]); - const checkForDynamicTasks = async (nodeExecution: NodeExecution) => { + const getDynamicTasks = async (nodeExecution: NodeExecution) => { const taskDetails = await getTaskThroughExecution( queryClient, nodeExecution, @@ -130,7 +135,7 @@ export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { const getDetails = async ( nodeExecution?: NodeExecution, - ): Promise => { + ): Promise => { if (!executionTree || !nodeExecution) { return UNKNOWN_DETAILS; } @@ -148,7 +153,9 @@ export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { } // look for specific task by nodeId in current execution - details = await checkForDynamicTasks(nodeExecution); + if (nodeExecution.metadata?.isDynamic) { + details = await getDynamicTasks(nodeExecution); + } return details; } diff --git a/packages/console/src/components/Executions/contexts.ts b/packages/console/src/components/Executions/contexts.ts index 1b5a380a2..0f592e9cf 100644 --- a/packages/console/src/components/Executions/contexts.ts +++ b/packages/console/src/components/Executions/contexts.ts @@ -1,14 +1,28 @@ -import { Execution, NodeExecution } from 'models/Execution/types'; +import { Execution, LogsByPhase, NodeExecution } from 'models/Execution/types'; import { createContext } from 'react'; export interface ExecutionContextData { execution: Execution; } +export interface WorkflowNodeExecution extends NodeExecution { + tasksFetched?: boolean; + logsByPhase?: LogsByPhase; +} + export const ExecutionContext = createContext( {} as ExecutionContextData, ); -export const NodeExecutionsByIdContext = createContext< - Dictionary ->({}); +export interface INodeExecutionsByIdContext { + nodeExecutionsById: Dictionary; + setCurrentNodeExecutionsById: ( + currentNodeExecutionsById: Dictionary, + ) => void; +} + +export const NodeExecutionsByIdContext = + createContext({ + nodeExecutionsById: {}, + setCurrentNodeExecutionsById: () => {}, + }); diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 483ae802c..f7ef95932 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -1,7 +1,5 @@ -import { compareTimestampsAscending } from 'common/utils'; import { QueryInput, QueryType } from 'components/data/types'; import { retriesToZero } from 'components/flytegraph/ReactFlow/utils'; -import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; import { isEqual } from 'lodash'; import { PaginatedEntityResponse, @@ -22,12 +20,11 @@ import { WorkflowExecutionIdentifier, } from 'models/Execution/types'; import { endNodeId, startNodeId } from 'models/Node/constants'; -import { QueryClient, QueryObserverResult, useQueryClient } from 'react-query'; -import { executionRefreshIntervalMs } from './constants'; +import { QueryClient } from 'react-query'; import { fetchTaskExecutionList } from './taskExecutionQueries'; import { formatRetryAttempt } from './TaskExecutionsList/utils'; import { NodeExecutionGroup } from './types'; -import { isParentNode, nodeExecutionIsTerminal } from './utils'; +import { isParentNode } from './utils'; const ignoredNodeIds = [startNodeId, endNodeId]; function removeSystemNodes(nodeExecutions: NodeExecution[]): NodeExecution[] { @@ -296,7 +293,7 @@ async function fetchGroupsForParentNodeExecution( return Array.from(groupsByName.values()); } -function fetchChildNodeExecutionGroups( +export function fetchChildNodeExecutionGroups( queryClient: QueryClient, nodeExecution: NodeExecution, config: RequestConfig, @@ -327,97 +324,3 @@ function fetchChildNodeExecutionGroups( } return fetchGroupsForTaskExecutionNode(queryClient, nodeExecution, config); } - -/** - * Query returns all children (not only direct childs) for a list of `nodeExecutions` - */ -async function fetchAllTreeNodeExecutions( - queryClient: QueryClient, - nodeExecutions: NodeExecution[], - config: RequestConfig, -): Promise { - const queue: NodeExecution[] = [...nodeExecutions]; - let left = 0; - let right = queue.length; - - while (left < right) { - const top: NodeExecution = queue[left++]; - const executionGroups: NodeExecutionGroup[] = - await fetchChildNodeExecutionGroups(queryClient, top, config); - for (let i = 0; i < executionGroups.length; i++) { - for (let j = 0; j < executionGroups[i].nodeExecutions.length; j++) { - queue.push(executionGroups[i].nodeExecutions[j]); - right++; - } - } - } - - const sorted: NodeExecution[] = queue.sort( - (na: NodeExecution, nb: NodeExecution) => { - if (!na.closure.startedAt) { - return 1; - } - if (!nb.closure.startedAt) { - return -1; - } - return compareTimestampsAscending( - na.closure.startedAt, - nb.closure.startedAt, - ); - }, - ); - - return sorted; -} - -/** - * - * @param nodeExecutions list of parent node executionId's - * @param config - * @returns - */ -export function useAllTreeNodeExecutionGroupsQuery( - nodeExecutions: NodeExecution[], - config: RequestConfig, -): QueryObserverResult { - 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 n = nodeExecutions.length - 1; - let key = ''; - if (n >= 0) { - const keyP1 = `${nodeExecutions[0]?.scopedId}-${nodeExecutions[0].closure.phase}-${nodeExecutions[0].closure?.startedAt?.nanos}`; - key = keyP1; - if (n >= 1) { - const keyP2 = `${nodeExecutions[n]?.scopedId}-${nodeExecutions[n].closure.phase}-${nodeExecutions[n].closure?.startedAt?.nanos}`; - key = keyP1 + '-' + keyP2; - } - } - - return useConditionalQuery( - { - queryKey: [QueryType.NodeExecutionTreeList, key, config], - queryFn: () => - fetchAllTreeNodeExecutions(queryClient, nodeExecutions, config), - refetchInterval: executionRefreshIntervalMs, - }, - shouldEnableFn, - ); -} diff --git a/packages/console/src/components/Executions/useNodeExecutionsById.ts b/packages/console/src/components/Executions/useNodeExecutionsById.ts new file mode 100644 index 000000000..0ebec211a --- /dev/null +++ b/packages/console/src/components/Executions/useNodeExecutionsById.ts @@ -0,0 +1,23 @@ +import { NodeExecution } from 'models/Execution/types'; +import { useCallback, useState } from 'react'; +import { INodeExecutionsByIdContext } from './contexts'; + +export const useNodeExecutionsById = ( + initialNodeExecutionsById?: Dictionary, +): INodeExecutionsByIdContext => { + const [nodeExecutionsById, setNodeExecutionsById] = useState( + initialNodeExecutionsById ?? {}, + ); + + const setCurrentNodeExecutionsById = useCallback( + (currentNodeExecutionsById: Dictionary): void => { + setNodeExecutionsById(currentNodeExecutionsById); + }, + [], + ); + + return { + nodeExecutionsById, + setCurrentNodeExecutionsById, + }; +}; diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index 316222eb5..14583479e 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -1,4 +1,5 @@ import { durationToMilliseconds, timestampToDate } from 'common/utils'; +import { clone, isEqual, keyBy, merge } from 'lodash'; import { runningExecutionStates, terminalExecutionStates, @@ -19,12 +20,16 @@ import { TaskExecution, } from 'models/Execution/types'; import { CompiledNode } from 'models/Node/types'; +import { QueryClient } from 'react-query'; import { nodeExecutionPhaseConstants, taskExecutionPhaseConstants, taskTypeToNodeExecutionDisplayType, workflowExecutionPhaseConstants, } from './constants'; +import { WorkflowNodeExecution } from './contexts'; +import { isChildGroupsFetched } from './ExecutionDetails/utils'; +import { fetchChildNodeExecutionGroups } from './nodeExecutionQueries'; import { ExecutionPhaseConstants, NodeExecutionDisplayType, @@ -209,3 +214,44 @@ export function getNodeFrontendPhase( ? NodeExecutionPhase.PAUSED : phase; } + +export async function fetchChildrenExecutions( + queryClient: QueryClient, + scopedId: string, + nodeExecutionsById: Dictionary, + setCurrentNodeExecutionsById: ( + currentNodeExecutionsById: Dictionary, + ) => void, + setShouldUpdate?: (val: boolean) => void, +) { + if (!isChildGroupsFetched(scopedId, nodeExecutionsById)) { + const childGroups = await fetchChildNodeExecutionGroups( + queryClient, + nodeExecutionsById[scopedId], + {}, + ); + + let childGroupsExecutionsById; + childGroups.forEach(group => { + childGroupsExecutionsById = merge( + childGroupsExecutionsById, + keyBy(group.nodeExecutions, 'scopedId'), + ); + }); + if (childGroupsExecutionsById) { + const prevNodeExecutionsById = clone(nodeExecutionsById); + const currentNodeExecutionsById = merge( + nodeExecutionsById, + childGroupsExecutionsById, + ); + if ( + setShouldUpdate && + !isEqual(prevNodeExecutionsById, currentNodeExecutionsById) + ) { + setShouldUpdate(true); + } + + setCurrentNodeExecutionsById(currentNodeExecutionsById); + } + } +} diff --git a/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx b/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx index a9f6f37d9..9355fbdab 100644 --- a/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx +++ b/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx @@ -1,7 +1,6 @@ import { DialogContent, Typography } from '@material-ui/core'; import { getCacheKey } from 'components/Cache/utils'; -import * as React from 'react'; -import { useState, useContext, useEffect, useMemo } from 'react'; +import React, { useState, useContext, useEffect, useMemo } from 'react'; import { NodeExecution } from 'models/Execution/types'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; @@ -39,7 +38,7 @@ export const ResumeSignalForm: React.FC = ({ nodeId, onClose, }); - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); const [nodeExecution, setNodeExecution] = useState( nodeExecutionsById[nodeId], ); diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index a1362c879..a76ed09e0 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -1,5 +1,5 @@ -import * as React from 'react'; -import ReactFlowGraphComponent from 'components/flytegraph/ReactFlow/ReactFlowGraphComponent'; +import React from 'react'; +import { ReactFlowGraphComponent } from 'components/flytegraph/ReactFlow/ReactFlowGraphComponent'; import { Error } from 'models/Common/types'; import { NonIdealState } from 'components/common/NonIdealState'; import { CompiledNode } from 'models/Node/types'; @@ -16,6 +16,8 @@ export interface WorkflowGraphProps { error: Error | null; dynamicWorkflows: any; initialNodes: dNode[]; + shouldUpdate: boolean; + setShouldUpdate: (val: boolean) => void; } export interface DynamicWorkflowMapping { rootGraphNodeId: CompiledNode; @@ -31,6 +33,8 @@ export const WorkflowGraph: React.FC = ({ error, dynamicWorkflows, initialNodes, + shouldUpdate, + setShouldUpdate, }) => { if (error) { return ( @@ -57,6 +61,8 @@ export const WorkflowGraph: React.FC = ({ selectedPhase={selectedPhase} isDetailsTabClosed={isDetailsTabClosed} initialNodes={initialNodes} + shouldUpdate={shouldUpdate} + setShouldUpdate={setShouldUpdate} /> ); }; diff --git a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx index a75cc61dd..bd173aacc 100644 --- a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx +++ b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -11,6 +11,7 @@ import { CompiledWorkflow, CompiledWorkflowClosure, } from 'models/Workflow/types'; +import { isParentNode } from 'components/Executions/utils'; import { isStartOrEndNode, getDisplayName, @@ -33,6 +34,7 @@ const debug = createDebugLogger('@transformerWorkflowToDag'); export const transformerWorkflowToDag = ( workflow: CompiledWorkflowClosure, dynamicToMerge: any | null = null, + nodeExecutionsById = {}, ): any => { const { primary } = workflow; const staticExecutionIdsMap = {}; @@ -57,8 +59,12 @@ export const transformerWorkflowToDag = ( taskTemplate?: CompiledTask; typeOverride?: dTypes; } - const createDNode = (props: CreateDNodeProps): dNode => { - const { compiledNode, parentDNode, taskTemplate, typeOverride } = props; + const createDNode = ({ + compiledNode, + parentDNode, + taskTemplate, + typeOverride, + }: CreateDNodeProps): dNode => { const nodeValue = taskTemplate == null ? compiledNode @@ -99,6 +105,9 @@ export const transformerWorkflowToDag = ( ? getNodeTypeFromCompiledNode(compiledNode) : typeOverride; + const nodeExecution = nodeExecutionsById[scopedId]; + const isParent = nodeExecution && isParentNode(nodeExecution); + const output = { id: compiledNode.id, scopedId: scopedId, @@ -108,6 +117,7 @@ export const transformerWorkflowToDag = ( nodes: [], edges: [], gateNode: compiledNode.gateNode, + isParentNode: isParent, } as dNode; staticExecutionIdsMap[output.scopedId] = compiledNode; diff --git a/packages/console/src/components/common/utils.ts b/packages/console/src/components/common/utils.ts index 15c7defc8..5c7712552 100644 --- a/packages/console/src/components/common/utils.ts +++ b/packages/console/src/components/common/utils.ts @@ -30,14 +30,16 @@ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { 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]; + if (dynamicExecution) { + const dynamicExecutionId = + dynamicExecution.metadata?.specNodeId || dynamicExecution.id; + const uniqueParentId = dynamicExecution.fromUniqueParentId; + if (uniqueParentId) { + if (parentsToFetch[uniqueParentId]) { + parentsToFetch[uniqueParentId].push(dynamicExecutionId); + } else { + parentsToFetch[uniqueParentId] = [dynamicExecutionId]; + } } } } diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index e5f2a530f..ab43d7116 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,11 +1,17 @@ -import * as React from 'react'; -import { useState, useEffect, useContext } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { isNodeGateNode } from 'components/Executions/utils'; import { dNode } from 'models/Graph/types'; +import { useQueryClient } from 'react-query'; +import { fetchTaskExecutionList } from 'components/Executions/taskExecutionQueries'; +import { isMapTaskV1 } from 'models/Task/utils'; +import { ExternalResource, LogsByPhase } from 'models/Execution/types'; +import { getGroupedLogs } from 'components/Executions/TaskExecutionsList/utils'; +import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; +import { keyBy, merge } from 'lodash'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; @@ -49,7 +55,7 @@ const graphNodeCountChanged = (previous, data) => { } }; -const ReactFlowGraphComponent = ({ +export const ReactFlowGraphComponent = ({ data, onNodeSelectionChanged, onPhaseSelectionChanged, @@ -57,10 +63,16 @@ const ReactFlowGraphComponent = ({ isDetailsTabClosed, dynamicWorkflows, initialNodes, + shouldUpdate, + setShouldUpdate, }) => { - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const queryClient = useQueryClient(); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( + NodeExecutionsByIdContext, + ); const { compiledWorkflowClosure } = useNodeExecutionContext(); + const [loading, setLoading] = useState(true); const [pausedNodes, setPausedNodes] = useState([]); const [state, setState] = useState({ @@ -74,15 +86,92 @@ const ReactFlowGraphComponent = ({ rfGraphJson: null, }); + useEffect(() => { + // fetch map tasks data for all available node executions to display graph nodes properly + let isCurrent = true; + + async function fetchData(baseNodeExecutions, queryClient) { + setLoading(true); + const nodeExecutionsWithResources = await Promise.all( + baseNodeExecutions.map(async baseNodeExecution => { + if ( + !baseNodeExecution || + nodeExecutionsById[baseNodeExecution.scopedId].tasksFetched + ) { + return; + } + 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 }), + tasksFetched: true, + }; + }), + ); + + if (isCurrent) { + const nodeExecutionsWithResourcesMap = keyBy( + nodeExecutionsWithResources, + 'scopedId', + ); + const newNodeExecutionsById = merge( + nodeExecutionsById, + nodeExecutionsWithResourcesMap, + ); + setCurrentNodeExecutionsById(newNodeExecutionsById); + const newRFGraphData = buildReactFlowGraphData(); + setState(state => ({ + ...state, + nodeExecutionsById: newNodeExecutionsById, + rfGraphJson: newRFGraphData, + })); + setLoading(false); + } + } + + const nodeExecutions = Object.values(nodeExecutionsById); + if (nodeExecutions.length > 0) { + fetchData(nodeExecutions, queryClient); + } else { + if (isCurrent) { + setLoading(false); + } + } + return () => { + isCurrent = false; + }; + }, [initialNodes]); + const onAddNestedView = view => { const currentView = state.currentNestedView[view.parent] || []; const newView = { [view.parent]: [...currentView, view.view], }; - setState(state => ({ - ...state, - currentNestedView: { ...newView }, - })); + setState(state => ({ ...state, currentNestedView: { ...newView } })); }; const onRemoveNestedView = (viewParent, viewIndex) => { @@ -93,10 +182,7 @@ const ReactFlowGraphComponent = ({ if (currentNestedView[viewParent]?.length < 1) { delete currentNestedView[viewParent]; } - setState(state => ({ - ...state, - currentNestedView, - })); + setState(state => ({ ...state, currentNestedView })); }; const buildReactFlowGraphData = () => { @@ -115,18 +201,17 @@ const ReactFlowGraphComponent = ({ useEffect(() => { const newRFGraphData = buildReactFlowGraphData(); - setState(state => ({ - ...state, - rfGraphJson: newRFGraphData, - })); - }, [state.currentNestedView, state.nodeExecutionsById, isDetailsTabClosed]); + setState(state => ({ ...state, rfGraphJson: newRFGraphData })); + }, [ + state.currentNestedView, + state.nodeExecutionsById, + isDetailsTabClosed, + shouldUpdate, + ]); useEffect(() => { if (graphNodeCountChanged(state.data, data)) { - setState(state => ({ - ...state, - data: data, - })); + setState(state => ({ ...state, data: data })); } if ( nodeExecutionStatusChanged( @@ -135,10 +220,7 @@ const ReactFlowGraphComponent = ({ ) || nodeExecutionLogsChanged(state.nodeExecutionsById, nodeExecutionsById) ) { - setState(state => ({ - ...state, - nodeExecutionsById, - })); + setState(state => ({ ...state, nodeExecutionsById })); } }, [data, nodeExecutionsById]); @@ -177,6 +259,14 @@ const ReactFlowGraphComponent = ({ setPausedNodes(nodesWithExecutions); }, [initialNodes]); + if (loading) { + return ( +
+ +
+ ); + } + const containerStyle: React.CSSProperties = { display: 'flex', flex: `1 1 100%`, @@ -193,6 +283,7 @@ const ReactFlowGraphComponent = ({ type: RFGraphTypes.main, nodeExecutionsById, currentNestedView: state.currentNestedView, + setShouldUpdate, }; return (
@@ -207,5 +298,3 @@ const ReactFlowGraphComponent = ({ return state.rfGraphJson ? renderGraph() : <>; }; - -export default ReactFlowGraphComponent; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index e08de3a6e..e49f99345 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -1,7 +1,9 @@ -import * as React from 'react'; -import { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useContext } from 'react'; import ReactFlow, { Background } from 'react-flow-renderer'; -import { RFWrapperProps } from './types'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { useQueryClient } from 'react-query'; +import { fetchChildrenExecutions } from 'components/Executions/utils'; +import { getPositionedNodes, ReactFlowIdHash } from './utils'; import { ReactFlowCustomEndNode, ReactFlowCustomNestedPoint, @@ -13,7 +15,7 @@ import { ReactFlowStaticNode, ReactFlowGateNode, } from './customNodeComponents'; -import { getPositionedNodes, ReactFlowIdHash } from './utils'; +import { RFWrapperProps } from './types'; /** * Mapping for using custom nodes inside ReactFlow @@ -36,7 +38,12 @@ export const ReactFlowWrapper: React.FC = ({ backgroundStyle, currentNestedView, version, + setShouldUpdate, }) => { + const queryClient = useQueryClient(); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( + NodeExecutionsByIdContext, + ); const [state, setState] = useState({ shouldUpdate: true, nodes: rfGraphJson.nodes, @@ -104,7 +111,17 @@ export const ReactFlowWrapper: React.FC = ({ flexDirection: 'column', }; - const onNodeClick = () => { + const onNodeClick = async (_event, node) => { + const scopedId = node.data.scopedId; + if (node.data.isParentNode) { + fetchChildrenExecutions( + queryClient, + scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + } setState(state => ({ ...state, needFitView: false })); }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 0928950ec..3e356d0ba 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -1,11 +1,10 @@ -import * as React from 'react'; -import { useState, useEffect, useContext } from 'react'; +import React, { useState, useEffect, useContext } from 'react'; import { Handle, Position } from 'react-flow-renderer'; import { dTypes } from 'models/Graph/types'; import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums'; import { RENDER_ORDER } from 'components/Executions/TaskExecutionsList/constants'; import { whiteColor } from 'components/Theme/constants'; -import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline'; +import { PlayCircleOutline } from '@material-ui/icons'; import { Tooltip } from '@material-ui/core'; import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; import { getNodeFrontendPhase } from 'components/Executions/utils'; @@ -223,7 +222,7 @@ const TaskPhaseItem = ({ export const ReactFlowGateNode = ({ data }: RFNode) => { const { compiledWorkflowClosure } = useNodeExecutionContext(); - const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); const { nodeType, nodeExecutionStatus, @@ -263,7 +262,7 @@ export const ReactFlowGateNode = ({ data }: RFNode) => { {text} {phase === NodeExecutionPhase.PAUSED && ( - + )}
diff --git a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index 0ecec8324..1e4d2b1a6 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -58,24 +58,23 @@ interface BuildDataProps { rootParentNode: dNode; currentNestedView: string[]; } -const buildReactFlowDataProps = (props: BuildDataProps) => { - const { - node, - nodeExecutionsById, - onNodeSelectionChanged, - onPhaseSelectionChanged, - selectedPhase, - onAddNestedView, - onRemoveNestedView, - rootParentNode, - currentNestedView, - } = props; - +const buildReactFlowDataProps = ({ + node, + nodeExecutionsById, + onNodeSelectionChanged, + onPhaseSelectionChanged, + selectedPhase, + onAddNestedView, + onRemoveNestedView, + rootParentNode, + currentNestedView, +}: BuildDataProps) => { const { value: nodeValue, name: displayName, scopedId, type: nodeType, + isParentNode, } = node; const taskType = nodeValue?.template?.type ?? null; @@ -111,6 +110,7 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { scopedId, taskType, nodeLogsByPhase, + isParentNode, cacheStatus, selectedPhase, onNodeSelectionChanged: () => { diff --git a/packages/console/src/components/flytegraph/ReactFlow/types.ts b/packages/console/src/components/flytegraph/ReactFlow/types.ts index 87f6263e9..68e71d025 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/types.ts +++ b/packages/console/src/components/flytegraph/ReactFlow/types.ts @@ -15,12 +15,12 @@ export interface RFWrapperProps { onNodeSelectionChanged?: any; nodeExecutionsById?: any; version?: string; + setShouldUpdate?: (val: boolean) => void; } /* Note: extending to allow applying styles directly to handle */ export interface RFHandleProps extends HandleProps { style: any; - id?: string; } export enum RFGraphTypes { @@ -79,6 +79,7 @@ interface RFCustomData { dag: any; taskType: dTypes; cacheStatus: CatalogCacheStatus; + isParentNode: boolean; nodeLogsByPhase: LogsByPhase; selectedPhase: TaskExecutionPhase; currentNestedView: string[]; diff --git a/packages/console/src/models/Graph/types.ts b/packages/console/src/models/Graph/types.ts index b979e1cfb..d53a76d63 100644 --- a/packages/console/src/models/Graph/types.ts +++ b/packages/console/src/models/Graph/types.ts @@ -60,4 +60,5 @@ export interface dNode { expanded?: boolean; level?: number; execution?: NodeExecution; + isParentNode?: boolean; } From e83345e006937d6108260ef96f623bf1fa8a2cea Mon Sep 17 00:00:00 2001 From: Olga Nad Date: Tue, 31 Jan 2023 19:39:58 +0100 Subject: [PATCH 02/14] fix: incorrect import Signed-off-by: Olga Nad Signed-off-by: Jason Porter --- .../Executions/ExecutionDetails/ExecutionTabContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx index 5bd36ec01..60333ae5b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx @@ -53,8 +53,8 @@ const executionMatchesPhaseFilter = ( if (key === 'phase' && operation === FilterOperationName.VALUE_IN) { // default to UNKNOWN phase if the field does not exist on a closure const itemValue = - nodeExecutionPhaseConstants[nodeExecution?.closure[key]]?.value ?? - nodeExecutionPhaseConstants[0].value; + nodeExecutionPhaseConstants()[nodeExecution?.closure[key]]?.value ?? + nodeExecutionPhaseConstants()[0].value; // phase check filters always return values in an array const valuesArray = value as FilterOperationValueList; return valuesArray.includes(itemValue); From 45bca5cad5a405f349651e0355a0b217f86dc40c Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 2 Feb 2023 14:56:05 -0800 Subject: [PATCH 03/14] chore: fix expander bug (#677) * chore: fix expander bug Signed-off-by: Carina Ursu * chore: add await everywhere Signed-off-by: Carina Ursu --------- Signed-off-by: Carina Ursu Signed-off-by: Jason Porter --- .../Timeline/ExecutionTimeline.tsx | 2 +- .../ExecutionDetails/Timeline/TaskNames.tsx | 3 ++ .../Executions/Tables/NodeExecutionRow.tsx | 33 ++++++++------ .../Executions/Tables/NodeExecutionsTable.tsx | 19 ++++++-- .../Executions/Tables/RowExpander.tsx | 45 +++++++++++-------- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 2 +- 6 files changed, 66 insertions(+), 38 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 8f634c0cf..915cb3bfa 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -172,7 +172,7 @@ export const ExecutionTimeline: React.FC = ({ }; const toggleNode = async (id: string, scopedId: string, level: number) => { - fetchChildrenExecutions( + await fetchChildrenExecutions( queryClient, scopedId, nodeExecutionsById, diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx index 7a58f9fc5..83987f101 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -56,6 +56,8 @@ export const TaskNames = React.forwardRef( const styles = useStyles(); const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const expanderRef = React.useRef(); + return (
{nodes.map(node => { @@ -84,6 +86,7 @@ export const TaskNames = React.forwardRef(
{nodeExecution && isParentNode(nodeExecution) ? ( } expanded={node.expanded || false} onClick={() => onToggle(node.id, node.scopedId, nodeLevel) diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index 26107716f..c1511afa6 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -44,12 +44,13 @@ export const NodeExecutionRow: React.FC = ({ }) => { const styles = useStyles(); const theme = useTheme(); + const expanderRef = React.useRef(); + const tableStyles = useExecutionTableStyles(); const { selectedExecution, setSelectedExecution } = useContext(DetailsPanelContext); const nodeLevel = node?.level ?? 0; - const expanded = isExpanded(node); // For the first level, we want the borders to span the entire table, // so we'll use padding to space the content. For nested rows, we want the @@ -66,18 +67,20 @@ export const NodeExecutionRow: React.FC = ({ ? isEqual(selectedExecution, nodeExecution) : false; - const expanderContent = ( -
- {isParentNode(nodeExecution) ? ( - onToggle(node.id, node.scopedId, nodeLevel)} - /> - ) : ( -
- )} -
- ); + const expanderContent = React.useMemo(() => { + return isParentNode(nodeExecution) ? ( + } + key={node.scopedId} + expanded={isExpanded(node)} + onClick={() => { + onToggle(node.id, node.scopedId, nodeLevel); + }} + /> + ) : ( +
+ ); + }, [node, nodeLevel]); // open the side panel for selected execution's detail // use null in case if there is no execution provided - when it is null, will close side panel @@ -99,7 +102,9 @@ export const NodeExecutionRow: React.FC = ({
- {expanderContent} +
+ {expanderContent} +
{columns.map(({ className, key: columnKey, cellRenderer }) => (
= ({ ); useEffect(() => { - setOriginalNodes( - appliedFilters.length > 0 && filteredNodes ? filteredNodes : initialNodes, - ); + setOriginalNodes(ogn => { + const newNodes = + appliedFilters.length > 0 && filteredNodes + ? filteredNodes + : merge(initialNodes, ogn); + + if (!eq(newNodes, ogn)) { + return newNodes; + } + + return ogn; + }); + const plainNodes = convertToPlainNodes(originalNodes); const updatedShownNodesMap = plainNodes.map(node => { const execution = nodeExecutionsById[node.scopedId]; @@ -88,7 +99,7 @@ export const NodeExecutionsTable: React.FC = ({ }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); const toggleNode = async (id: string, scopedId: string, level: number) => { - fetchChildrenExecutions( + await fetchChildrenExecutions( queryClient, scopedId, nodeExecutionsById, diff --git a/packages/console/src/components/Executions/Tables/RowExpander.tsx b/packages/console/src/components/Executions/Tables/RowExpander.tsx index cca089ddb..b9d4f3642 100644 --- a/packages/console/src/components/Executions/Tables/RowExpander.tsx +++ b/packages/console/src/components/Executions/Tables/RowExpander.tsx @@ -1,25 +1,34 @@ +import * as React from 'react'; import { IconButton } from '@material-ui/core'; import ChevronRight from '@material-ui/icons/ChevronRight'; import ExpandMore from '@material-ui/icons/ExpandMore'; -import * as React from 'react'; import t from './strings'; -/** A simple expand/collapse arrow to be rendered next to row items. */ -export const RowExpander: React.FC<{ +interface RowExpanderProps { expanded: boolean; + key?: string; onClick: () => void; -}> = ({ expanded, onClick }) => ( - ) => { - // prevent the parent row body onClick event trigger - e.stopPropagation(); - onClick(); - }} - > - {expanded ? : } - -); +} +/** A simple expand/collapse arrow to be rendered next to row items. */ +export const RowExpander = React.forwardRef< + HTMLButtonElement, + RowExpanderProps +>(({ expanded, key, onClick }, ref) => { + return ( + ) => { + // prevent the parent row body onClick event trigger + e.stopPropagation(); + onClick(); + }} + > + {expanded ? : } + + ); +}); diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index e49f99345..e4fd67641 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -114,7 +114,7 @@ export const ReactFlowWrapper: React.FC = ({ const onNodeClick = async (_event, node) => { const scopedId = node.data.scopedId; if (node.data.isParentNode) { - fetchChildrenExecutions( + await fetchChildrenExecutions( queryClient, scopedId, nodeExecutionsById, From e614e8dfc9ac0f79c458153352598f0b6b2464c0 Mon Sep 17 00:00:00 2001 From: Olga Nad Date: Mon, 6 Feb 2023 12:48:00 +0100 Subject: [PATCH 04/14] fix: toggle in timeline Signed-off-by: Olga Nad Signed-off-by: Jason Porter --- .../Timeline/ExecutionTimeline.tsx | 50 ++++++------------- .../ExecutionDetails/Timeline/TaskNames.tsx | 7 ++- .../Executions/Tables/NodeExecutionsTable.tsx | 32 +----------- .../src/components/Executions/utils.ts | 31 ++++++++++++ 4 files changed, 54 insertions(+), 66 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 915cb3bfa..2d8e98206 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -6,17 +6,16 @@ import React, { useState, } from 'react'; import { makeStyles, Typography } from '@material-ui/core'; -import { - isEndNode, - isStartNode, - isExpanded, -} from 'components/WorkflowGraph/utils'; import { tableHeaderColor } from 'components/Theme/constants'; import { timestampToDate } from 'common/utils'; import { dNode } from 'models/Graph/types'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; -import { fetchChildrenExecutions } from 'components/Executions/utils'; +import { + fetchChildrenExecutions, + searchNode, +} from 'components/Executions/utils'; import { useQueryClient } from 'react-query'; +import { eq, merge } from 'lodash'; import { convertToPlainNodes } from './helpers'; import { ChartHeader } from './ChartHeader'; import { useScaleContext } from './scaleContext'; @@ -103,10 +102,16 @@ export const ExecutionTimeline: React.FC = ({ const { chartInterval: chartTimeInterval } = useScaleContext(); useEffect(() => { - setOriginalNodes(initialNodes); - }, [initialNodes]); + setOriginalNodes(ogn => { + const newNodes = merge(initialNodes, ogn); + + if (!eq(newNodes, ogn)) { + return newNodes; + } + + return ogn; + }); - useEffect(() => { const plainNodes = convertToPlainNodes(originalNodes); const updatedShownNodesMap = plainNodes.map(node => { const execution = nodeExecutionsById[node.scopedId]; @@ -123,7 +128,7 @@ export const ExecutionTimeline: React.FC = ({ if (firstStartedAt) { setStartedAt(timestampToDate(firstStartedAt)); } - }, [originalNodes, nodeExecutionsById]); + }, [initialNodes, originalNodes, nodeExecutionsById]); const { items: barItemsData, totalDurationSec } = getChartDurationData( showNodes, @@ -179,30 +184,7 @@ export const ExecutionTimeline: React.FC = ({ setCurrentNodeExecutionsById, setShouldUpdate, ); - - const searchNode = (nodes: dNode[], nodeLevel: number) => { - if (!nodes || nodes.length === 0) { - return; - } - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (isStartNode(node) || isEndNode(node)) { - continue; - } - if ( - node.id === id && - node.scopedId === scopedId && - nodeLevel === level - ) { - nodes[i].expanded = !nodes[i].expanded; - return; - } - if (node.nodes.length > 0 && isExpanded(node)) { - searchNode(node.nodes, nodeLevel + 1); - } - } - }; - searchNode(originalNodes, 0); + searchNode(originalNodes, 0, id, scopedId, level); setOriginalNodes([...originalNodes]); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx index 83987f101..dcb94c60a 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -1,7 +1,10 @@ import React, { useContext } from 'react'; import { IconButton, makeStyles, Theme, Tooltip } from '@material-ui/core'; import { RowExpander } from 'components/Executions/Tables/RowExpander'; -import { getNodeTemplateName } from 'components/WorkflowGraph/utils'; +import { + getNodeTemplateName, + isExpanded, +} from 'components/WorkflowGraph/utils'; import { dNode } from 'models/Graph/types'; import { PlayCircleOutline } from '@material-ui/icons'; import { isParentNode } from 'components/Executions/utils'; @@ -87,7 +90,7 @@ export const TaskNames = React.forwardRef( {nodeExecution && isParentNode(nodeExecution) ? ( } - expanded={node.expanded || false} + expanded={isExpanded(node)} onClick={() => onToggle(node.id, node.scopedId, nodeLevel) } diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 043958b25..8e5371d23 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -7,11 +7,6 @@ import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { dateToTimestamp } from 'common/utils'; import React, { useMemo, useEffect, useState, useContext } from 'react'; -import { - isEndNode, - isExpanded, - isStartNode, -} from 'components/WorkflowGraph/utils'; import { useQueryClient } from 'react-query'; import { merge, eq } from 'lodash'; import { ExecutionsTableHeader } from './ExecutionsTableHeader'; @@ -23,7 +18,7 @@ import { convertToPlainNodes } from '../ExecutionDetails/Timeline/helpers'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionRow } from './NodeExecutionRow'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { fetchChildrenExecutions } from '../utils'; +import { fetchChildrenExecutions, searchNode } from '../utils'; interface NodeExecutionsTableProps { initialNodes: dNode[]; @@ -106,30 +101,7 @@ export const NodeExecutionsTable: React.FC = ({ setCurrentNodeExecutionsById, setShouldUpdate, ); - - const searchNode = (nodes: dNode[], nodeLevel: number) => { - if (!nodes || nodes.length === 0) { - return; - } - for (let i = 0; i < nodes.length; i++) { - const node = nodes[i]; - if (isStartNode(node) || isEndNode(node)) { - continue; - } - if ( - node.id === id && - node.scopedId === scopedId && - nodeLevel === level - ) { - nodes[i].expanded = !nodes[i].expanded; - return; - } - if (node.nodes.length > 0 && isExpanded(node)) { - searchNode(node.nodes, nodeLevel + 1); - } - } - }; - searchNode(originalNodes, 0); + searchNode(originalNodes, 0, id, scopedId, level); setOriginalNodes([...originalNodes]); }; diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index 14583479e..b67507a08 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -1,4 +1,9 @@ import { durationToMilliseconds, timestampToDate } from 'common/utils'; +import { + isEndNode, + isExpanded, + isStartNode, +} from 'components/WorkflowGraph/utils'; import { clone, isEqual, keyBy, merge } from 'lodash'; import { runningExecutionStates, @@ -19,6 +24,7 @@ import { NodeExecutionIdentifier, TaskExecution, } from 'models/Execution/types'; +import { dNode } from 'models/Graph/types'; import { CompiledNode } from 'models/Node/types'; import { QueryClient } from 'react-query'; import { @@ -215,6 +221,31 @@ export function getNodeFrontendPhase( : phase; } +export function searchNode( + nodes: dNode[], + nodeLevel: number, + id: string, + scopedId: string, + level: number, +) { + if (!nodes || nodes.length === 0) { + return; + } + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (isStartNode(node) || isEndNode(node)) { + continue; + } + if (node.id === id && node.scopedId === scopedId && nodeLevel === level) { + nodes[i].expanded = !nodes[i].expanded; + return; + } + if (node.nodes.length > 0 && isExpanded(node)) { + searchNode(node.nodes, nodeLevel + 1, id, scopedId, level); + } + } +} + export async function fetchChildrenExecutions( queryClient: QueryClient, scopedId: string, From 5fa9176f47a856cef3c5c8131b1736bf995d8f2e Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Feb 2023 13:34:28 -0500 Subject: [PATCH 05/14] fix: checkForDynamicExecutions Signed-off-by: Jason Porter --- packages/console/src/components/common/utils.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/console/src/components/common/utils.ts b/packages/console/src/components/common/utils.ts index 5c7712552..3cff6d5b3 100644 --- a/packages/console/src/components/common/utils.ts +++ b/packages/console/src/components/common/utils.ts @@ -27,13 +27,15 @@ export function measureText(fontDefinition: string, text: string) { */ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { const parentsToFetch = {}; + const executionsByNodeId = {}; for (const executionId in allExecutions) { + const execution = allExecutions[executionId]; + executionsByNodeId[execution.id.nodeId] = execution; if (!staticExecutions[executionId]) { - const dynamicExecution = allExecutions[executionId]; - if (dynamicExecution) { + if (execution) { const dynamicExecutionId = - dynamicExecution.metadata?.specNodeId || dynamicExecution.id; - const uniqueParentId = dynamicExecution.fromUniqueParentId; + execution.metadata?.specNodeId || execution.id; + const uniqueParentId = execution.fromUniqueParentId; if (uniqueParentId) { if (parentsToFetch[uniqueParentId]) { parentsToFetch[uniqueParentId].push(dynamicExecutionId); @@ -46,7 +48,8 @@ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { } const result = {}; for (const parentId in parentsToFetch) { - result[parentId] = allExecutions[parentId]; + const execution = executionsByNodeId[parentId]; + result[execution.scopedId] = execution; } return result; }; From 0c99d87acd386b990460a114fc5e3f9fb219f1e3 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Feb 2023 13:35:31 -0500 Subject: [PATCH 06/14] Revert "fix: checkForDynamicExecutions" This reverts commit 450d1449403aa47399b4e66b6c453cd2dd4eb91a. Signed-off-by: Jason Porter --- packages/console/src/components/common/utils.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/console/src/components/common/utils.ts b/packages/console/src/components/common/utils.ts index 3cff6d5b3..5c7712552 100644 --- a/packages/console/src/components/common/utils.ts +++ b/packages/console/src/components/common/utils.ts @@ -27,15 +27,13 @@ export function measureText(fontDefinition: string, text: string) { */ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { const parentsToFetch = {}; - const executionsByNodeId = {}; for (const executionId in allExecutions) { - const execution = allExecutions[executionId]; - executionsByNodeId[execution.id.nodeId] = execution; if (!staticExecutions[executionId]) { - if (execution) { + const dynamicExecution = allExecutions[executionId]; + if (dynamicExecution) { const dynamicExecutionId = - execution.metadata?.specNodeId || execution.id; - const uniqueParentId = execution.fromUniqueParentId; + dynamicExecution.metadata?.specNodeId || dynamicExecution.id; + const uniqueParentId = dynamicExecution.fromUniqueParentId; if (uniqueParentId) { if (parentsToFetch[uniqueParentId]) { parentsToFetch[uniqueParentId].push(dynamicExecutionId); @@ -48,8 +46,7 @@ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { } const result = {}; for (const parentId in parentsToFetch) { - const execution = executionsByNodeId[parentId]; - result[execution.scopedId] = execution; + result[parentId] = allExecutions[parentId]; } return result; }; From 595f6eb47dd1322e778acdea63c51773740b69a8 Mon Sep 17 00:00:00 2001 From: james-union <105876962+james-union@users.noreply.github.com> Date: Fri, 10 Feb 2023 12:06:23 -0500 Subject: [PATCH 07/14] Perf Refactor: Some parent nodes don't load children (#680) fix: checkForDynamicExecutions Signed-off-by: Jason Porter --- packages/console/src/components/common/utils.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/console/src/components/common/utils.ts b/packages/console/src/components/common/utils.ts index 5c7712552..3cff6d5b3 100644 --- a/packages/console/src/components/common/utils.ts +++ b/packages/console/src/components/common/utils.ts @@ -27,13 +27,15 @@ export function measureText(fontDefinition: string, text: string) { */ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { const parentsToFetch = {}; + const executionsByNodeId = {}; for (const executionId in allExecutions) { + const execution = allExecutions[executionId]; + executionsByNodeId[execution.id.nodeId] = execution; if (!staticExecutions[executionId]) { - const dynamicExecution = allExecutions[executionId]; - if (dynamicExecution) { + if (execution) { const dynamicExecutionId = - dynamicExecution.metadata?.specNodeId || dynamicExecution.id; - const uniqueParentId = dynamicExecution.fromUniqueParentId; + execution.metadata?.specNodeId || execution.id; + const uniqueParentId = execution.fromUniqueParentId; if (uniqueParentId) { if (parentsToFetch[uniqueParentId]) { parentsToFetch[uniqueParentId].push(dynamicExecutionId); @@ -46,7 +48,8 @@ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { } const result = {}; for (const parentId in parentsToFetch) { - result[parentId] = allExecutions[parentId]; + const execution = executionsByNodeId[parentId]; + result[execution.scopedId] = execution; } return result; }; From a1f831f94c20373284ba1fd3f8dcea59dc0172b5 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Fri, 10 Feb 2023 14:42:56 -0800 Subject: [PATCH 08/14] chore: simplify graph code Signed-off-by: Carina Ursu Signed-off-by: Jason Porter --- .../WorkflowGraph/WorkflowGraph.tsx | 1 - .../console/src/components/common/utils.ts | 2 +- .../ReactFlow/ReactFlowGraphComponent.tsx | 165 +++++------------- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 8 +- website/webpack.utilities.ts | 2 +- 5 files changed, 50 insertions(+), 128 deletions(-) diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index a76ed09e0..dfbc8da5c 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -54,7 +54,6 @@ export const WorkflowGraph: React.FC = ({ return ( { const executionsByNodeId = {}; for (const executionId in allExecutions) { const execution = allExecutions[executionId]; - executionsByNodeId[execution.id.nodeId] = execution; + executionsByNodeId[execution?.id.nodeId] = execution; if (!staticExecutions[executionId]) { if (execution) { const dynamicExecutionId = diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index ab43d7116..84d451a87 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useContext } from 'react'; +import React, { useState, useEffect, useContext, useMemo } from 'react'; import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; @@ -18,50 +18,12 @@ import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; import { PausedTasksComponent } from './PausedTasksComponent'; -const nodeExecutionStatusChanged = (previous, nodeExecutionsById) => { - for (const exe in nodeExecutionsById) { - const oldStatus = previous[exe]?.closure.phase; - const newStatus = nodeExecutionsById[exe]?.closure.phase; - if (oldStatus !== newStatus) { - return true; - } - } - return false; -}; - -const nodeExecutionLogsChanged = (previous, nodeExecutionsById) => { - for (const exe in nodeExecutionsById) { - const oldLogs = previous[exe]?.logsByPhase ?? new Map(); - const newLogs = nodeExecutionsById[exe]?.logsByPhase ?? new Map(); - if (oldLogs.size !== newLogs.size) { - return true; - } - for (const phase in newLogs) { - const oldNumOfLogs = oldLogs.get(phase)?.length ?? 0; - const newNumOfLogs = newLogs.get(phase)?.length ?? 0; - if (oldNumOfLogs !== newNumOfLogs) { - return true; - } - } - } - return false; -}; - -const graphNodeCountChanged = (previous, data) => { - if (previous.nodes.length !== data.nodes.length) { - return true; - } else { - return false; - } -}; - export const ReactFlowGraphComponent = ({ data, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, isDetailsTabClosed, - dynamicWorkflows, initialNodes, shouldUpdate, setShouldUpdate, @@ -74,17 +36,51 @@ export const ReactFlowGraphComponent = ({ const [loading, setLoading] = useState(true); const [pausedNodes, setPausedNodes] = useState([]); + const [currentNestedView, setcurrentNestedView] = useState({}); + + const onAddNestedView = view => { + const currentView = currentNestedView[view.parent] || []; + const newView = { + [view.parent]: [...currentView, view.view], + }; + setcurrentNestedView(newView); + }; - const [state, setState] = useState({ + const onRemoveNestedView = (viewParent, viewIndex) => { + const newcurrentNestedView: any = { ...currentNestedView }; + newcurrentNestedView[viewParent] = newcurrentNestedView[viewParent]?.filter( + (_item, i) => i <= viewIndex, + ); + if (newcurrentNestedView[viewParent]?.length < 1) { + delete newcurrentNestedView[viewParent]; + } + setcurrentNestedView(newcurrentNestedView); + }; + + const rfGraphJson = useMemo(() => { + return ConvertFlyteDagToReactFlows({ + root: data, + nodeExecutionsById, + onNodeSelectionChanged, + onPhaseSelectionChanged, + selectedPhase, + onAddNestedView, + onRemoveNestedView, + currentNestedView, + maxRenderDepth: 1, + } as ConvertDagProps); + }, [ data, - dynamicWorkflows, - currentNestedView: {}, + isDetailsTabClosed, + shouldUpdate, nodeExecutionsById, - selectedPhase, onNodeSelectionChanged, onPhaseSelectionChanged, - rfGraphJson: null, - }); + selectedPhase, + onAddNestedView, + onRemoveNestedView, + currentNestedView, + ]); useEffect(() => { // fetch map tasks data for all available node executions to display graph nodes properly @@ -143,12 +139,6 @@ export const ReactFlowGraphComponent = ({ nodeExecutionsWithResourcesMap, ); setCurrentNodeExecutionsById(newNodeExecutionsById); - const newRFGraphData = buildReactFlowGraphData(); - setState(state => ({ - ...state, - nodeExecutionsById: newNodeExecutionsById, - rfGraphJson: newRFGraphData, - })); setLoading(false); } } @@ -166,73 +156,6 @@ export const ReactFlowGraphComponent = ({ }; }, [initialNodes]); - const onAddNestedView = view => { - const currentView = state.currentNestedView[view.parent] || []; - const newView = { - [view.parent]: [...currentView, view.view], - }; - setState(state => ({ ...state, currentNestedView: { ...newView } })); - }; - - const onRemoveNestedView = (viewParent, viewIndex) => { - const currentNestedView: any = { ...state.currentNestedView }; - currentNestedView[viewParent] = currentNestedView[viewParent]?.filter( - (_item, i) => i <= viewIndex, - ); - if (currentNestedView[viewParent]?.length < 1) { - delete currentNestedView[viewParent]; - } - setState(state => ({ ...state, currentNestedView })); - }; - - const buildReactFlowGraphData = () => { - return ConvertFlyteDagToReactFlows({ - root: state.data, - nodeExecutionsById: state.nodeExecutionsById, - onNodeSelectionChanged: state.onNodeSelectionChanged, - onPhaseSelectionChanged: state.onPhaseSelectionChanged, - selectedPhase, - onAddNestedView, - onRemoveNestedView, - currentNestedView: state.currentNestedView, - maxRenderDepth: 1, - } as ConvertDagProps); - }; - - useEffect(() => { - const newRFGraphData = buildReactFlowGraphData(); - setState(state => ({ ...state, rfGraphJson: newRFGraphData })); - }, [ - state.currentNestedView, - state.nodeExecutionsById, - isDetailsTabClosed, - shouldUpdate, - ]); - - useEffect(() => { - if (graphNodeCountChanged(state.data, data)) { - setState(state => ({ ...state, data: data })); - } - if ( - nodeExecutionStatusChanged( - state.nodeExecutionsById, - nodeExecutionsById, - ) || - nodeExecutionLogsChanged(state.nodeExecutionsById, nodeExecutionsById) - ) { - setState(state => ({ ...state, nodeExecutionsById })); - } - }, [data, nodeExecutionsById]); - - useEffect(() => { - setState(state => ({ - ...state, - onNodeSelectionChanged, - onPhaseSelectionChanged, - selectedPhase, - })); - }, [onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase]); - const backgroundStyle = getRFBackground().nested; useEffect(() => { @@ -279,10 +202,10 @@ export const ReactFlowGraphComponent = ({ const renderGraph = () => { const ReactFlowProps: RFWrapperProps = { backgroundStyle, - rfGraphJson: state.rfGraphJson, + rfGraphJson, type: RFGraphTypes.main, nodeExecutionsById, - currentNestedView: state.currentNestedView, + currentNestedView: currentNestedView, setShouldUpdate, }; return ( @@ -296,5 +219,5 @@ export const ReactFlowGraphComponent = ({ ); }; - return state.rfGraphJson ? renderGraph() : <>; + return rfGraphJson ? renderGraph() : <>; }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index e4fd67641..e2a5944a4 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -46,8 +46,8 @@ export const ReactFlowWrapper: React.FC = ({ ); const [state, setState] = useState({ shouldUpdate: true, - nodes: rfGraphJson.nodes, - edges: rfGraphJson.edges, + nodes: rfGraphJson?.nodes, + edges: rfGraphJson?.edges, version: version, reactFlowInstance: null, needFitView: false, @@ -57,8 +57,8 @@ export const ReactFlowWrapper: React.FC = ({ setState(state => ({ ...state, shouldUpdate: true, - nodes: rfGraphJson.nodes, - edges: rfGraphJson.edges.map(edge => ({ ...edge, zIndex: 0 })), + nodes: rfGraphJson?.nodes, + edges: rfGraphJson?.edges?.map(edge => ({ ...edge, zIndex: 0 })), })); }, [rfGraphJson]); diff --git a/website/webpack.utilities.ts b/website/webpack.utilities.ts index 6f32fcadd..002d85320 100644 --- a/website/webpack.utilities.ts +++ b/website/webpack.utilities.ts @@ -76,7 +76,7 @@ export const getConfigFile = (mode: Mode): string => // Determines whether to use CDN based on current development mode. export const getShouldLoadReactFromCDN = (mode: Mode) => mode === 'production' || - fs.existsSync(path.resolve(__dirname, '../node_modules/react')); + !fs.existsSync(path.resolve(__dirname, '../node_modules/react')); // Report current configuration export const logWebpackStats = (mode: Mode) => { From b51fe8866c234d06c09bcf5d00b0adc011022cac Mon Sep 17 00:00:00 2001 From: Jason Porter Date: Sun, 12 Feb 2023 18:44:21 -0800 Subject: [PATCH 09/14] adding util Signed-off-by: Jason Porter --- .../console/src/components/flytegraph/ReactFlow/utils.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/console/src/components/flytegraph/ReactFlow/utils.tsx b/packages/console/src/components/flytegraph/ReactFlow/utils.tsx index efe95829e..9fa902b3f 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/utils.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/utils.tsx @@ -123,6 +123,10 @@ export const getStatusColor: ( } }; +export const isUnFetchedDynamicNode = node => { + return node.isParentNode && node.nodes.length === 0; +}; + export const getNestedGraphContainerStyle = overwrite => { let width = overwrite.width; let height = overwrite.height; From c1f4e5032f95745091e4e0db710eb13b2c187ab1 Mon Sep 17 00:00:00 2001 From: Jason Porter Date: Sun, 12 Feb 2023 18:59:20 -0800 Subject: [PATCH 10/14] Changing type Signed-off-by: Jason Porter --- .../ReactFlow/transformDAGToReactFlowV2.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index 1e4d2b1a6..13175dc33 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -8,7 +8,7 @@ import { import { createDebugLogger } from 'common/log'; import { LogsByPhase } from 'models/Execution/types'; import { isMapTaskType } from 'models/Task/utils'; -import { ReactFlowGraphConfig } from './utils'; +import { isUnFetchedDynamicNode, ReactFlowGraphConfig } from './utils'; import { ConvertDagProps } from './types'; interface rfNode extends Node { @@ -277,6 +277,18 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { } } + /* + * Because dyanmic nodes are now fetched on demand we need to check + * for nodes that are parents without children; in which case we want + * to override the type to mimic unopened subworkflow (nestedMaxDepth) + */ + const type = + isStaticGraph === true + ? dTypes.staticNode + : isUnFetchedDynamicNode(node) + ? dTypes.nestedMaxDepth + : undefined; + if (rootParentNode) { const rootParentId = rootParentNode.scopedId; const contextParentId = contextParent?.scopedId; From 0c7e5e5d6dc0bd3997086800bcda6c5e31c7a5bb Mon Sep 17 00:00:00 2001 From: Jason Porter Date: Mon, 13 Feb 2023 08:16:17 -0800 Subject: [PATCH 11/14] Updated click handlers Signed-off-by: Jason Porter --- .../ReactFlow/ReactFlowGraphComponent.tsx | 29 ++++++++++++++----- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 17 +---------- .../ReactFlow/transformDAGToReactFlowV2.tsx | 28 ++++++++++-------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 84d451a87..760b46705 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -3,7 +3,10 @@ import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/tra import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionPhase } from 'models/Execution/enums'; -import { isNodeGateNode } from 'components/Executions/utils'; +import { + fetchChildrenExecutions, + isNodeGateNode, +} from 'components/Executions/utils'; import { dNode } from 'models/Graph/types'; import { useQueryClient } from 'react-query'; import { fetchTaskExecutionList } from 'components/Executions/taskExecutionQueries'; @@ -13,7 +16,7 @@ import { getGroupedLogs } from 'components/Executions/TaskExecutionsList/utils'; import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { keyBy, merge } from 'lodash'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; -import { getRFBackground } from './utils'; +import { getRFBackground, isUnFetchedDynamicNode } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; import { PausedTasksComponent } from './PausedTasksComponent'; @@ -38,12 +41,22 @@ export const ReactFlowGraphComponent = ({ const [pausedNodes, setPausedNodes] = useState([]); const [currentNestedView, setcurrentNestedView] = useState({}); - const onAddNestedView = view => { - const currentView = currentNestedView[view.parent] || []; - const newView = { - [view.parent]: [...currentView, view.view], - }; - setcurrentNestedView(newView); + const onAddNestedView = async (view, sourceNode: any = null) => { + if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { + await fetchChildrenExecutions( + queryClient, + sourceNode.scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + } else { + const currentView = currentNestedView[view.parent] || []; + const newView = { + [view.parent]: [...currentView, view.view], + }; + setcurrentNestedView(newView); + } }; const onRemoveNestedView = (viewParent, viewIndex) => { diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index e2a5944a4..b304c1b35 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -38,12 +38,7 @@ export const ReactFlowWrapper: React.FC = ({ backgroundStyle, currentNestedView, version, - setShouldUpdate, }) => { - const queryClient = useQueryClient(); - const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( - NodeExecutionsByIdContext, - ); const [state, setState] = useState({ shouldUpdate: true, nodes: rfGraphJson?.nodes, @@ -111,17 +106,7 @@ export const ReactFlowWrapper: React.FC = ({ flexDirection: 'column', }; - const onNodeClick = async (_event, node) => { - const scopedId = node.data.scopedId; - if (node.data.isParentNode) { - await fetchChildrenExecutions( - queryClient, - scopedId, - nodeExecutionsById, - setCurrentNodeExecutionsById, - setShouldUpdate, - ); - } + const onNodeClick = async _event => { setState(state => ({ ...state, needFitView: false })); }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index 13175dc33..c7becd4c2 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -124,10 +124,13 @@ const buildReactFlowDataProps = ({ } }, onAddNestedView: () => { - onAddNestedView({ - parent: rootParentNode.scopedId, - view: scopedId, - }); + onAddNestedView( + { + parent: rootParentNode ? rootParentNode.scopedId : scopedId, + view: scopedId, + }, + node, + ); }, onRemoveNestedView, }; @@ -278,11 +281,14 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { } /* - * Because dyanmic nodes are now fetched on demand we need to check - * for nodes that are parents without children; in which case we want - * to override the type to mimic unopened subworkflow (nestedMaxDepth) + * case: isUnfetcedDyanmic + * dyanmic nodes are now fetched on demand; thse will be nodes that are + * parents without children; in which case we override the type nestedMaxDepth + * + * case dTypes.nestedMaxDepth + * for nodes that subworkflows; this is the unexpanded view */ - const type = + const typeOVerride = isStaticGraph === true ? dTypes.staticNode : isUnFetchedDynamicNode(node) @@ -307,16 +313,14 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { dataProps: nodeDataProps, rootParentNode: rootParentNode, parentNode: contextParent, - typeOverride: - isStaticGraph === true ? dTypes.staticNode : undefined, + typeOverride: typeOVerride, }); context.nodes[reactFlowNode.id] = reactFlowNode; } else { const reactFlowNode = buildReactFlowNode({ node: node, dataProps: nodeDataProps, - typeOverride: - isStaticGraph === true ? dTypes.staticNode : undefined, + typeOverride: typeOVerride, }); root.nodes[reactFlowNode.id] = reactFlowNode; } From ed06dbfb81a611522053eab4477d82da4a157ecb Mon Sep 17 00:00:00 2001 From: Jason Porter Date: Mon, 13 Feb 2023 11:24:39 -0800 Subject: [PATCH 12/14] Fixed graph dynamic node issue Signed-off-by: Jason Porter --- .../flytegraph/ReactFlow/ReactFlowGraphComponent.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 760b46705..e2939e926 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -50,13 +50,13 @@ export const ReactFlowGraphComponent = ({ setCurrentNodeExecutionsById, setShouldUpdate, ); - } else { - const currentView = currentNestedView[view.parent] || []; - const newView = { - [view.parent]: [...currentView, view.view], - }; - setcurrentNestedView(newView); } + + const currentView = currentNestedView[view.parent] || []; + const newView = { + [view.parent]: [...currentView, view.view], + }; + setcurrentNestedView(newView); }; const onRemoveNestedView = (viewParent, viewIndex) => { From a895d1ffc0ce4c3d7a3cb76d23ed2fd5fad64fc4 Mon Sep 17 00:00:00 2001 From: Jason Porter Date: Tue, 14 Feb 2023 09:41:12 -0800 Subject: [PATCH 13/14] Key was not a valid prop in this case, removed Signed-off-by: Jason Porter --- .../src/components/Executions/Tables/NodeExecutionRow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index c1511afa6..9d0165c7e 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -71,7 +71,6 @@ export const NodeExecutionRow: React.FC = ({ return isParentNode(nodeExecution) ? ( } - key={node.scopedId} expanded={isExpanded(node)} onClick={() => { onToggle(node.id, node.scopedId, nodeLevel); From 70b42e4311ab9357dd41eb0f9464d7670f19ce87 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Tue, 14 Feb 2023 12:05:53 -0800 Subject: [PATCH 14/14] chore: fix tests Signed-off-by: Carina Ursu Signed-off-by: Jason Porter --- .../ExecutionDetails/test/TaskNames.test.tsx | 66 ++++++++++++++++++- .../Tables/test/NodeExecutionRow.test.tsx | 22 ++++--- .../Tables/test/NodeExecutionsTable.test.tsx | 7 +- .../LaunchForm/test/ResumeSignalForm.test.tsx | 2 +- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx index d66bf3355..7a8807b39 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx @@ -1,11 +1,27 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { dTypes } from 'models/Graph/types'; +import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { mockWorkflowId } from 'mocks/data/fixtures/types'; +import { createTestQueryClient } from 'test/utils'; +import { dateToTimestamp } from 'common/utils'; +import { createMockWorkflow } from 'models/__mocks__/workflowData'; import { TaskNames } from '../Timeline/TaskNames'; const onToggle = jest.fn(); const onAction = jest.fn(); +jest.mock('models/Workflow/api', () => { + const originalModule = jest.requireActual('models/Workflow/api'); + return { + __esModule: true, + ...originalModule, + getWorkflow: jest.fn().mockResolvedValue({}), + }; +}); + const node1 = { id: 'n1', scopedId: 'n1', @@ -25,7 +41,51 @@ const node2 = { }; describe('ExecutionDetails > Timeline > TaskNames', () => { - const renderComponent = props => render(); + let queryClient: QueryClient; + beforeEach(() => { + queryClient = createTestQueryClient(); + }); + + const renderComponent = props => { + const nodeExecutionsById = props.nodes.reduce( + (accumulator, currentValue) => { + accumulator[currentValue.id] = { + closure: { + createdAt: dateToTimestamp(new Date()), + outputUri: '', + phase: 1, + }, + metadata: currentValue.metadata, + id: { + executionId: { + domain: 'development', + name: 'MyWorkflow', + project: 'flytetest', + }, + nodeId: currentValue.id, + }, + inputUri: '', + scopedId: currentValue.scopedId, + }; + return accumulator; + }, + {}, + ); + return render( + + + {}, + }} + > + + + + , + ); + }; it('should render task names list', () => { const nodes = [node1, node2]; @@ -56,8 +116,8 @@ describe('ExecutionDetails > Timeline > TaskNames', () => { }, ]; const nodes = [ - { ...node1, nodes: nestedNodes }, - { ...node2, nodes: nestedNodes }, + { ...node1, metadata: { isParentNode: true }, nodes: nestedNodes }, + { ...node2, metadata: { isParentNode: true }, nodes: nestedNodes }, ]; const { getAllByTestId, getAllByTitle } = renderComponent({ nodes, diff --git a/packages/console/src/components/Executions/Tables/test/NodeExecutionRow.test.tsx b/packages/console/src/components/Executions/Tables/test/NodeExecutionRow.test.tsx index 24c4bdb05..fe1419661 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionRow.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionRow.test.tsx @@ -14,10 +14,6 @@ import { NodeExecutionRow } from '../NodeExecutionRow'; jest.mock('components/Workflow/workflowQueries'); const { fetchWorkflow } = require('components/Workflow/workflowQueries'); -jest.mock('components/Executions/Tables/RowExpander', () => ({ - RowExpander: jest.fn(() =>
), -})); - const columns = []; const node = { id: 'n1', @@ -44,15 +40,15 @@ describe('Executions > Tables > NodeExecutionRow', () => { ); }); - const renderComponent = props => - render( + const renderComponent = props => { + return render( , ); - + }; it('should not render expander if node is a leaf', async () => { const { queryByRole, queryByTestId } = renderComponent({ columns, @@ -67,8 +63,14 @@ describe('Executions > Tables > NodeExecutionRow', () => { }); it('should render expander if node contains list of nodes', async () => { - const mockNode = { ...node, nodes: [node, node] }; - const { queryByRole, queryByTestId } = renderComponent({ + const mockNode = { + ...node, + nodes: [node, node], + }; + + (execution.metadata as any).isParentNode = true; + + const { queryByRole, queryByTitle } = renderComponent({ columns, node: mockNode, nodeExecution: execution, @@ -77,6 +79,6 @@ describe('Executions > Tables > NodeExecutionRow', () => { await waitFor(() => queryByRole('listitem')); expect(queryByRole('listitem')).toBeInTheDocument(); - expect(queryByTestId('expander')).toBeInTheDocument(); + expect(queryByTitle('Expand row')).toBeInTheDocument(); }); }); diff --git a/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx b/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx index 8aecbcbea..8460dc5e1 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -91,7 +91,12 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { render( - + {}, + }} + > { }} >