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..60333ae5b 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, @@ -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..2d8e98206 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -1,15 +1,21 @@ -import * as React from 'react'; +import React, { + createRef, + useContext, + useEffect, + useRef, + 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 { createRef, useContext, useEffect, useRef, useState } from 'react'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +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'; @@ -72,11 +78,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,14 +95,23 @@ 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(() => { - 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]; @@ -111,7 +128,7 @@ export const ExecutionTimeline: React.FC = ({ if (firstStartedAt) { setStartedAt(timestampToDate(firstStartedAt)); } - }, [originalNodes, nodeExecutionsById]); + }, [initialNodes, originalNodes, nodeExecutionsById]); const { items: barItemsData, totalDurationSec } = getChartDurationData( showNodes, @@ -159,30 +176,15 @@ export const ExecutionTimeline: React.FC = ({ } }; - const toggleNode = (id: string, scopeId: string, level: number) => { - 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 === scopeId && - nodeLevel === level - ) { - nodes[i].expanded = !nodes[i].expanded; - return; - } - if (node.nodes.length > 0 && isExpanded(node)) { - searchNode(node.nodes, nodeLevel + 1); - } - } - }; - searchNode(originalNodes, 0); + const toggleNode = async (id: string, scopedId: string, level: number) => { + await fetchChildrenExecutions( + queryClient, + scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + searchNode(originalNodes, 0, id, scopedId, level); setOriginalNodes([...originalNodes]); }; 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..dcb94c60a 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -1,10 +1,14 @@ -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 { + getNodeTemplateName, + isExpanded, +} 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 +57,16 @@ interface TaskNamesProps { export const TaskNames = React.forwardRef( ({ nodes, onScroll, onToggle, onAction }, ref) => { const styles = useStyles(); + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + + const expanderRef = React.useRef(); return (
{nodes.map(node => { const nodeLevel = node?.level ?? 0; + const nodeExecution = nodeExecutionsById[node.scopedId]; + return (
( }} >
- {node.nodes?.length ? ( + {nodeExecution && isParentNode(nodeExecution) ? ( } + expanded={isExpanded(node)} onClick={() => onToggle(node.id, node.scopedId, nodeLevel) } @@ -104,7 +114,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/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/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..9d0165c7e 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: { @@ -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,19 @@ export const NodeExecutionRow: React.FC = ({ ? isEqual(selectedExecution, nodeExecution) : false; - const expanderContent = ( -
- {node.nodes?.length ? ( - onToggle(node.id, node.scopedId, nodeLevel)} - /> - ) : ( -
- )} -
- ); + const expanderContent = React.useMemo(() => { + return isParentNode(nodeExecution) ? ( + } + 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 +101,9 @@ export const NodeExecutionRow: React.FC = ({
- {expanderContent} +
+ {expanderContent} +
{columns.map(({ className, key: columnKey, cellRenderer }) => (
void; } const scrollbarPadding = scrollbarSize(); @@ -43,10 +41,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, @@ -66,9 +68,19 @@ export const NodeExecutionsTable: React.FC = ({ ); 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]; @@ -81,30 +93,15 @@ export const NodeExecutionsTable: React.FC = ({ setShowNodes(updatedShownNodesMap); }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); - const toggleNode = (id: string, scopeId: string, level: number) => { - 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 === scopeId && - nodeLevel === level - ) { - nodes[i].expanded = !nodes[i].expanded; - return; - } - if (node.nodes.length > 0 && isExpanded(node)) { - searchNode(node.nodes, nodeLevel + 1); - } - } - }; - searchNode(originalNodes, 0); + const toggleNode = async (id: string, scopedId: string, level: number) => { + await fetchChildrenExecutions( + queryClient, + scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + searchNode(originalNodes, 0, id, scopedId, level); setOriginalNodes([...originalNodes]); }; 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/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/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( - + {}, + }} + > 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..b67507a08 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -1,4 +1,10 @@ import { durationToMilliseconds, timestampToDate } from 'common/utils'; +import { + isEndNode, + isExpanded, + isStartNode, +} from 'components/WorkflowGraph/utils'; +import { clone, isEqual, keyBy, merge } from 'lodash'; import { runningExecutionStates, terminalExecutionStates, @@ -18,13 +24,18 @@ 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 { nodeExecutionPhaseConstants, taskExecutionPhaseConstants, taskTypeToNodeExecutionDisplayType, workflowExecutionPhaseConstants, } from './constants'; +import { WorkflowNodeExecution } from './contexts'; +import { isChildGroupsFetched } from './ExecutionDetails/utils'; +import { fetchChildNodeExecutionGroups } from './nodeExecutionQueries'; import { ExecutionPhaseConstants, NodeExecutionDisplayType, @@ -209,3 +220,69 @@ export function getNodeFrontendPhase( ? NodeExecutionPhase.PAUSED : 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, + 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/Launch/LaunchForm/test/ResumeSignalForm.test.tsx b/packages/console/src/components/Launch/LaunchForm/test/ResumeSignalForm.test.tsx index 3a0dd5b09..a4e019c93 100644 --- a/packages/console/src/components/Launch/LaunchForm/test/ResumeSignalForm.test.tsx +++ b/packages/console/src/components/Launch/LaunchForm/test/ResumeSignalForm.test.tsx @@ -107,7 +107,7 @@ describe('ResumeSignalForm', () => { }} > void; } export interface DynamicWorkflowMapping { rootGraphNodeId: CompiledNode; @@ -31,6 +33,8 @@ export const WorkflowGraph: React.FC = ({ error, dynamicWorkflows, initialNodes, + shouldUpdate, + setShouldUpdate, }) => { if (error) { return ( @@ -50,13 +54,14 @@ export const WorkflowGraph: React.FC = ({ return ( ); }; 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..e4ab7f3e7 100644 --- a/packages/console/src/components/common/utils.ts +++ b/packages/console/src/components/common/utils.ts @@ -27,24 +27,29 @@ 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]; - 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 (execution) { + const dynamicExecutionId = + execution.metadata?.specNodeId || execution.id; + const uniqueParentId = execution.fromUniqueParentId; + if (uniqueParentId) { + if (parentsToFetch[uniqueParentId]) { + parentsToFetch[uniqueParentId].push(dynamicExecutionId); + } else { + parentsToFetch[uniqueParentId] = [dynamicExecutionId]; + } } } } } const result = {}; for (const parentId in parentsToFetch) { - result[parentId] = allExecutions[parentId]; + const execution = executionsByNodeId[parentId]; + result[execution.scopedId] = execution; } return result; }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index e5f2a530f..e2939e926 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,155 +1,173 @@ -import * as React from 'react'; -import { 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'; 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'; +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 { getRFBackground, isUnFetchedDynamicNode } from './utils'; 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; - } -}; - -const ReactFlowGraphComponent = ({ +export const ReactFlowGraphComponent = ({ data, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, 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 [currentNestedView, setcurrentNestedView] = useState({}); - const [state, setState] = useState({ - data, - dynamicWorkflows, - currentNestedView: {}, - nodeExecutionsById, - selectedPhase, - onNodeSelectionChanged, - onPhaseSelectionChanged, - rfGraphJson: null, - }); + const onAddNestedView = async (view, sourceNode: any = null) => { + if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { + await fetchChildrenExecutions( + queryClient, + sourceNode.scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + setShouldUpdate, + ); + } - const onAddNestedView = view => { - const currentView = state.currentNestedView[view.parent] || []; + const currentView = currentNestedView[view.parent] || []; const newView = { [view.parent]: [...currentView, view.view], }; - setState(state => ({ - ...state, - currentNestedView: { ...newView }, - })); + setcurrentNestedView(newView); }; const onRemoveNestedView = (viewParent, viewIndex) => { - const currentNestedView: any = { ...state.currentNestedView }; - currentNestedView[viewParent] = currentNestedView[viewParent]?.filter( + const newcurrentNestedView: any = { ...currentNestedView }; + newcurrentNestedView[viewParent] = newcurrentNestedView[viewParent]?.filter( (_item, i) => i <= viewIndex, ); - if (currentNestedView[viewParent]?.length < 1) { - delete currentNestedView[viewParent]; + if (newcurrentNestedView[viewParent]?.length < 1) { + delete newcurrentNestedView[viewParent]; } - setState(state => ({ - ...state, - currentNestedView, - })); + setcurrentNestedView(newcurrentNestedView); }; - const buildReactFlowGraphData = () => { + const rfGraphJson = useMemo(() => { return ConvertFlyteDagToReactFlows({ - root: state.data, - nodeExecutionsById: state.nodeExecutionsById, - onNodeSelectionChanged: state.onNodeSelectionChanged, - onPhaseSelectionChanged: state.onPhaseSelectionChanged, + root: data, + nodeExecutionsById, + onNodeSelectionChanged, + onPhaseSelectionChanged, selectedPhase, onAddNestedView, onRemoveNestedView, - currentNestedView: state.currentNestedView, + currentNestedView, maxRenderDepth: 1, } as ConvertDagProps); - }; - - useEffect(() => { - const newRFGraphData = buildReactFlowGraphData(); - setState(state => ({ - ...state, - rfGraphJson: newRFGraphData, - })); - }, [state.currentNestedView, state.nodeExecutionsById, isDetailsTabClosed]); + }, [ + data, + isDetailsTabClosed, + shouldUpdate, + nodeExecutionsById, + onNodeSelectionChanged, + onPhaseSelectionChanged, + selectedPhase, + onAddNestedView, + onRemoveNestedView, + currentNestedView, + ]); useEffect(() => { - if (graphNodeCountChanged(state.data, data)) { - setState(state => ({ - ...state, - data: data, - })); - } - if ( - nodeExecutionStatusChanged( - state.nodeExecutionsById, - nodeExecutionsById, - ) || - nodeExecutionLogsChanged(state.nodeExecutionsById, nodeExecutionsById) - ) { - setState(state => ({ - ...state, - nodeExecutionsById, - })); + // 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); + setLoading(false); + } } - }, [data, nodeExecutionsById]); - useEffect(() => { - setState(state => ({ - ...state, - onNodeSelectionChanged, - onPhaseSelectionChanged, - selectedPhase, - })); - }, [onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase]); + const nodeExecutions = Object.values(nodeExecutionsById); + if (nodeExecutions.length > 0) { + fetchData(nodeExecutions, queryClient); + } else { + if (isCurrent) { + setLoading(false); + } + } + return () => { + isCurrent = false; + }; + }, [initialNodes]); const backgroundStyle = getRFBackground().nested; @@ -177,6 +195,14 @@ const ReactFlowGraphComponent = ({ setPausedNodes(nodesWithExecutions); }, [initialNodes]); + if (loading) { + return ( +
+ +
+ ); + } + const containerStyle: React.CSSProperties = { display: 'flex', flex: `1 1 100%`, @@ -189,10 +215,11 @@ const ReactFlowGraphComponent = ({ const renderGraph = () => { const ReactFlowProps: RFWrapperProps = { backgroundStyle, - rfGraphJson: state.rfGraphJson, + rfGraphJson, type: RFGraphTypes.main, nodeExecutionsById, - currentNestedView: state.currentNestedView, + currentNestedView: currentNestedView, + setShouldUpdate, }; return (
@@ -205,7 +232,5 @@ const ReactFlowGraphComponent = ({ ); }; - return state.rfGraphJson ? renderGraph() : <>; + return 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..b304c1b35 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 @@ -39,8 +41,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, @@ -50,8 +52,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]); @@ -104,7 +106,7 @@ export const ReactFlowWrapper: React.FC = ({ flexDirection: 'column', }; - const onNodeClick = () => { + const onNodeClick = async _event => { 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..c7becd4c2 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 { @@ -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: () => { @@ -124,10 +124,13 @@ const buildReactFlowDataProps = (props: BuildDataProps) => { } }, onAddNestedView: () => { - onAddNestedView({ - parent: rootParentNode.scopedId, - view: scopedId, - }); + onAddNestedView( + { + parent: rootParentNode ? rootParentNode.scopedId : scopedId, + view: scopedId, + }, + node, + ); }, onRemoveNestedView, }; @@ -277,6 +280,21 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { } } + /* + * 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 typeOVerride = + isStaticGraph === true + ? dTypes.staticNode + : isUnFetchedDynamicNode(node) + ? dTypes.nestedMaxDepth + : undefined; + if (rootParentNode) { const rootParentId = rootParentNode.scopedId; const contextParentId = contextParent?.scopedId; @@ -295,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; } 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/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; 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; } 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) => {