From 35ccd382eb7194151cc2788d1a0b8b78a1c242e3 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 30 Mar 2023 19:32:06 -0700 Subject: [PATCH 01/25] fix: force node executions to pull their status Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionDetails.tsx | 6 +- .../ExecutionDetailsAppBarContent.tsx | 15 +- .../ExecutionDetails/ExecutionMetadata.tsx | 7 +- .../ExecutionDetails/ExecutionNodeViews.tsx | 95 ++-------- .../ExecutionDetails/ExecutionTab.tsx | 13 +- .../useExecutionNodeViewsState.ts | 15 +- .../ExecutionDetails/useNodeExecutionRow.ts | 38 ++++ .../Executions/ExecutionDetails/utils.ts | 8 +- .../Executions/Tables/NodeExecutionRow.tsx | 61 +++++- .../Executions/Tables/NodeExecutionsTable.tsx | 43 ++++- .../NodeExecutionDetailsContextProvider.tsx | 176 +++++++++++++++++ .../NodeExecutionsByIdContextProvider.tsx | 146 ++++++++++++++ .../NodeExecutionDetails/index.tsx | 178 +----------------- .../src/components/Executions/contexts.ts | 18 +- .../Executions/nodeExecutionQueries.ts | 21 +++ .../Executions/useNodeExecutionsById.ts | 23 --- .../src/components/Executions/utils.ts | 17 +- 17 files changed, 535 insertions(+), 345 deletions(-) create mode 100644 packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts create mode 100644 packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx create mode 100644 packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx delete mode 100644 packages/console/src/components/Executions/useNodeExecutionsById.ts diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx index ab6a570d1..a6945e962 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx @@ -61,10 +61,10 @@ const RenderExecutionDetails: React.FC = ({ return ( - +
- +
@@ -76,7 +76,7 @@ const RenderExecutionDetails: React.FC = ({
- +
); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx index 3e57d4fa8..c5fcc052a 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsAppBarContent.tsx @@ -8,7 +8,6 @@ import { ButtonCircularProgress } from 'components/common/ButtonCircularProgress import { MoreOptionsMenu } from 'components/common/MoreOptionsMenu'; import { useCommonStyles } from 'components/common/styles'; import { useLocationState } from 'components/hooks/useLocationState'; -import { Execution } from 'models/Execution/types'; import { Link as RouterLink } from 'react-router-dom'; import { history } from 'routes/history'; import { Routes } from 'routes/routes'; @@ -22,6 +21,7 @@ import { backLinkTitle, executionActionStrings } from './constants'; import { RelaunchExecutionForm } from './RelaunchExecutionForm'; import { getExecutionBackLink, getExecutionSourceId } from './utils'; import { useRecoverExecutionState } from './useRecoverExecutionState'; +import { ExecutionContext } from '../contexts'; const useStyles = makeStyles((theme: Theme) => { return { @@ -73,11 +73,12 @@ const useStyles = makeStyles((theme: Theme) => { }); /** Renders information about a given Execution into the NavBar */ -export const ExecutionDetailsAppBarContentInner: React.FC<{ - execution: Execution; -}> = ({ execution }) => { +export const ExecutionDetailsAppBarContentInner: React.FC<{}> = () => { const commonStyles = useCommonStyles(); const styles = useStyles(); + + const { execution } = React.useContext(ExecutionContext); + const [showInputsOutputs, setShowInputsOutputs] = React.useState(false); const [showRelaunchForm, setShowRelaunchForm] = React.useState(false); const { domain, name, project } = execution.id; @@ -231,12 +232,10 @@ export const ExecutionDetailsAppBarContentInner: React.FC<{ ); }; -export const ExecutionDetailsAppBarContent: React.FC<{ - execution: Execution; -}> = ({ execution }) => { +export const ExecutionDetailsAppBarContent: React.FC<{}> = () => { return ( - + ); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx index e83fa0543..a5a4428a8 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadata.tsx @@ -10,6 +10,7 @@ import { secondaryBackgroundColor } from 'components/Theme/constants'; import { Execution } from 'models/Execution/types'; import { Link as RouterLink } from 'react-router-dom'; import { Routes } from 'routes/routes'; +import { ExecutionContext } from '../contexts'; import { ExpandableExecutionError } from '../Tables/ExpandableExecutionError'; import { ExecutionMetadataLabels } from './constants'; import { ExecutionMetadataExtra } from './ExecutionMetadataExtra'; @@ -60,12 +61,12 @@ interface DetailItem { } /** Renders metadata details about a given Execution */ -export const ExecutionMetadata: React.FC<{ - execution: Execution; -}> = ({ execution }) => { +export const ExecutionMetadata: React.FC<{}> = () => { const commonStyles = useCommonStyles(); const styles = useStyles(); + const { execution } = React.useContext(ExecutionContext); + const { domain } = execution.id; const { abortMetadata, duration, error, startedAt, workflowId } = execution.closure; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index dc09b04d6..afc5e7111 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -1,22 +1,17 @@ -import React, { useEffect } from 'react'; +import React, { useContext } 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 } from 'models/Execution/types'; -import { clone, keyBy, merge } from 'lodash'; -import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; -import { FilterOperation } from 'models/AdminEntity/types'; -import { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from '../contexts'; +import { + NodeExecutionDetailsContextProvider, + NodeExecutionsByIdContextProvider, +} from '../contextProvider/NodeExecutionDetails'; +import { ExecutionContext } from '../contexts'; import { ExecutionFilters } from '../ExecutionFilters'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { tabs } from './constants'; -import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; import { ExecutionTab } from './ExecutionTab'; -import { useNodeExecutionsById } from '../useNodeExecutionsById'; const useStyles = makeStyles((theme: Theme) => ({ filters: { @@ -33,80 +28,20 @@ const useStyles = makeStyles((theme: Theme) => ({ background: secondaryBackgroundColor, paddingLeft: theme.spacing(3.5), }, - loading: { - margin: 'auto', - }, })); -const isPhaseFilter = (appliedFilters: FilterOperation[]) => { - if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { - return true; - } - return false; -}; - -interface ExecutionNodeViewsProps { - execution: Execution; -} - /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionNodeViews: React.FC = ({ - execution, -}) => { +export const ExecutionNodeViews: React.FC = () => { const defaultTab = tabs.nodes.id; const styles = useStyles(); const filterState = useNodeExecutionFiltersState(); const tabState = useTabState(tabs, defaultTab); + const { execution } = useContext(ExecutionContext); const { closure: { workflowId }, } = execution; - // query to get all data to build Graph and Timeline - const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); - // query to get filtered data to narrow down Table outputs - const { - nodeExecutionsQuery: { data: filteredNodeExecutions }, - } = useExecutionNodeViewsState(execution, filterState.appliedFilters); - - const { nodeExecutionsById, setCurrentNodeExecutionsById } = - useNodeExecutionsById(); - - useEffect(() => { - const currentNodeExecutionsById = keyBy( - nodeExecutionsQuery.data, - 'scopedId', - ); - const prevNodeExecutionsById = clone(nodeExecutionsById); - const newNodeExecutionsById = merge( - prevNodeExecutionsById, - currentNodeExecutionsById, - ); - setCurrentNodeExecutionsById(newNodeExecutionsById); - }, [nodeExecutionsQuery.data]); - - const LoadingComponent = () => { - return ( -
- -
- ); - }; - - const renderTab = tabType => { - return ( - - ); - }; - return ( <> @@ -115,24 +50,16 @@ export const ExecutionNodeViews: React.FC = ({ - +
{tabState.value === tabs.nodes.id && (
)} - - {() => renderTab(tabState.value)} - +
-
+
); diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 8bac79209..33f498dc1 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -4,26 +4,25 @@ import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; import { Workflow } from 'models/Workflow/types'; import * as React from 'react'; import { useQuery, useQueryClient } from 'react-query'; -import { NodeExecution } from 'models/Execution/types'; -import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from '../contextProvider/NodeExecutionDetails'; import { ScaleProvider } from './Timeline/scaleContext'; import { ExecutionTabContent } from './ExecutionTabContent'; interface ExecutionTabProps { tabType: string; - filteredNodeExecutions?: NodeExecution[]; } /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionTab: React.FC = ({ - tabType, - filteredNodeExecutions, -}) => { +export const ExecutionTab: React.FC = ({ tabType }) => { const queryClient = useQueryClient(); const { workflowId } = useNodeExecutionContext(); const workflowQuery = useQuery( makeWorkflowQuery(queryClient, workflowId), ); + const { filteredNodeExecutions } = useNodeExecutionsById(); return ( diff --git a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts index 3e1b45963..250e6c6ab 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts @@ -3,16 +3,13 @@ import { limits } from 'models/AdminEntity/constants'; import { FilterOperation, SortDirection } from 'models/AdminEntity/types'; import { executionSortFields } from 'models/Execution/constants'; import { Execution, NodeExecution } from 'models/Execution/types'; -import { useQueryClient } from 'react-query'; +import { useQueryClient, UseQueryResult } from 'react-query'; import { executionRefreshIntervalMs } from '../constants'; import { makeNodeExecutionListQuery } from '../nodeExecutionQueries'; import { executionIsTerminal, nodeExecutionIsTerminal } from '../utils'; -export function useExecutionNodeViewsState( - execution: Execution, - filter: FilterOperation[] = [], -): { - nodeExecutionsQuery: any; +export interface UseExecutionNodeViewsState { + nodeExecutionsQuery: UseQueryResult; nodeExecutionsRequestConfig: { filter: FilterOperation[]; sort: { @@ -21,7 +18,11 @@ export function useExecutionNodeViewsState( }; limit: number; }; -} { +} +export function useExecutionNodeViewsState( + execution: Execution, + filter: FilterOperation[] = [], +): UseExecutionNodeViewsState { const sort = { key: executionSortFields.createdAt, direction: SortDirection.ASCENDING, diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts new file mode 100644 index 000000000..f907c05d0 --- /dev/null +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -0,0 +1,38 @@ +import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; +import { NodeExecution } from 'models/Execution/types'; + +import { useQueryClient, UseQueryResult } from 'react-query'; +import { executionRefreshIntervalMs } from '../constants'; +import { makeNodeExecutionQueryEnhanced } from '../nodeExecutionQueries'; +import { isParentNode, nodeExecutionIsTerminal } from '../utils'; + +export const useNodeExecutionRow = ( + execution: NodeExecution, + isInView: boolean, + parentNodeCallback: (nodeExecution: NodeExecution) => void, +): { + nodeExecutionRowQuery: UseQueryResult; +} => { + const shouldEnableQuery = () => { + if (!isInView) { + return false; + } + const isTerminal = nodeExecutionIsTerminal(execution); + // const isParent = isParentNode(execution); + return !isTerminal; + // return !isTerminal && isParent; + }; + + const nodeExecutionRowQuery = useConditionalQuery( + { + ...makeNodeExecutionQueryEnhanced(execution.id, useQueryClient()), + onSettled: async nodeExecution => { + await parentNodeCallback(nodeExecution!); + }, + refetchInterval: executionRefreshIntervalMs, + }, + shouldEnableQuery, + ); + + return { nodeExecutionRowQuery }; +}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/utils.ts b/packages/console/src/components/Executions/ExecutionDetails/utils.ts index 7d6c84c7a..61c3ca507 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/utils.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/utils.ts @@ -38,9 +38,7 @@ export function isChildGroupsFetched( scopedId: string, nodeExecutionsById: Dictionary, ): boolean { - return Object.values(nodeExecutionsById).find( - exe => exe?.fromUniqueParentId === scopedId, - ) - ? true - : false; + return Object.values(nodeExecutionsById).some( + v => v?.fromUniqueParentId === scopedId, + ); } diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index 9d0165c7e..cb100d11a 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -1,18 +1,25 @@ +import React, { useContext, useEffect } from 'react'; import classnames from 'classnames'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; -import React, { useContext } from 'react'; import { isExpanded } from 'components/WorkflowGraph/utils'; import { isEqual } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; import { makeStyles } from '@material-ui/core'; +import { LoadingSpinner } from 'components/common/LoadingSpinner'; +import { useInView } from 'react-intersection-observer'; import { selectedClassName, useExecutionTableStyles } from './styles'; import { NodeExecutionColumnDefinition } from './types'; import { DetailsPanelContext } from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; -import { isParentNode } from '../utils'; +import { isParentNode, nodeExecutionIsTerminal } from '../utils'; +import { useNodeExecutionRow } from '../ExecutionDetails/useNodeExecutionRow'; +import { + NodeExecutionsByIdContext, + SetCurrentNodeExecutionsById, +} from '../contexts'; const useStyles = makeStyles(() => ({ namesContainerExpander: { @@ -31,6 +38,11 @@ interface NodeExecutionRowProps { level?: number; style?: React.CSSProperties; node: dNode; + parentNodeCallback: ( + nodeExecution: NodeExecution, + node: dNode, + level: number, + ) => void; onToggle: (id: string, scopeId: string, level: number) => void; } @@ -41,14 +53,11 @@ export const NodeExecutionRow: React.FC = ({ node, style, onToggle, + parentNodeCallback, }) => { const styles = useStyles(); const theme = useTheme(); - const expanderRef = React.useRef(); - const tableStyles = useExecutionTableStyles(); - const { selectedExecution, setSelectedExecution } = - useContext(DetailsPanelContext); const nodeLevel = node?.level ?? 0; @@ -63,15 +72,48 @@ export const NodeExecutionRow: React.FC = ({ )}px`, }; + const { setCurrentNodeExecutionsById } = useContext( + NodeExecutionsByIdContext, + ); + + const expanderRef = React.useRef(); + const { ref, inView } = useInView(); + + const { selectedExecution, setSelectedExecution } = + useContext(DetailsPanelContext); + const { nodeExecutionRowQuery } = useNodeExecutionRow( + nodeExecution, + inView, + newNodeExecution => { + return parentNodeCallback(newNodeExecution, node, nodeLevel); + }, + ); + const selected = selectedExecution ? isEqual(selectedExecution, nodeExecution) : false; + useEffect(() => { + // don't update if still fetching + if (nodeExecutionRowQuery.isFetching || !nodeExecutionRowQuery.data) { + return; + } + + const currentNodeExecutionsById = nodeExecutionRowQuery.data; + setCurrentNodeExecutionsById({ + [nodeExecution.scopedId!]: currentNodeExecutionsById!, + }); + }, [nodeExecutionRowQuery]); + const expanderContent = React.useMemo(() => { - return isParentNode(nodeExecution) ? ( + const isParent = isParentNode(nodeExecution); + const isExpandedVal = isExpanded(node); + return !isParent && !nodeExecutionIsTerminal(nodeExecution) ? ( + + ) : isParent ? ( } - expanded={isExpanded(node)} + expanded={isExpandedVal} onClick={() => { onToggle(node.id, node.scopedId, nodeLevel); }} @@ -79,7 +121,7 @@ export const NodeExecutionRow: React.FC = ({ ) : (
); - }, [node, nodeLevel]); + }, [node, nodeLevel, nodeExecution]); // 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 @@ -95,6 +137,7 @@ export const NodeExecutionRow: React.FC = ({ })} style={style} onClick={onClickRow} + ref={ref} >
diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index af3d4cf08..b18bcf674 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -19,11 +19,12 @@ import { convertToPlainNodes } from '../ExecutionDetails/Timeline/helpers'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionRow } from './NodeExecutionRow'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { fetchChildrenExecutions, searchNode } from '../utils'; +import { fetchChildrenExecutions, isParentNode, searchNode } from '../utils'; interface NodeExecutionsTableProps { initialNodes: dNode[]; filteredNodes?: dNode[]; + shouldUpdate?: boolean; setShouldUpdate: (val: boolean) => void; } @@ -80,14 +81,16 @@ export const NodeExecutionsTable: React.FC = ({ }); const plainNodes = convertToPlainNodes(originalNodes); - const updatedShownNodesMap = plainNodes.map(node => { - const execution = nodeExecutionsById[node.scopedId]; - return { - ...node, - startedAt: execution?.closure.startedAt, - execution, - }; - }); + const updatedShownNodesMap = plainNodes + .map(node => { + const execution = nodeExecutionsById[node.scopedId]; + return { + ...node, + startedAt: execution?.closure.startedAt, + execution, + }; + }) + .filter(n => !!n?.execution); setShowNodes(updatedShownNodesMap); }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); @@ -103,6 +106,27 @@ export const NodeExecutionsTable: React.FC = ({ setOriginalNodes([...originalNodes]); }; + const parentNodeCallback = async ( + nodeExecution: NodeExecution, + node: dNode, + ) => { + if (!isParentNode(nodeExecution)) { + return; + } + + const { scopedId } = node; + await fetchChildrenExecutions( + queryClient, + scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + // pass undefined to setShouldUpdate + undefined, + // force updates + true, + ); + }; + return (
= ({ nodeExecution={nodeExecution} node={node} onToggle={toggleNode} + parentNodeCallback={parentNodeCallback} /> ); }) diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx new file mode 100644 index 000000000..63497f8c1 --- /dev/null +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx @@ -0,0 +1,176 @@ +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'; +import { CompiledWorkflowClosure } from 'models/Workflow/types'; +import { useQueryClient } from 'react-query'; +import { fetchWorkflow } from 'components/Workflow/workflowQueries'; +import { NodeExecutionDetails } from '../../types'; +import { UNKNOWN_DETAILS } from './types'; +import { + createExecutionDetails, + CurrentExecutionDetails, +} from './createExecutionArray'; +import { getTaskThroughExecution } from './getTaskThroughExecution'; + +interface NodeExecutionDetailsState { + getNodeExecutionDetails: ( + nodeExecution?: NodeExecution, + ) => Promise; + workflowId: Identifier; + compiledWorkflowClosure: CompiledWorkflowClosure | null; +} + +const NOT_AVAILABLE = 'NotAvailable'; +/** Use this Context to redefine Provider returns in storybooks */ +export const NodeExecutionDetailsContext = + createContext({ + /** Default values used if ContextProvider wasn't initialized. */ + getNodeExecutionDetails: async () => { + log.error( + 'ERROR: No NodeExecutionDetailsContextProvider was found in parent components.', + ); + return UNKNOWN_DETAILS; + }, + workflowId: { + project: NOT_AVAILABLE, + domain: NOT_AVAILABLE, + name: NOT_AVAILABLE, + version: NOT_AVAILABLE, + }, + compiledWorkflowClosure: null, + }); + +/** Should be used to get NodeExecutionDetails for a specific nodeExecution. */ +export const useNodeExecutionDetails = (nodeExecution?: NodeExecution) => + useContext(NodeExecutionDetailsContext).getNodeExecutionDetails( + nodeExecution, + ); + +/** Could be used to access the whole NodeExecutionDetailsState */ +export const useNodeExecutionContext = (): NodeExecutionDetailsState => + useContext(NodeExecutionDetailsContext); + +interface ProviderProps { + workflowId: Identifier; + children?: React.ReactNode; +} + +/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ +export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { + // workflow Identifier - separated to parameters, to minimize re-render count + // as useEffect doesn't know how to do deep comparison + const { resourceType, project, domain, name, version } = props.workflowId; + + const [executionTree, setExecutionTree] = + useState(null); + const [tasks, setTasks] = useState(new Map()); + const [closure, setClosure] = useState(null); + + const resetState = () => { + setExecutionTree(null); + }; + + const queryClient = useQueryClient(); + const isMounted = useRef(false); + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + useEffect(() => { + let isCurrent = true; + async function fetchData() { + const workflowId: Identifier = { + resourceType, + project, + domain, + name, + version, + }; + const result = await fetchWorkflow(queryClient, workflowId); + if (!result) { + resetState(); + return; + } + const workflow = JSON.parse(JSON.stringify(result)); + const tree = createExecutionDetails(workflow); + if (isCurrent) { + setClosure(workflow.closure?.compiledWorkflow ?? null); + setExecutionTree(tree); + } + } + + fetchData(); + + // This handles the unmount case + return () => { + isCurrent = false; + resetState(); + }; + }, [queryClient, resourceType, project, domain, name, version]); + + const getDynamicTasks = async (nodeExecution: NodeExecution) => { + const taskDetails = await getTaskThroughExecution( + queryClient, + nodeExecution, + ); + + const tasksMap = tasks; + tasksMap.set(nodeExecution.id.nodeId, taskDetails); + if (isMounted.current) { + setTasks(tasksMap); + } + + return taskDetails; + }; + + const getDetails = async ( + nodeExecution?: NodeExecution, + ): Promise => { + if (!executionTree || !nodeExecution) { + return UNKNOWN_DETAILS; + } + + const specId = + nodeExecution.scopedId || + nodeExecution.metadata?.specNodeId || + nodeExecution.id.nodeId; + const nodeDetail = executionTree.nodes.filter(n => n.scopedId === specId); + if (nodeDetail.length === 0) { + let details = tasks.get(nodeExecution.id.nodeId); + if (details) { + // we already have looked for it and found + return details; + } + + // look for specific task by nodeId in current execution + if (nodeExecution.metadata?.isDynamic) { + details = await getDynamicTasks(nodeExecution); + } + return details; + } + + return nodeDetail?.[0] ?? UNKNOWN_DETAILS; + }; + + return ( + + {props.children} + + ); +}; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx new file mode 100644 index 000000000..65f7abc2a --- /dev/null +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx @@ -0,0 +1,146 @@ +import React, { + PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import { NodeExecutionsById } from 'models/Execution/types'; +import { useExecutionNodeViewsState } from 'components/Executions/ExecutionDetails/useExecutionNodeViewsState'; +import { ExecutionFiltersState } from 'components/Executions/filters/useExecutionFiltersState'; +import { + ExecutionContext, + FilteredNodeExecutions, + INodeExecutionsByIdContext, + NodeExecutionsByIdContext, +} from 'components/Executions/contexts'; +import { clone, isEqual, keyBy, merge } from 'lodash'; +import { FilterOperation } from 'models'; +import { WaitForQuery } from 'components/common'; +import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; +import { DataError } from 'components/Errors/DataError'; + +const DEFAULT_FALUE = {}; + +const isPhaseFilter = (appliedFilters: FilterOperation[]) => { + if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { + return true; + } + return false; +}; + +export type NodeExecutionsByIdContextProviderProps = PropsWithChildren<{ + initialNodeExecutionsById?: NodeExecutionsById; + filterState: ExecutionFiltersState; +}>; + +/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ +export const NodeExecutionsByIdContextProvider = ({ + filterState, + initialNodeExecutionsById, + children, +}: NodeExecutionsByIdContextProviderProps) => { + const { execution } = useContext(ExecutionContext); + + const [nodeExecutionsById, setNodeExecutionsById] = + useState(initialNodeExecutionsById ?? DEFAULT_FALUE); + + const [filteredNodeExecutions, setFilteredNodeExecutions] = + useState(); + + // query to get all data to build Graph and Timeline + const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); + // query to get filtered data to narrow down Table outputs + const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = + useExecutionNodeViewsState(execution, filterState.appliedFilters); + + useEffect(() => { + if (nodeExecutionsQuery.isFetching || !nodeExecutionsQuery.data) { + return; + } + const currentNodeExecutionsById = keyBy( + nodeExecutionsQuery.data, + 'scopedId', + ); + const prevNodeExecutionsById = clone(nodeExecutionsById); + const newNodeExecutionsById = merge( + prevNodeExecutionsById, + currentNodeExecutionsById, + ); + setCurrentNodeExecutionsById(newNodeExecutionsById); + }, [nodeExecutionsQuery]); + + useEffect(() => { + if ( + filteredNodeExecutionsQuery.isFetching || + !filteredNodeExecutionsQuery.data + ) { + return; + } + const newFilteredNodeExecutions = isPhaseFilter(filterState.appliedFilters) + ? undefined + : filteredNodeExecutions; + + setFilteredNodeExecutions(newFilteredNodeExecutions); + }, [filteredNodeExecutionsQuery]); + + const setCurrentNodeExecutionsById = useCallback( + (currentNodeExecutionsById: NodeExecutionsById): void => { + setNodeExecutionsById(prev => { + const newNodes = merge({ ...prev }, currentNodeExecutionsById); + if (isEqual(prev, newNodes)) { + return prev; + } + + return newNodes; + }); + }, + [], + ); + + const resetCurrentNodeExecutionsById = useCallback( + (currentNodeExecutionsById?: NodeExecutionsById): void => { + setNodeExecutionsById(currentNodeExecutionsById || DEFAULT_FALUE); + }, + [], + ); + + return ( + + + {() => ( + + {() => children} + + )} + + + ); +}; + +export const useNodeExecutionsById = (): INodeExecutionsByIdContext => { + return useContext(NodeExecutionsByIdContext); +}; + +const LoadingComponent = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx index 63497f8c1..71baaf304 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx @@ -1,176 +1,2 @@ -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'; -import { CompiledWorkflowClosure } from 'models/Workflow/types'; -import { useQueryClient } from 'react-query'; -import { fetchWorkflow } from 'components/Workflow/workflowQueries'; -import { NodeExecutionDetails } from '../../types'; -import { UNKNOWN_DETAILS } from './types'; -import { - createExecutionDetails, - CurrentExecutionDetails, -} from './createExecutionArray'; -import { getTaskThroughExecution } from './getTaskThroughExecution'; - -interface NodeExecutionDetailsState { - getNodeExecutionDetails: ( - nodeExecution?: NodeExecution, - ) => Promise; - workflowId: Identifier; - compiledWorkflowClosure: CompiledWorkflowClosure | null; -} - -const NOT_AVAILABLE = 'NotAvailable'; -/** Use this Context to redefine Provider returns in storybooks */ -export const NodeExecutionDetailsContext = - createContext({ - /** Default values used if ContextProvider wasn't initialized. */ - getNodeExecutionDetails: async () => { - log.error( - 'ERROR: No NodeExecutionDetailsContextProvider was found in parent components.', - ); - return UNKNOWN_DETAILS; - }, - workflowId: { - project: NOT_AVAILABLE, - domain: NOT_AVAILABLE, - name: NOT_AVAILABLE, - version: NOT_AVAILABLE, - }, - compiledWorkflowClosure: null, - }); - -/** Should be used to get NodeExecutionDetails for a specific nodeExecution. */ -export const useNodeExecutionDetails = (nodeExecution?: NodeExecution) => - useContext(NodeExecutionDetailsContext).getNodeExecutionDetails( - nodeExecution, - ); - -/** Could be used to access the whole NodeExecutionDetailsState */ -export const useNodeExecutionContext = (): NodeExecutionDetailsState => - useContext(NodeExecutionDetailsContext); - -interface ProviderProps { - workflowId: Identifier; - children?: React.ReactNode; -} - -/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ -export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { - // workflow Identifier - separated to parameters, to minimize re-render count - // as useEffect doesn't know how to do deep comparison - const { resourceType, project, domain, name, version } = props.workflowId; - - const [executionTree, setExecutionTree] = - useState(null); - const [tasks, setTasks] = useState(new Map()); - const [closure, setClosure] = useState(null); - - const resetState = () => { - setExecutionTree(null); - }; - - const queryClient = useQueryClient(); - const isMounted = useRef(false); - useEffect(() => { - isMounted.current = true; - return () => { - isMounted.current = false; - }; - }, []); - - useEffect(() => { - let isCurrent = true; - async function fetchData() { - const workflowId: Identifier = { - resourceType, - project, - domain, - name, - version, - }; - const result = await fetchWorkflow(queryClient, workflowId); - if (!result) { - resetState(); - return; - } - const workflow = JSON.parse(JSON.stringify(result)); - const tree = createExecutionDetails(workflow); - if (isCurrent) { - setClosure(workflow.closure?.compiledWorkflow ?? null); - setExecutionTree(tree); - } - } - - fetchData(); - - // This handles the unmount case - return () => { - isCurrent = false; - resetState(); - }; - }, [queryClient, resourceType, project, domain, name, version]); - - const getDynamicTasks = async (nodeExecution: NodeExecution) => { - const taskDetails = await getTaskThroughExecution( - queryClient, - nodeExecution, - ); - - const tasksMap = tasks; - tasksMap.set(nodeExecution.id.nodeId, taskDetails); - if (isMounted.current) { - setTasks(tasksMap); - } - - return taskDetails; - }; - - const getDetails = async ( - nodeExecution?: NodeExecution, - ): Promise => { - if (!executionTree || !nodeExecution) { - return UNKNOWN_DETAILS; - } - - const specId = - nodeExecution.scopedId || - nodeExecution.metadata?.specNodeId || - nodeExecution.id.nodeId; - const nodeDetail = executionTree.nodes.filter(n => n.scopedId === specId); - if (nodeDetail.length === 0) { - let details = tasks.get(nodeExecution.id.nodeId); - if (details) { - // we already have looked for it and found - return details; - } - - // look for specific task by nodeId in current execution - if (nodeExecution.metadata?.isDynamic) { - details = await getDynamicTasks(nodeExecution); - } - return details; - } - - return nodeDetail?.[0] ?? UNKNOWN_DETAILS; - }; - - return ( - - {props.children} - - ); -}; +export * from './NodeExecutionDetailsContextProvider'; +export * from './NodeExecutionsByIdContextProvider'; diff --git a/packages/console/src/components/Executions/contexts.ts b/packages/console/src/components/Executions/contexts.ts index 0f592e9cf..41cfb2b42 100644 --- a/packages/console/src/components/Executions/contexts.ts +++ b/packages/console/src/components/Executions/contexts.ts @@ -14,15 +14,25 @@ export const ExecutionContext = createContext( {} as ExecutionContextData, ); +export type NodeExecutionsById = Dictionary; +export type FilteredNodeExecutions = WorkflowNodeExecution[] | undefined; +export type SetCurrentNodeExecutionsById = ( + currentNodeExecutionsById: Dictionary, +) => void; + +export type ResetCurrentNodeExecutionsById = ( + currentNodeExecutionsById?: Dictionary, +) => void; export interface INodeExecutionsByIdContext { - nodeExecutionsById: Dictionary; - setCurrentNodeExecutionsById: ( - currentNodeExecutionsById: Dictionary, - ) => void; + nodeExecutionsById: NodeExecutionsById; + filteredNodeExecutions?: FilteredNodeExecutions; + setCurrentNodeExecutionsById: SetCurrentNodeExecutionsById; + resetCurrentNodeExecutionsById: ResetCurrentNodeExecutionsById; } export const NodeExecutionsByIdContext = createContext({ nodeExecutionsById: {}, setCurrentNodeExecutionsById: () => {}, + resetCurrentNodeExecutionsById: () => {}, }); diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index f7ef95932..2c22e8dad 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -50,6 +50,27 @@ export function makeNodeExecutionQuery( }; } +/** A query for fetching a single `NodeExecution` by id. */ +export function makeNodeExecutionQueryEnhanced( + id: NodeExecutionIdentifier, + queryClient: QueryClient, +): QueryInput { + return { + queryKey: [QueryType.NodeExecution, id], + queryFn: async () => { + const data = await getNodeExecution(id); + if (data.metadata?.specNodeId) { + data.scopedId = retriesToZero(data.metadata.specNodeId); + } else { + data.scopedId = retriesToZero(data.id.nodeId); + } + cacheNodeExecutions(queryClient, [data]); + + return data; + }, + }; +} + export function makeListTaskExecutionsQuery( id: NodeExecutionIdentifier, ): QueryInput> { diff --git a/packages/console/src/components/Executions/useNodeExecutionsById.ts b/packages/console/src/components/Executions/useNodeExecutionsById.ts deleted file mode 100644 index 0ebec211a..000000000 --- a/packages/console/src/components/Executions/useNodeExecutionsById.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 c86f4d48e..d9fb8cd65 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -33,7 +33,10 @@ import { taskTypeToNodeExecutionDisplayType, workflowExecutionPhaseConstants, } from './constants'; -import { WorkflowNodeExecution } from './contexts'; +import { + SetCurrentNodeExecutionsById, + WorkflowNodeExecution, +} from './contexts'; import { isChildGroupsFetched } from './ExecutionDetails/utils'; import { fetchChildNodeExecutionGroups } from './nodeExecutionQueries'; import { @@ -247,15 +250,15 @@ export async function fetchChildrenExecutions( queryClient: QueryClient, scopedId: string, nodeExecutionsById: Dictionary, - setCurrentNodeExecutionsById: ( - currentNodeExecutionsById: Dictionary, - ) => void, + setCurrentNodeExecutionsById: SetCurrentNodeExecutionsById, setShouldUpdate?: (val: boolean) => void, + skipCache = false, ) { - if (!isChildGroupsFetched(scopedId, nodeExecutionsById)) { + const nodeExecutionsByIdAdapted = skipCache ? {} : nodeExecutionsById; + if (!isChildGroupsFetched(scopedId, nodeExecutionsByIdAdapted)) { const childGroups = await fetchChildNodeExecutionGroups( queryClient, - nodeExecutionsById[scopedId], + nodeExecutionsByIdAdapted[scopedId], {}, ); @@ -269,7 +272,7 @@ export async function fetchChildrenExecutions( if (childGroupsExecutionsById) { const prevNodeExecutionsById = clone(nodeExecutionsById); const currentNodeExecutionsById = merge( - nodeExecutionsById, + nodeExecutionsByIdAdapted, childGroupsExecutionsById, ); if ( From 8ddaa25ea29e7f500adebbefee23b36482180ca7 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 30 Mar 2023 22:42:53 -0700 Subject: [PATCH 02/25] chore: progress Signed-off-by: Carina Ursu --- .../ExecutionDetailsActions.tsx | 7 +-- .../ExecutionDetails/ExecutionTabContent.tsx | 10 +++- .../ExecutionDetails/useNodeExecutionRow.ts | 15 ++--- .../Executions/Tables/NodeExecutionRow.tsx | 33 ++++------ .../Executions/Tables/NodeExecutionsTable.tsx | 35 +---------- .../Executions/nodeExecutionQueries.ts | 60 +++++++++++++++---- .../src/components/Executions/utils.ts | 4 +- packages/console/src/components/data/types.ts | 1 + 8 files changed, 81 insertions(+), 84 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx index 1a66af50c..330c33736 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx @@ -1,5 +1,5 @@ +import React, { useContext, useEffect, useState } from 'react'; import { Button, Dialog, IconButton } from '@material-ui/core'; -import * as React from 'react'; import { ResourceIdentifier, Identifier } from 'models/Common/types'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { getTask } from 'models/Task/api'; @@ -14,7 +14,6 @@ import { TaskInitialLaunchParameters } from 'components/Launch/LaunchForm/types' import { NodeExecutionPhase } from 'models/Execution/enums'; import { extractCompiledNodes } from 'components/hooks/utils'; import Close from '@material-ui/icons/Close'; -import { useEffect, useState } from 'react'; import classnames from 'classnames'; import { NodeExecutionDetails } from '../types'; import t from './strings'; @@ -63,7 +62,6 @@ const useStyles = makeStyles((theme: Theme) => { }, }; }); - interface ExecutionDetailsActionsProps { className?: string; details?: NodeExecutionDetails; @@ -91,12 +89,11 @@ export const ExecutionDetailsActions = ({ const [initialParameters, setInitialParameters] = useState< TaskInitialLaunchParameters | undefined >(undefined); - + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); const executionData = useNodeExecutionData(nodeExecutionId); const execution = useNodeExecution(nodeExecutionId); const { compiledWorkflowClosure } = useNodeExecutionContext(); const id = details?.taskTemplate?.id; - const { nodeExecutionsById } = React.useContext(NodeExecutionsByIdContext); const compiledNode = extractCompiledNodes(compiledWorkflowClosure).find( node => diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx index 60333ae5b..916f0ab65 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx @@ -76,7 +76,7 @@ export const ExecutionTabContent: React.FC = ({ const [dynamicParents, setDynamicParents] = useState( checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap), ); - const { data: dynamicWorkflows, refetch } = useQuery( + const { data: dynamicWorkflows } = useQuery( makeNodeExecutionDynamicWorkflowQuery(dynamicParents), ); @@ -96,8 +96,12 @@ export const ExecutionTabContent: React.FC = ({ nodeExecutionsById, staticExecutionIdsMap, ); - setDynamicParents(newDynamicParents); - refetch(); + setDynamicParents(prev => { + if (isEqual(prev, newDynamicParents)) { + return prev; + } + return newDynamicParents; + }); setShouldUpdate(false); } }, [shouldUpdate]); diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index f907c05d0..6368c4630 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -4,31 +4,28 @@ import { NodeExecution } from 'models/Execution/types'; import { useQueryClient, UseQueryResult } from 'react-query'; import { executionRefreshIntervalMs } from '../constants'; import { makeNodeExecutionQueryEnhanced } from '../nodeExecutionQueries'; -import { isParentNode, nodeExecutionIsTerminal } from '../utils'; +import { nodeExecutionIsTerminal } from '../utils'; export const useNodeExecutionRow = ( execution: NodeExecution, isInView: boolean, - parentNodeCallback: (nodeExecution: NodeExecution) => void, ): { - nodeExecutionRowQuery: UseQueryResult; + nodeExecutionRowQuery: UseQueryResult; } => { const shouldEnableQuery = () => { if (!isInView) { return false; } + + // No need for isParent check in here, the conditionalQuery + // will gate the fetchChildExecutions call with a isParent check. const isTerminal = nodeExecutionIsTerminal(execution); - // const isParent = isParentNode(execution); return !isTerminal; - // return !isTerminal && isParent; }; const nodeExecutionRowQuery = useConditionalQuery( { - ...makeNodeExecutionQueryEnhanced(execution.id, useQueryClient()), - onSettled: async nodeExecution => { - await parentNodeCallback(nodeExecution!); - }, + ...makeNodeExecutionQueryEnhanced(execution, useQueryClient()), refetchInterval: executionRefreshIntervalMs, }, shouldEnableQuery, diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index cb100d11a..7433fe0f2 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -4,7 +4,7 @@ import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { isExpanded } from 'components/WorkflowGraph/utils'; -import { isEqual } from 'lodash'; +import { isEqual, keyBy } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; import { makeStyles } from '@material-ui/core'; import { LoadingSpinner } from 'components/common/LoadingSpinner'; @@ -16,10 +16,7 @@ import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; import { isParentNode, nodeExecutionIsTerminal } from '../utils'; import { useNodeExecutionRow } from '../ExecutionDetails/useNodeExecutionRow'; -import { - NodeExecutionsByIdContext, - SetCurrentNodeExecutionsById, -} from '../contexts'; +import { NodeExecutionsByIdContext } from '../contexts'; const useStyles = makeStyles(() => ({ namesContainerExpander: { @@ -38,11 +35,7 @@ interface NodeExecutionRowProps { level?: number; style?: React.CSSProperties; node: dNode; - parentNodeCallback: ( - nodeExecution: NodeExecution, - node: dNode, - level: number, - ) => void; + setShouldUpdate: (val: boolean) => void; onToggle: (id: string, scopeId: string, level: number) => void; } @@ -52,8 +45,8 @@ export const NodeExecutionRow: React.FC = ({ nodeExecution, node, style, + setShouldUpdate, onToggle, - parentNodeCallback, }) => { const styles = useStyles(); const theme = useTheme(); @@ -81,13 +74,7 @@ export const NodeExecutionRow: React.FC = ({ const { selectedExecution, setSelectedExecution } = useContext(DetailsPanelContext); - const { nodeExecutionRowQuery } = useNodeExecutionRow( - nodeExecution, - inView, - newNodeExecution => { - return parentNodeCallback(newNodeExecution, node, nodeLevel); - }, - ); + const { nodeExecutionRowQuery } = useNodeExecutionRow(nodeExecution, inView); const selected = selectedExecution ? isEqual(selectedExecution, nodeExecution) @@ -99,10 +86,12 @@ export const NodeExecutionRow: React.FC = ({ return; } - const currentNodeExecutionsById = nodeExecutionRowQuery.data; - setCurrentNodeExecutionsById({ - [nodeExecution.scopedId!]: currentNodeExecutionsById!, - }); + const currentNodeExecutions = nodeExecutionRowQuery.data; + const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'id.nodeId'); + // Forces ExecutionTabContent to recompute dynamic parents + // TODO: move logic to provider. + setShouldUpdate(true); + setCurrentNodeExecutionsById(currentNodeExecutionsById); }, [nodeExecutionRowQuery]); const expanderContent = React.useMemo(() => { diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index b18bcf674..fde57b129 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -47,10 +47,7 @@ export const NodeExecutionsTable: React.FC = ({ }) => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); - const queryClient = useQueryClient(); - const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( - NodeExecutionsByIdContext, - ); + const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); const { appliedFilters } = useNodeExecutionFiltersState(); const [originalNodes, setOriginalNodes] = useState( appliedFilters.length > 0 && filteredNodes ? filteredNodes : initialNodes, @@ -95,38 +92,10 @@ export const NodeExecutionsTable: React.FC = ({ }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); const toggleNode = async (id: string, scopedId: string, level: number) => { - await fetchChildrenExecutions( - queryClient, - scopedId, - nodeExecutionsById, - setCurrentNodeExecutionsById, - setShouldUpdate, - ); searchNode(originalNodes, 0, id, scopedId, level); setOriginalNodes([...originalNodes]); }; - const parentNodeCallback = async ( - nodeExecution: NodeExecution, - node: dNode, - ) => { - if (!isParentNode(nodeExecution)) { - return; - } - - const { scopedId } = node; - await fetchChildrenExecutions( - queryClient, - scopedId, - nodeExecutionsById, - setCurrentNodeExecutionsById, - // pass undefined to setShouldUpdate - undefined, - // force updates - true, - ); - }; - return (
= ({ nodeExecution={nodeExecution} node={node} onToggle={toggleNode} - parentNodeCallback={parentNodeCallback} + setShouldUpdate={setShouldUpdate} /> ); }) diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 2c22e8dad..a9d4b1b6d 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -52,21 +52,61 @@ export function makeNodeExecutionQuery( /** A query for fetching a single `NodeExecution` by id. */ export function makeNodeExecutionQueryEnhanced( - id: NodeExecutionIdentifier, + nodeExecution: NodeExecution, queryClient: QueryClient, -): QueryInput { +): QueryInput { + const { id } = nodeExecution; + return { - queryKey: [QueryType.NodeExecution, id], + queryKey: [QueryType.NodeExecutionAndChilList, id], queryFn: async () => { - const data = await getNodeExecution(id); - if (data.metadata?.specNodeId) { - data.scopedId = retriesToZero(data.metadata.specNodeId); - } else { - data.scopedId = retriesToZero(data.id.nodeId); + // const parentNode = await getNodeExecution(id); + // if (parentNode.metadata?.specNodeId) { + // parentNode.scopedId = retriesToZero(parentNode.metadata.specNodeId); + // } else { + // parentNode.scopedId = retriesToZero(parentNode.id.nodeId); + // } + + const parentScopeId = + nodeExecution.scopedId ?? nodeExecution.metadata?.specNodeId; + + nodeExecution.scopedId = parentScopeId; + const parentNodeID = nodeExecution.id.nodeId; + const isParent = isParentNode(nodeExecution); + + let childExecutions: NodeExecution[] = []; + + if (isParent) { + const rawChildExecutions = await fetchNodeExecutionList( + queryClient, + id.executionId, + { + params: { + [nodeExecutionQueryParams.parentNodeId]: parentNodeID, + }, + }, + ); + + childExecutions = rawChildExecutions?.map(childExecution => { + // TODO @jason: why are there two different wayt of generating these? + if (parentScopeId !== undefined) { + const scopedId = childExecution.metadata?.specNodeId + ? retriesToZero(childExecution?.metadata?.specNodeId) + : retriesToZero(childExecution?.id?.nodeId); + childExecution['scopedId'] = `${parentScopeId}-0-${scopedId}`; + } else { + childExecution['scopedId'] = childExecution.metadata?.specNodeId; + } + // childExecution['scopedId'] = ; + childExecution['fromUniqueParentId'] = parentNodeID; + + return childExecution; + }); } - cacheNodeExecutions(queryClient, [data]); - return data; + const finalExecutions = [nodeExecution, ...childExecutions]; + cacheNodeExecutions(queryClient, finalExecutions); + return finalExecutions; }, }; } diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index d9fb8cd65..5416013ec 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -21,7 +21,6 @@ import { BaseExecutionClosure, Execution, NodeExecution, - NodeExecutionIdentifier, TaskExecution, } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; @@ -254,11 +253,12 @@ export async function fetchChildrenExecutions( setShouldUpdate?: (val: boolean) => void, skipCache = false, ) { + const cachedParentNode = nodeExecutionsById[scopedId]; const nodeExecutionsByIdAdapted = skipCache ? {} : nodeExecutionsById; if (!isChildGroupsFetched(scopedId, nodeExecutionsByIdAdapted)) { const childGroups = await fetchChildNodeExecutionGroups( queryClient, - nodeExecutionsByIdAdapted[scopedId], + cachedParentNode, {}, ); diff --git a/packages/console/src/components/data/types.ts b/packages/console/src/components/data/types.ts index b402ab734..9dfeb3bcf 100644 --- a/packages/console/src/components/data/types.ts +++ b/packages/console/src/components/data/types.ts @@ -8,6 +8,7 @@ export enum QueryType { DynamicWorkflowFromNodeExecution = 'DynamicWorkflowFromNodeExecution', NodeExecution = 'nodeExecution', NodeExecutionList = 'nodeExecutionList', + NodeExecutionAndChilList = 'nodeExecutionAndChilcList', NodeExecutionChildList = 'nodeExecutionChildList', NodeExecutionTreeList = 'nodeExecutionTreeList', TaskExecution = 'taskExecution', From 95b159741168283dd526698842897d3c2f6fa920 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Fri, 31 Mar 2023 00:06:23 -0700 Subject: [PATCH 03/25] chore: nodes not loading fix Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionNodeViews.tsx | 14 +++++++++++--- .../Executions/ExecutionDetails/ExecutionTab.tsx | 10 +++++++++- .../ExecutionDetails/ExecutionTabContent.tsx | 14 ++++++++------ .../Executions/Tables/NodeExecutionRow.tsx | 2 +- .../NodeExecutionsByIdContextProvider.tsx | 3 +++ .../components/Executions/nodeExecutionQueries.ts | 13 +++++-------- .../ReactFlow/ReactFlowGraphComponent.tsx | 2 +- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index afc5e7111..e9641c57b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { useTabState } from 'components/hooks/useTabState'; @@ -37,6 +37,7 @@ export const ExecutionNodeViews: React.FC = () => { const filterState = useNodeExecutionFiltersState(); const tabState = useTabState(tabs, defaultTab); const { execution } = useContext(ExecutionContext); + const [shouldUpdate, setShouldUpdate] = useState(false); const { closure: { workflowId }, @@ -50,14 +51,21 @@ export const ExecutionNodeViews: React.FC = () => { - +
{tabState.value === tabs.nodes.id && (
)} - +
diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 33f498dc1..ed736c3c7 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -13,10 +13,16 @@ import { ExecutionTabContent } from './ExecutionTabContent'; interface ExecutionTabProps { tabType: string; + setShouldUpdate: (boolean) => void; + shouldUpdate: boolean; } /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionTab: React.FC = ({ tabType }) => { +export const ExecutionTab: React.FC = ({ + tabType, + setShouldUpdate, + shouldUpdate, +}) => { const queryClient = useQueryClient(); const { workflowId } = useNodeExecutionContext(); const workflowQuery = useQuery( @@ -31,6 +37,8 @@ export const ExecutionTab: React.FC = ({ tabType }) => { )} diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx index 916f0ab65..d52290efa 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx @@ -28,11 +28,6 @@ import { DetailsPanelContext } from './DetailsPanelContext'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { nodeExecutionPhaseConstants } from '../constants'; -interface ExecutionTabContentProps { - tabType: string; - filteredNodeExecutions?: NodeExecution[]; -} - const useStyles = makeStyles(() => ({ wrapper: { display: 'flex', @@ -62,9 +57,17 @@ const executionMatchesPhaseFilter = ( return false; }; +interface ExecutionTabContentProps { + tabType: string; + filteredNodeExecutions?: NodeExecution[]; + setShouldUpdate: (boolean) => void; + shouldUpdate: boolean; +} export const ExecutionTabContent: React.FC = ({ tabType, filteredNodeExecutions, + setShouldUpdate, + shouldUpdate, }) => { const styles = useStyles(); const { compiledWorkflowClosure } = useNodeExecutionContext(); @@ -88,7 +91,6 @@ export const ExecutionTabContent: React.FC = ({ const [mergedDag, setMergedDag] = useState(null); const [filters, setFilters] = useState(appliedFilters); const [isFiltersChanged, setIsFiltersChanged] = useState(false); - const [shouldUpdate, setShouldUpdate] = useState(false); useEffect(() => { if (shouldUpdate) { diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index 7433fe0f2..d69987642 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -87,7 +87,7 @@ export const NodeExecutionRow: React.FC = ({ } const currentNodeExecutions = nodeExecutionRowQuery.data; - const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'id.nodeId'); + const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'scopedId'); // Forces ExecutionTabContent to recompute dynamic parents // TODO: move logic to provider. setShouldUpdate(true); diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx index 65f7abc2a..8e7cdacf8 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx @@ -32,12 +32,14 @@ const isPhaseFilter = (appliedFilters: FilterOperation[]) => { export type NodeExecutionsByIdContextProviderProps = PropsWithChildren<{ initialNodeExecutionsById?: NodeExecutionsById; filterState: ExecutionFiltersState; + setShouldUpdate: (boolean) => void; }>; /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ export const NodeExecutionsByIdContextProvider = ({ filterState, initialNodeExecutionsById, + setShouldUpdate, children, }: NodeExecutionsByIdContextProviderProps) => { const { execution } = useContext(ExecutionContext); @@ -67,6 +69,7 @@ export const NodeExecutionsByIdContextProvider = ({ prevNodeExecutionsById, currentNodeExecutionsById, ); + setShouldUpdate(true); setCurrentNodeExecutionsById(newNodeExecutionsById); }, [nodeExecutionsQuery]); diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index a9d4b1b6d..9f3a9dd4b 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -89,14 +89,11 @@ export function makeNodeExecutionQueryEnhanced( childExecutions = rawChildExecutions?.map(childExecution => { // TODO @jason: why are there two different wayt of generating these? - if (parentScopeId !== undefined) { - const scopedId = childExecution.metadata?.specNodeId - ? retriesToZero(childExecution?.metadata?.specNodeId) - : retriesToZero(childExecution?.id?.nodeId); - childExecution['scopedId'] = `${parentScopeId}-0-${scopedId}`; - } else { - childExecution['scopedId'] = childExecution.metadata?.specNodeId; - } + const scopedId = childExecution.metadata?.specNodeId + ? retriesToZero(childExecution?.metadata?.specNodeId) + : retriesToZero(childExecution?.id?.nodeId); + childExecution['scopedId'] = `${parentScopeId}-0-${scopedId}`; + // childExecution['scopedId'] = ; childExecution['fromUniqueParentId'] = parentNodeID; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index e54e9de98..0a7753a7c 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -106,7 +106,7 @@ export const ReactFlowGraphComponent = ({ baseNodeExecutions.map(async baseNodeExecution => { if ( !baseNodeExecution || - nodeExecutionsById[baseNodeExecution.scopedId].tasksFetched + nodeExecutionsById?.[baseNodeExecution.scopedId]?.tasksFetched ) { return; } From 481f23572ab287dd8bdbe08220398c5269b6b337 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Fri, 31 Mar 2023 09:43:17 -0700 Subject: [PATCH 04/25] chore: fix Signed-off-by: Carina Ursu --- .../Executions/Tables/NodeExecutionsTable.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index fde57b129..275154d85 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -78,16 +78,14 @@ export const NodeExecutionsTable: React.FC = ({ }); const plainNodes = convertToPlainNodes(originalNodes); - const updatedShownNodesMap = plainNodes - .map(node => { - const execution = nodeExecutionsById[node.scopedId]; - return { - ...node, - startedAt: execution?.closure.startedAt, - execution, - }; - }) - .filter(n => !!n?.execution); + const updatedShownNodesMap = plainNodes.map(node => { + const execution = nodeExecutionsById[node.scopedId]; + return { + ...node, + startedAt: execution?.closure.startedAt, + execution, + }; + }); setShowNodes(updatedShownNodesMap); }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); From e31dc029fd30e7b5185fafb0bbd8913b794832b0 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 3 Apr 2023 13:26:45 -0700 Subject: [PATCH 05/25] chore: fixes Signed-off-by: Carina Ursu --- .../src/components/Executions/CacheStatus.tsx | 9 +- .../ExecutionDetailsActions.tsx | 8 +- .../ExecutionDetails/ExecutionNodeViews.tsx | 72 ++++++++---- .../ExecutionDetails/ExecutionTab.tsx | 10 +- .../ExecutionDetails/ExecutionTabContent.tsx | 63 ++++++++-- .../NodeExecutionDetailsPanelContent.tsx | 2 +- .../TaskExecutionNode.tsx | 3 +- .../Timeline/NodeExecutionName.tsx | 12 +- .../ExecutionDetails/Timeline/TaskNames.tsx | 6 +- .../ExecutionDetails/Timeline/helpers.ts | 6 +- .../useExecutionNodeViewsState.ts | 12 +- .../ExecutionDetails/useNodeExecutionRow.ts | 20 +--- .../Executions/NodeExecutionCacheStatus.tsx | 5 +- .../Tables/NodeExecutionActions.tsx | 8 +- .../Executions/Tables/NodeExecutionRow.tsx | 109 +++++++++++++++--- .../Executions/Tables/NodeExecutionsTable.tsx | 44 ++++--- .../Executions/Tables/RowExpander.tsx | 4 +- .../Tables/nodeExecutionColumns.tsx | 47 +++++--- .../components/Executions/Tables/styles.ts | 28 +++++ .../src/components/Executions/Tables/types.ts | 1 + .../NodeExecutionsByIdContextProvider.tsx | 77 +++++-------- .../src/components/Executions/contexts.ts | 16 ++- .../Executions/nodeExecutionQueries.ts | 2 +- .../Launch/LaunchForm/ResumeSignalForm.tsx | 3 +- .../MapTaskExecutionsList/TaskNameList.tsx | 14 ++- .../ReactFlow/PausedTasksComponent.tsx | 7 +- .../ReactFlow/customNodeComponents.tsx | 7 +- packages/console/src/models/Graph/types.ts | 1 + 28 files changed, 412 insertions(+), 184 deletions(-) diff --git a/packages/console/src/components/Executions/CacheStatus.tsx b/packages/console/src/components/Executions/CacheStatus.tsx index 2772c7054..2a6a43823 100644 --- a/packages/console/src/components/Executions/CacheStatus.tsx +++ b/packages/console/src/components/Executions/CacheStatus.tsx @@ -82,6 +82,7 @@ export interface CacheStatusProps { variant?: 'normal' | 'iconOnly'; sourceTaskExecutionId?: TaskExecutionIdentifier; iconStyles?: React.CSSProperties; + className?: string; } export const CacheStatus: React.FC = ({ @@ -89,6 +90,7 @@ export const CacheStatus: React.FC = ({ sourceTaskExecutionId, variant = 'normal', iconStyles, + className, }) => { const commonStyles = useCommonStyles(); const styles = useStyles(); @@ -100,11 +102,12 @@ export const CacheStatus: React.FC = ({ const message = cacheStatusMessages[cacheStatus] || unknownCacheStatusString; return variant === 'iconOnly' ? ( - + = ({ ) : ( <> @@ -122,6 +125,7 @@ export const CacheStatus: React.FC = ({ className={classnames( commonStyles.iconSecondary, commonStyles.iconLeft, + className, )} /> {message} @@ -131,6 +135,7 @@ export const CacheStatus: React.FC = ({ className={classnames( commonStyles.primaryLink, styles.sourceExecutionLink, + className, )} to={Routes.ExecutionDetails.makeUrl( sourceTaskExecutionId.nodeExecutionId.executionId, diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx index 330c33736..fd19094d9 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx @@ -18,8 +18,10 @@ import classnames from 'classnames'; import { NodeExecutionDetails } from '../types'; import t from './strings'; import { ExecutionNodeDeck } from './ExecutionNodeDeck'; -import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from '../contexts'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from '../contextProvider/NodeExecutionDetails'; const useStyles = makeStyles((theme: Theme) => { return { @@ -89,7 +91,7 @@ export const ExecutionDetailsActions = ({ const [initialParameters, setInitialParameters] = useState< TaskInitialLaunchParameters | undefined >(undefined); - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const executionData = useNodeExecutionData(nodeExecutionId); const execution = useNodeExecution(nodeExecutionId); const { compiledWorkflowClosure } = useNodeExecutionContext(); diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index e9641c57b..70e802ef5 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -1,17 +1,24 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { useTabState } from 'components/hooks/useTabState'; import { secondaryBackgroundColor } from 'components/Theme/constants'; +import { WaitForQuery } from 'components/common'; +import { DataError } from 'components/Errors/DataError'; +import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { NodeExecutionDetailsContextProvider, NodeExecutionsByIdContextProvider, } from '../contextProvider/NodeExecutionDetails'; import { ExecutionContext } from '../contexts'; import { ExecutionFilters } from '../ExecutionFilters'; -import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; +import { + ExecutionFiltersState, + useNodeExecutionFiltersState, +} from '../filters/useExecutionFiltersState'; import { tabs } from './constants'; import { ExecutionTab } from './ExecutionTab'; +import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; const useStyles = makeStyles((theme: Theme) => ({ filters: { @@ -37,12 +44,17 @@ export const ExecutionNodeViews: React.FC = () => { const filterState = useNodeExecutionFiltersState(); const tabState = useTabState(tabs, defaultTab); const { execution } = useContext(ExecutionContext); - const [shouldUpdate, setShouldUpdate] = useState(false); const { closure: { workflowId }, } = execution; + // query to get all data to build Graph and Timeline + const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); + // query to get filtered data to narrow down Table outputs + const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = + useExecutionNodeViewsState(execution, filterState?.appliedFilters); + return ( <> @@ -50,25 +62,41 @@ export const ExecutionNodeViews: React.FC = () => { - - -
- {tabState.value === tabs.nodes.id && ( -
- -
- )} - -
-
-
+ + {filterState ? ( + + +
+ {tabState.value === tabs.nodes.id && ( +
+ +
+ )} + + {() => } + +
+
+
+ ) : ( + + )} ); }; + +const LoadingComponent = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index ed736c3c7..1aaaa7c0b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -13,21 +13,17 @@ import { ExecutionTabContent } from './ExecutionTabContent'; interface ExecutionTabProps { tabType: string; - setShouldUpdate: (boolean) => void; - shouldUpdate: boolean; } /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionTab: React.FC = ({ - tabType, - setShouldUpdate, - shouldUpdate, -}) => { +export const ExecutionTab: React.FC = ({ tabType }) => { const queryClient = useQueryClient(); const { workflowId } = useNodeExecutionContext(); const workflowQuery = useQuery( makeWorkflowQuery(queryClient, workflowId), ); + + const { setShouldUpdate, shouldUpdate } = useNodeExecutionsById(); const { filteredNodeExecutions } = useNodeExecutionsById(); return ( diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx index d52290efa..296990831 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx @@ -3,9 +3,13 @@ import { DetailsPanel } from 'components/common/DetailsPanel'; import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; import { TaskExecutionPhase } from 'models/Execution/enums'; -import { NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; +import { + NodeExecution, + NodeExecutionIdentifier, + NodeExecutionsById, +} from 'models/Execution/types'; import { startNodeId, endNodeId } from 'models/Node/constants'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { checkForDynamicExecutions } from 'components/common/utils'; import { dNode } from 'models/Graph/types'; @@ -15,9 +19,11 @@ import { FilterOperationName, FilterOperationValueList, } from 'models/AdminEntity/types'; -import { isEqual } from 'lodash'; -import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from '../contexts'; +import { cloneDeep, isEqual } from 'lodash'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { tabs } from './constants'; import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; @@ -57,6 +63,39 @@ const executionMatchesPhaseFilter = ( return false; }; +const filterNodes = ( + initialNodes: dNode[], + nodeExecutionsById: NodeExecutionsById, + appliedFilters: FilterOperation[], +) => { + if (!initialNodes?.length) { + return []; + } + + let initialClone = cloneDeep(initialNodes); + + initialClone.forEach(n => { + n.nodes = filterNodes(n.nodes, nodeExecutionsById, appliedFilters); + }); + + initialClone = initialClone.filter(node => { + const hasFilteredChildren = node.nodes?.length; + const shouldBeIncluded = executionMatchesPhaseFilter( + nodeExecutionsById[node.scopedId], + appliedFilters[0], + ); + const result = hasFilteredChildren || shouldBeIncluded; + + if (hasFilteredChildren && !shouldBeIncluded) { + node.grayedOut = true; + } + + return result; + }); + + return initialClone; +}; + interface ExecutionTabContentProps { tabType: string; filteredNodeExecutions?: NodeExecution[]; @@ -72,7 +111,7 @@ export const ExecutionTabContent: React.FC = ({ const styles = useStyles(); const { compiledWorkflowClosure } = useNodeExecutionContext(); const { appliedFilters } = useNodeExecutionFiltersState(); - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const { staticExecutionIdsMap } = compiledWorkflowClosure ? transformerWorkflowToDag(compiledWorkflowClosure) : { staticExecutionIdsMap: {} }; @@ -102,6 +141,7 @@ export const ExecutionTabContent: React.FC = ({ if (isEqual(prev, newDynamicParents)) { return prev; } + return newDynamicParents; }); setShouldUpdate(false); @@ -166,12 +206,13 @@ export const ExecutionTabContent: React.FC = ({ // if filter was apllied, but filteredNodeExecutions is empty, we only appliied Phase filter, // and need to clear out items manually if (!filteredNodeExecutions) { - const filteredNodes = initialNodes.filter(node => - executionMatchesPhaseFilter( - nodeExecutionsById[node.scopedId], - appliedFilters[0], - ), + // top level + const filteredNodes = filterNodes( + initialNodes, + nodeExecutionsById, + appliedFilters, ); + setInitialFilteredNodes(filteredNodes); } else { const filteredNodes = initialNodes.filter((node: dNode) => diff --git a/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index 93a0d0048..a6f23cb7d 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -325,7 +325,7 @@ export const NodeExecutionDetailsPanelContent: React.FC< if (nodeExecution) { if ( nodeExecution.scopedId && - !nodeExecutionsById[nodeExecution.scopedId].tasksFetched + !nodeExecutionsById?.[nodeExecution.scopedId]?.tasksFetched ) fetchTasksData(nodeExecution, queryClient); } else { diff --git a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx index 620d58a6e..14851c26a 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx @@ -1,3 +1,4 @@ +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { getNodeExecutionPhaseConstants } from 'components/Executions/utils'; import { NodeRendererProps, Point } from 'components/flytegraph/types'; import { TaskNodeRenderer } from 'components/WorkflowGraph/TaskNodeRenderer'; @@ -14,7 +15,7 @@ export const TaskExecutionNode: React.FC< NodeRendererProps > = props => { const { node, config, selected } = props; - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const nodeExecution = nodeExecutionsById[node.id]; const phase = nodeExecution diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx index 4db08a68a..4adae5236 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx @@ -1,5 +1,6 @@ import { makeStyles, Theme } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; +import classNames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { SelectNodeExecutionLink } from 'components/Executions/Tables/SelectNodeExecutionLink'; @@ -13,6 +14,7 @@ interface NodeExecutionTimelineNameData { name: string; templateName?: string; execution?: NodeExecution; + className?: string; } const useStyles = makeStyles((_theme: Theme) => ({ @@ -31,6 +33,7 @@ export const NodeExecutionName: React.FC = ({ name, templateName, execution, + className, }) => { const commonStyles = useCommonStyles(); const styles = useStyles(); @@ -67,12 +70,15 @@ export const NodeExecutionName: React.FC = ({ <> {isSelected || execution.closure.phase === NodeExecutionPhase.UNDEFINED ? ( - + {truncatedName} ) : ( = ({ {templateName} diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx index dcb94c60a..82989ec31 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React from 'react'; import { IconButton, makeStyles, Theme, Tooltip } from '@material-ui/core'; import { RowExpander } from 'components/Executions/Tables/RowExpander'; import { @@ -8,7 +8,7 @@ import { import { dNode } from 'models/Graph/types'; import { PlayCircleOutline } from '@material-ui/icons'; import { isParentNode } from 'components/Executions/utils'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionName } from './NodeExecutionName'; import t from '../strings'; @@ -57,7 +57,7 @@ interface TaskNamesProps { export const TaskNames = React.forwardRef( ({ nodes, onScroll, onToggle, onAction }, ref) => { const styles = useStyles(); - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const expanderRef = React.useRef(); diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts b/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts index 08de0a61b..e6100ed73 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts @@ -9,7 +9,7 @@ export const TimeZone = { export function isTransitionNode(node: dNode) { // In case of bracnhNode childs, start and end nodes could be present as 'n0-start-node' etc. - return node.id.includes(startNodeId) || node.id.includes(endNodeId); + return node?.id?.includes(startNodeId) || node?.id?.includes(endNodeId); } export function convertToPlainNodes(nodes: dNode[], level = 0): dNode[] { @@ -17,12 +17,12 @@ export function convertToPlainNodes(nodes: dNode[], level = 0): dNode[] { if (!nodes || nodes.length === 0) { return result; } - nodes.forEach(node => { + nodes?.forEach(node => { if (isTransitionNode(node)) { return; } result.push({ ...node, level }); - if (node.nodes.length > 0 && isExpanded(node)) { + if (node?.nodes?.length > 0 && isExpanded(node)) { result.push(...convertToPlainNodes(node.nodes, level + 1)); } }); diff --git a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts index 250e6c6ab..7a4e58062 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts @@ -1,8 +1,10 @@ import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; +import { isEqual } from 'lodash'; import { limits } from 'models/AdminEntity/constants'; import { FilterOperation, SortDirection } from 'models/AdminEntity/types'; import { executionSortFields } from 'models/Execution/constants'; import { Execution, NodeExecution } from 'models/Execution/types'; +import { useEffect, useState } from 'react'; import { useQueryClient, UseQueryResult } from 'react-query'; import { executionRefreshIntervalMs } from '../constants'; import { makeNodeExecutionListQuery } from '../nodeExecutionQueries'; @@ -27,15 +29,19 @@ export function useExecutionNodeViewsState( key: executionSortFields.createdAt, direction: SortDirection.ASCENDING, }; + const nodeExecutionsRequestConfig = { filter, sort, limit: limits.NONE, }; - const shouldEnableQuery = (executions: NodeExecution[]) => - !executionIsTerminal(execution) || - executions.some(ne => !nodeExecutionIsTerminal(ne)); + const shouldEnableQuery = (executions: NodeExecution[]) => { + return ( + !executionIsTerminal(execution) || + executions.some(ne => !nodeExecutionIsTerminal(ne)) + ); + }; const nodeExecutionsQuery = useConditionalQuery( { diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index 6368c4630..8fac7d150 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -1,31 +1,21 @@ import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; +import { nodeExecutionQueryParams } from 'models/Execution/constants'; import { NodeExecution } from 'models/Execution/types'; -import { useQueryClient, UseQueryResult } from 'react-query'; +import { QueryClient, UseQueryResult } from 'react-query'; import { executionRefreshIntervalMs } from '../constants'; import { makeNodeExecutionQueryEnhanced } from '../nodeExecutionQueries'; -import { nodeExecutionIsTerminal } from '../utils'; export const useNodeExecutionRow = ( + queryClient: QueryClient, execution: NodeExecution, - isInView: boolean, + shouldEnableQuery: () => boolean, ): { nodeExecutionRowQuery: UseQueryResult; } => { - const shouldEnableQuery = () => { - if (!isInView) { - return false; - } - - // No need for isParent check in here, the conditionalQuery - // will gate the fetchChildExecutions call with a isParent check. - const isTerminal = nodeExecutionIsTerminal(execution); - return !isTerminal; - }; - const nodeExecutionRowQuery = useConditionalQuery( { - ...makeNodeExecutionQueryEnhanced(execution, useQueryClient()), + ...makeNodeExecutionQueryEnhanced(execution, queryClient), refetchInterval: executionRefreshIntervalMs, }, shouldEnableQuery, diff --git a/packages/console/src/components/Executions/NodeExecutionCacheStatus.tsx b/packages/console/src/components/Executions/NodeExecutionCacheStatus.tsx index 37ca5ca8f..319bb0c8d 100644 --- a/packages/console/src/components/Executions/NodeExecutionCacheStatus.tsx +++ b/packages/console/src/components/Executions/NodeExecutionCacheStatus.tsx @@ -13,6 +13,7 @@ interface NodeExecutionCacheStatusProps { * `iconOnly` will render just the icon with the description as a tooltip */ variant?: 'normal' | 'iconOnly'; + className?: string; } /** For a given `NodeExecution.closure.taskNodeMetadata` object, will render * the cache status with a descriptive message. For `Core.CacheCatalogStatus.CACHE_HIT`, @@ -24,7 +25,7 @@ interface NodeExecutionCacheStatusProps { */ export const NodeExecutionCacheStatus: React.FC< NodeExecutionCacheStatusProps -> = ({ execution, variant = 'normal' }) => { +> = ({ execution, variant = 'normal', className }) => { const taskNodeMetadata = execution.closure?.taskNodeMetadata; const { getNodeExecutionDetails } = useNodeExecutionContext(); const [nodeDetails, setNodeDetails] = useState< @@ -49,6 +50,7 @@ export const NodeExecutionCacheStatus: React.FC< ); } @@ -64,6 +66,7 @@ export const NodeExecutionCacheStatus: React.FC< cacheStatus={taskNodeMetadata.cacheStatus} sourceTaskExecutionId={taskNodeMetadata.catalogKey?.sourceTaskExecution} variant={variant} + className={className} /> ); }; diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx index defeeca92..6f24b5234 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx @@ -21,10 +21,12 @@ import { DetailsPanelContext } from '../ExecutionDetails/DetailsPanelContext'; interface NodeExecutionActionsProps { execution: NodeExecution; + className?: string; } export const NodeExecutionActions = ({ execution, + className, }: NodeExecutionActionsProps): JSX.Element => { const { compiledWorkflowClosure, getNodeExecutionDetails } = useNodeExecutionContext(); @@ -111,20 +113,20 @@ export const NodeExecutionActions = ({ return (
{phase === NodeExecutionPhase.PAUSED && ( - + )} - + {id && initialParameters ? ( <> - + diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index d69987642..aff4d93c9 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useMemo } from 'react'; import classnames from 'classnames'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; @@ -7,18 +7,26 @@ import { isExpanded } from 'components/WorkflowGraph/utils'; import { isEqual, keyBy } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; import { makeStyles } from '@material-ui/core'; -import { LoadingSpinner } from 'components/common/LoadingSpinner'; import { useInView } from 'react-intersection-observer'; -import { selectedClassName, useExecutionTableStyles } from './styles'; +import { useQueryClient } from 'react-query'; +import { + grayedClassName, + selectedClassName, + useExecutionTableStyles, +} from './styles'; import { NodeExecutionColumnDefinition } from './types'; import { DetailsPanelContext } from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; import { isParentNode, nodeExecutionIsTerminal } from '../utils'; import { useNodeExecutionRow } from '../ExecutionDetails/useNodeExecutionRow'; -import { NodeExecutionsByIdContext } from '../contexts'; +import { NodeExecutionsById, NodeExecutionsByIdContext } from '../contexts'; +import { ignoredNodeIds } from '../nodeExecutionQueries'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ + [`${grayedClassName}`]: { + color: `${theme.palette.grey[300]} !important`, + }, namesContainerExpander: { display: 'flex', marginTop: 'auto', @@ -29,13 +37,41 @@ const useStyles = makeStyles(() => ({ }, })); +const checkEnableChildQuery = + ( + childExecutions: NodeExecution[], + nodeExecution: NodeExecution, + nodeExecutionsById: NodeExecutionsById, + node: dNode, + inView: boolean, + ) => + () => { + // check that we fetched all children otherwise force fetch + const missingChildren = + isParentNode(nodeExecution) && !childExecutions.length; + + const childrenStillRunning = childExecutions?.some( + c => !nodeExecutionIsTerminal(c), + ); + + const executionRunning = !nodeExecutionIsTerminal(nodeExecution); + + const forceRefetch = + inView && (missingChildren || childrenStillRunning || executionRunning); + + // force fetch: + // if parent's children haven't been fetched + // if parent is still running or + // if any childExecutions are still running + return forceRefetch; + }; + interface NodeExecutionRowProps { columns: NodeExecutionColumnDefinition[]; nodeExecution: NodeExecution; level?: number; style?: React.CSSProperties; node: dNode; - setShouldUpdate: (val: boolean) => void; onToggle: (id: string, scopeId: string, level: number) => void; } @@ -45,9 +81,9 @@ export const NodeExecutionRow: React.FC = ({ nodeExecution, node, style, - setShouldUpdate, onToggle, }) => { + const queryClient = useQueryClient(); const styles = useStyles(); const theme = useTheme(); const tableStyles = useExecutionTableStyles(); @@ -65,16 +101,45 @@ export const NodeExecutionRow: React.FC = ({ )}px`, }; - const { setCurrentNodeExecutionsById } = useContext( + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( NodeExecutionsByIdContext, ); + const childExecutions = useMemo(() => { + const children = node?.nodes?.reduce((accumulator, currentValue) => { + const potentialChild = nodeExecutionsById?.[currentValue?.scopedId]; + if (!ignoredNodeIds.includes(currentValue?.id) && potentialChild) { + accumulator.push(potentialChild); + } + + return accumulator; + }, [] as NodeExecution[]); + + return children; + }, [nodeExecutionsById, node]); + const expanderRef = React.useRef(); const { ref, inView } = useInView(); + const shouldForceFetchChildren = useMemo( + () => + checkEnableChildQuery( + childExecutions, + nodeExecution, + nodeExecutionsById, + node, + inView, + ), + [nodeExecution, nodeExecutionsById, node, inView], + ); + + const { nodeExecutionRowQuery } = useNodeExecutionRow( + queryClient, + nodeExecution, + shouldForceFetchChildren, + ); const { selectedExecution, setSelectedExecution } = useContext(DetailsPanelContext); - const { nodeExecutionRowQuery } = useNodeExecutionRow(nodeExecution, inView); const selected = selectedExecution ? isEqual(selectedExecution, nodeExecution) @@ -88,29 +153,26 @@ export const NodeExecutionRow: React.FC = ({ const currentNodeExecutions = nodeExecutionRowQuery.data; const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'scopedId'); - // Forces ExecutionTabContent to recompute dynamic parents - // TODO: move logic to provider. - setShouldUpdate(true); - setCurrentNodeExecutionsById(currentNodeExecutionsById); + setCurrentNodeExecutionsById(currentNodeExecutionsById, true); }, [nodeExecutionRowQuery]); const expanderContent = React.useMemo(() => { const isParent = isParentNode(nodeExecution); const isExpandedVal = isExpanded(node); - return !isParent && !nodeExecutionIsTerminal(nodeExecution) ? ( - - ) : isParent ? ( + + return isParent ? ( } expanded={isExpandedVal} onClick={() => { onToggle(node.id, node.scopedId, nodeLevel); }} + disabled={!childExecutions?.length} /> ) : (
); - }, [node, nodeLevel, nodeExecution]); + }, [node, nodeLevel, nodeExecution, childExecutions]); // 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 @@ -131,7 +193,11 @@ export const NodeExecutionRow: React.FC = ({
{expanderContent} @@ -140,11 +206,16 @@ export const NodeExecutionRow: React.FC = ({ {columns.map(({ className, key: columnKey, cellRenderer }) => (
{cellRenderer({ node, execution: nodeExecution, + className: node.grayedOut ? tableStyles.grayed : '', })}
))} diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 275154d85..380658976 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -6,20 +6,21 @@ import { NodeExecution } from 'models/Execution/types'; 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 { useQueryClient } from 'react-query'; -import { merge, eq } from 'lodash'; +import React, { useMemo, useEffect, useState } from 'react'; +import { merge, isEqual, cloneDeep } from 'lodash'; import { extractCompiledNodes } from 'components/hooks/utils'; import { ExecutionsTableHeader } from './ExecutionsTableHeader'; import { generateColumns } from './nodeExecutionColumns'; import { NoExecutionsContent } from './NoExecutionsContent'; import { useColumnStyles, useExecutionTableStyles } from './styles'; -import { NodeExecutionsByIdContext } from '../contexts'; import { convertToPlainNodes } from '../ExecutionDetails/Timeline/helpers'; -import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionRow } from './NodeExecutionRow'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { fetchChildrenExecutions, isParentNode, searchNode } from '../utils'; +import { searchNode } from '../utils'; interface NodeExecutionsTableProps { initialNodes: dNode[]; @@ -30,11 +31,24 @@ interface NodeExecutionsTableProps { const scrollbarPadding = scrollbarSize(); -/** - * TODO - * Refactor to avoid code duplication here and in ExecutionTimeline, ie toggleNode, the insides of the effect - */ +const mergeOriginIntoNodes = (target: dNode[], origin: dNode[]) => { + if (!target?.length) { + return target; + } + const newTarget = cloneDeep(target); + newTarget?.forEach(value => { + const originalNode = origin.find( + og => og.id === value.id && og.scopedId === value.scopedId, + ); + const newNodes = mergeOriginIntoNodes(value.nodes, origin); + + value = merge(value, originalNode); + value.nodes = newNodes; + return value; + }); + return newTarget; +}; /** Renders a table of NodeExecution records. Executions with errors will * have an expanadable container rendered as part of the table row. * NodeExecutions are expandable and will potentially render a list of child @@ -43,11 +57,10 @@ const scrollbarPadding = scrollbarSize(); export const NodeExecutionsTable: React.FC = ({ initialNodes, filteredNodes, - setShouldUpdate, }) => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const { appliedFilters } = useNodeExecutionFiltersState(); const [originalNodes, setOriginalNodes] = useState( appliedFilters.length > 0 && filteredNodes ? filteredNodes : initialNodes, @@ -64,20 +77,20 @@ export const NodeExecutionsTable: React.FC = ({ ); useEffect(() => { + const plainNodes = convertToPlainNodes(originalNodes || []); setOriginalNodes(ogn => { const newNodes = appliedFilters.length > 0 && filteredNodes - ? filteredNodes + ? mergeOriginIntoNodes(filteredNodes, plainNodes) : merge(initialNodes, ogn); - if (!eq(newNodes, ogn)) { + if (!isEqual(newNodes, ogn)) { return newNodes; } return ogn; }); - const plainNodes = convertToPlainNodes(originalNodes); const updatedShownNodesMap = plainNodes.map(node => { const execution = nodeExecutionsById[node.scopedId]; return { @@ -134,7 +147,6 @@ export const NodeExecutionsTable: React.FC = ({ nodeExecution={nodeExecution} node={node} onToggle={toggleNode} - setShouldUpdate={setShouldUpdate} /> ); }) diff --git a/packages/console/src/components/Executions/Tables/RowExpander.tsx b/packages/console/src/components/Executions/Tables/RowExpander.tsx index b9d4f3642..42f8cdb70 100644 --- a/packages/console/src/components/Executions/Tables/RowExpander.tsx +++ b/packages/console/src/components/Executions/Tables/RowExpander.tsx @@ -6,6 +6,7 @@ import t from './strings'; interface RowExpanderProps { expanded: boolean; + disabled?: boolean; key?: string; onClick: () => void; } @@ -13,7 +14,7 @@ interface RowExpanderProps { export const RowExpander = React.forwardRef< HTMLButtonElement, RowExpanderProps ->(({ expanded, key, onClick }, ref) => { +>(({ disabled, expanded, key, onClick }, ref) => { return ( {expanded ? : } diff --git a/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx b/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx index d41108801..23a1795c7 100644 --- a/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/packages/console/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -11,6 +11,7 @@ import { useEffect, useState } from 'react'; import { CompiledNode } from 'models/Node/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { getNodeTemplateName } from 'components/WorkflowGraph/utils'; +import classnames from 'classnames'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; @@ -28,7 +29,10 @@ import { import t from '../strings'; import { NodeExecutionName } from '../ExecutionDetails/Timeline/NodeExecutionName'; -const DisplayId: React.FC = ({ execution }) => { +const DisplayId: React.FC = ({ + execution, + className, +}) => { const commonStyles = useCommonStyles(); const { getNodeExecutionDetails } = useNodeExecutionContext(); const [displayId, setDisplayId] = useState(); @@ -48,13 +52,16 @@ const DisplayId: React.FC = ({ execution }) => { const nodeId = displayId ?? execution.id.nodeId; return ( -
{nodeId}
+
+ {nodeId} +
); }; const DisplayType: React.FC = ({ execution, + className, }) => { const { getNodeExecutionDetails } = useNodeExecutionContext(); const [type, setType] = useState(); @@ -71,7 +78,11 @@ const DisplayType: React.FC = ({ }; }); - return {type}; + return ( + + {type} + + ); }; export function generateColumns( @@ -80,11 +91,12 @@ export function generateColumns( ): NodeExecutionColumnDefinition[] { return [ { - cellRenderer: ({ node }) => ( + cellRenderer: ({ node, className }) => ( ), className: styles.columnName, @@ -104,7 +116,7 @@ export function generateColumns( label: t('typeLabel'), }, { - cellRenderer: ({ execution }) => { + cellRenderer: ({ execution, className }) => { const isGateNode = isNodeGateNode( nodes, execution.metadata?.specNodeId || execution.id.nodeId, @@ -117,10 +129,15 @@ export function generateColumns( return ( <> - + ); @@ -130,7 +147,7 @@ export function generateColumns( label: t('phaseLabel'), }, { - cellRenderer: ({ execution: { closure } }) => { + cellRenderer: ({ execution: { closure }, className }) => { const { startedAt } = closure; if (!startedAt) { return ''; @@ -138,10 +155,14 @@ export function generateColumns( const startedAtDate = timestampToDate(startedAt); return ( <> - + {formatDateUTC(startedAtDate)} - + {formatDateLocalTimezone(startedAtDate)} @@ -152,14 +173,14 @@ export function generateColumns( label: t('startedAtLabel'), }, { - cellRenderer: ({ execution }) => { + cellRenderer: ({ execution, className }) => { const timing = getNodeExecutionTimingMS(execution); if (timing === null) { return ''; } return ( <> - + {millisecondsToHMS(timing.duration)} @@ -179,9 +200,9 @@ export function generateColumns( ), }, { - cellRenderer: ({ execution }) => + cellRenderer: ({ execution, className }) => execution.closure.phase === NodeExecutionPhase.UNDEFINED ? null : ( - + ), className: styles.columnLogs, key: 'actions', diff --git a/packages/console/src/components/Executions/Tables/styles.ts b/packages/console/src/components/Executions/Tables/styles.ts index a4b969905..65c426fe8 100644 --- a/packages/console/src/components/Executions/Tables/styles.ts +++ b/packages/console/src/components/Executions/Tables/styles.ts @@ -12,12 +12,16 @@ import { } from './constants'; export const selectedClassName = 'selected'; +export const grayedClassName = 'grayed'; // NOTE: The order of these `makeStyles` calls is important, as it determines // specificity in the browser. The execution table styles are overridden by // the columns styles in some cases. So the column styles should be defined // last. export const useExecutionTableStyles = makeStyles((theme: Theme) => ({ + [grayedClassName]: { + color: theme.palette.grey[300], + }, borderBottom: { borderBottom: `1px solid ${theme.palette.divider}`, }, @@ -67,6 +71,9 @@ export const useExecutionTableStyles = makeStyles((theme: Theme) => ({ [`&.${selectedClassName}`]: { backgroundColor: listhoverColor, }, + [`&.${grayedClassName}`]: { + color: theme.palette.grey[300], + }, }, clickableRow: { cursor: 'pointer', @@ -97,6 +104,9 @@ export const useExecutionTableStyles = makeStyles((theme: Theme) => ({ export const nameColumnLeftMarginGridWidth = 6; export const useColumnStyles = makeStyles((theme: Theme) => ({ + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, columnName: { flexGrow: 1, // We want this to fluidly expand into whatever available space, @@ -106,13 +116,22 @@ export const useColumnStyles = makeStyles((theme: Theme) => ({ '&:first-of-type': { marginLeft: theme.spacing(nameColumnLeftMarginGridWidth), }, + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, }, columnNodeId: { flexBasis: nodeExecutionsTableColumnWidths.nodeId, + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, }, columnType: { flexBasis: nodeExecutionsTableColumnWidths.type, textTransform: 'capitalize', + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, }, columnStatus: { display: 'flex', @@ -120,15 +139,24 @@ export const useColumnStyles = makeStyles((theme: Theme) => ({ }, columnStartedAt: { flexBasis: nodeExecutionsTableColumnWidths.startedAt, + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, }, columnDuration: { flexBasis: nodeExecutionsTableColumnWidths.duration, textAlign: 'right', + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, }, columnLogs: { flexBasis: nodeExecutionsTableColumnWidths.logs, marginLeft: theme.spacing(4), marginRight: theme.spacing(2), + [`&.${grayedClassName}`]: { + color: theme.palette.grey[400], + }, }, selectedExecutionName: { fontWeight: 'bold', diff --git a/packages/console/src/components/Executions/Tables/types.ts b/packages/console/src/components/Executions/Tables/types.ts index c16488c50..ffa7eb4e1 100644 --- a/packages/console/src/components/Executions/Tables/types.ts +++ b/packages/console/src/components/Executions/Tables/types.ts @@ -28,6 +28,7 @@ export interface ColumnDefinition { export interface NodeExecutionCellRendererData { execution: NodeExecution; node: dNode; + className: string; } export type NodeExecutionColumnDefinition = ColumnDefinition; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx index 8e7cdacf8..d12de889f 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx @@ -5,20 +5,16 @@ import React, { useEffect, useState, } from 'react'; -import { NodeExecutionsById } from 'models/Execution/types'; -import { useExecutionNodeViewsState } from 'components/Executions/ExecutionDetails/useExecutionNodeViewsState'; +import { NodeExecution, NodeExecutionsById } from 'models/Execution/types'; import { ExecutionFiltersState } from 'components/Executions/filters/useExecutionFiltersState'; import { - ExecutionContext, FilteredNodeExecutions, INodeExecutionsByIdContext, NodeExecutionsByIdContext, } from 'components/Executions/contexts'; import { clone, isEqual, keyBy, merge } from 'lodash'; import { FilterOperation } from 'models'; -import { WaitForQuery } from 'components/common'; -import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; -import { DataError } from 'components/Errors/DataError'; +import { UseQueryResult } from 'react-query'; const DEFAULT_FALUE = {}; @@ -32,17 +28,19 @@ const isPhaseFilter = (appliedFilters: FilterOperation[]) => { export type NodeExecutionsByIdContextProviderProps = PropsWithChildren<{ initialNodeExecutionsById?: NodeExecutionsById; filterState: ExecutionFiltersState; - setShouldUpdate: (boolean) => void; + nodeExecutionsQuery: UseQueryResult; + filteredNodeExecutionsQuery: UseQueryResult; }>; /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ export const NodeExecutionsByIdContextProvider = ({ - filterState, initialNodeExecutionsById, - setShouldUpdate, + filterState, + nodeExecutionsQuery, + filteredNodeExecutionsQuery, children, }: NodeExecutionsByIdContextProviderProps) => { - const { execution } = useContext(ExecutionContext); + const [shouldUpdate, setShouldUpdate] = useState(false); const [nodeExecutionsById, setNodeExecutionsById] = useState(initialNodeExecutionsById ?? DEFAULT_FALUE); @@ -50,12 +48,6 @@ export const NodeExecutionsByIdContextProvider = ({ const [filteredNodeExecutions, setFilteredNodeExecutions] = useState(); - // query to get all data to build Graph and Timeline - const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); - // query to get filtered data to narrow down Table outputs - const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = - useExecutionNodeViewsState(execution, filterState.appliedFilters); - useEffect(() => { if (nodeExecutionsQuery.isFetching || !nodeExecutionsQuery.data) { return; @@ -69,32 +61,43 @@ export const NodeExecutionsByIdContextProvider = ({ prevNodeExecutionsById, currentNodeExecutionsById, ); - setShouldUpdate(true); + setCurrentNodeExecutionsById(newNodeExecutionsById); }, [nodeExecutionsQuery]); useEffect(() => { - if ( - filteredNodeExecutionsQuery.isFetching || - !filteredNodeExecutionsQuery.data - ) { + if (filteredNodeExecutionsQuery.isFetching) { return; } + const newFilteredNodeExecutions = isPhaseFilter(filterState.appliedFilters) ? undefined - : filteredNodeExecutions; + : filteredNodeExecutionsQuery.data; + + setFilteredNodeExecutions(prev => { + if (isEqual(prev, newFilteredNodeExecutions)) { + return prev; + } - setFilteredNodeExecutions(newFilteredNodeExecutions); + setShouldUpdate(true); + return newFilteredNodeExecutions; + }); }, [filteredNodeExecutionsQuery]); const setCurrentNodeExecutionsById = useCallback( - (currentNodeExecutionsById: NodeExecutionsById): void => { + ( + currentNodeExecutionsById: NodeExecutionsById, + checkForDynamicParents?: boolean, + ): void => { setNodeExecutionsById(prev => { const newNodes = merge({ ...prev }, currentNodeExecutionsById); if (isEqual(prev, newNodes)) { return prev; } + if (checkForDynamicParents) { + setShouldUpdate(true); + } return newNodes; }); }, @@ -115,23 +118,11 @@ export const NodeExecutionsByIdContextProvider = ({ filteredNodeExecutions, setCurrentNodeExecutionsById, resetCurrentNodeExecutionsById, + shouldUpdate, + setShouldUpdate, }} > - - {() => ( - - {() => children} - - )} - + {children} ); }; @@ -139,11 +130,3 @@ export const NodeExecutionsByIdContextProvider = ({ export const useNodeExecutionsById = (): INodeExecutionsByIdContext => { return useContext(NodeExecutionsByIdContext); }; - -const LoadingComponent = () => { - return ( -
- -
- ); -}; diff --git a/packages/console/src/components/Executions/contexts.ts b/packages/console/src/components/Executions/contexts.ts index 41cfb2b42..e0b18676b 100644 --- a/packages/console/src/components/Executions/contexts.ts +++ b/packages/console/src/components/Executions/contexts.ts @@ -18,21 +18,33 @@ export type NodeExecutionsById = Dictionary; export type FilteredNodeExecutions = WorkflowNodeExecution[] | undefined; export type SetCurrentNodeExecutionsById = ( currentNodeExecutionsById: Dictionary, + checkForDynamicParents?: boolean, ) => void; export type ResetCurrentNodeExecutionsById = ( currentNodeExecutionsById?: Dictionary, + checkForDynamicParents?: boolean, ) => void; export interface INodeExecutionsByIdContext { nodeExecutionsById: NodeExecutionsById; filteredNodeExecutions?: FilteredNodeExecutions; setCurrentNodeExecutionsById: SetCurrentNodeExecutionsById; resetCurrentNodeExecutionsById: ResetCurrentNodeExecutionsById; + shouldUpdate: boolean; + setShouldUpdate: (val: boolean) => void; } export const NodeExecutionsByIdContext = createContext({ nodeExecutionsById: {}, - setCurrentNodeExecutionsById: () => {}, - resetCurrentNodeExecutionsById: () => {}, + setCurrentNodeExecutionsById: () => { + throw new Error('Must use NodeExecutionsByIdContextProvider'); + }, + resetCurrentNodeExecutionsById: () => { + throw new Error('Must use NodeExecutionsByIdContextProvider'); + }, + shouldUpdate: false, + setShouldUpdate: _val => { + throw new Error('Must use NodeExecutionsByIdContextProvider'); + }, }); diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 9f3a9dd4b..f44e430b2 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -26,7 +26,7 @@ import { formatRetryAttempt } from './TaskExecutionsList/utils'; import { NodeExecutionGroup } from './types'; import { isParentNode } from './utils'; -const ignoredNodeIds = [startNodeId, endNodeId]; +export const ignoredNodeIds = [startNodeId, endNodeId]; function removeSystemNodes(nodeExecutions: NodeExecution[]): NodeExecution[] { return nodeExecutions.filter(ne => { if (ignoredNodeIds.includes(ne.id.nodeId)) { diff --git a/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx b/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx index f4523904f..faf6d5dc3 100644 --- a/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx +++ b/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx @@ -9,6 +9,7 @@ import { LiteralMapViewer } from 'components/Literals/LiteralMapViewer'; import { WaitForData } from 'components/common/WaitForData'; import t from 'components/common/strings'; import { CompiledNode } from 'models/Node/types'; +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { useStyles } from './styles'; import { BaseInterpretedLaunchState, @@ -39,7 +40,7 @@ export const ResumeSignalForm: React.FC = ({ nodeExecutionId, onClose, }); - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const [nodeExecution, setNodeExecution] = useState( nodeExecutionsById[nodeExecutionId.nodeId], ); diff --git a/packages/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx b/packages/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx index c68acee91..7b378a20a 100644 --- a/packages/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx +++ b/packages/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx @@ -6,6 +6,7 @@ import { getTaskLogName } from 'components/Executions/TaskExecutionsList/utils'; import { MapTaskExecution, TaskExecution } from 'models/Execution/types'; import { noLogsFoundString } from 'components/Executions/constants'; import { CacheStatus } from 'components/Executions/CacheStatus'; +import classnames from 'classnames'; import { useCommonStyles } from '../styles'; const useStyles = makeStyles((_theme: Theme) => ({ @@ -27,12 +28,14 @@ interface TaskNameListProps { taskExecution: TaskExecution; logs: Core.ITaskLog[]; onTaskSelected: (val: MapTaskExecution) => void; + className?: string; } export const TaskNameList = ({ taskExecution, logs, onTaskSelected, + className, }: TaskNameListProps) => { const commonStyles = useCommonStyles(); const styles = useStyles(); @@ -76,12 +79,19 @@ export const TaskNameList = ({ variant="body1" color={log.uri ? 'primary' : 'textPrimary'} onClick={log.uri ? handleClick : undefined} - className={log.uri ? styles.taskTitleLink : styles.taskTitle} + className={classnames( + log.uri ? styles.taskTitleLink : styles.taskTitle, + className, + )} data-testid="map-task-log" > {taskLogName}
- +
); })} diff --git a/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx index 60b8258b6..9347eb077 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx @@ -8,7 +8,10 @@ import { NodeExecutionPhase } from 'models/Execution/enums'; import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; import { nodeExecutionPhaseConstants } from 'components/Executions/constants'; import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog'; -import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { extractCompiledNodes } from 'components/hooks/utils'; import { @@ -36,7 +39,7 @@ export const PausedTasksComponent: React.FC = ({ pausedNodes, initialIsVisible = false, }) => { - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const { compiledWorkflowClosure } = useNodeExecutionContext(); const [isVisible, setIsVisible] = useState(initialIsVisible); const [showResumeForm, setShowResumeForm] = useState(false); diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 8154aaea5..88ed4db13 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -10,7 +10,10 @@ import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; import { getNodeFrontendPhase } from 'components/Executions/utils'; import { CacheStatus } from 'components/Executions/CacheStatus'; import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog'; -import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { extractCompiledNodes } from 'components/hooks/utils'; import { @@ -223,7 +226,7 @@ const TaskPhaseItem = ({ export const ReactFlowGateNode = ({ data }: RFNode) => { const { compiledWorkflowClosure } = useNodeExecutionContext(); - const { nodeExecutionsById } = useContext(NodeExecutionsByIdContext); + const { nodeExecutionsById } = useNodeExecutionsById(); const { nodeType, nodeExecutionStatus, diff --git a/packages/console/src/models/Graph/types.ts b/packages/console/src/models/Graph/types.ts index d53a76d63..fcc92cf07 100644 --- a/packages/console/src/models/Graph/types.ts +++ b/packages/console/src/models/Graph/types.ts @@ -58,6 +58,7 @@ export interface dNode { nodes: Array; edges: Array; expanded?: boolean; + grayedOut?: boolean; level?: number; execution?: NodeExecution; isParentNode?: boolean; From 676a7f3b120eebc6d7264545967cbb964a72f088 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 3 Apr 2023 18:27:42 -0700 Subject: [PATCH 06/25] chore: progress Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionNodeViews.tsx | 26 +++++++++---------- .../ExecutionDetails/ExecutionTab.tsx | 2 -- .../ExecutionDetails/ExecutionTabContent.tsx | 4 +-- .../ExecutionDetails/useNodeExecutionRow.ts | 1 - 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 70e802ef5..3ce1139db 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -65,17 +65,17 @@ export const ExecutionNodeViews: React.FC = () => { {filterState ? ( - -
- {tabState.value === tabs.nodes.id && ( -
- -
- )} +
+ {tabState.value === tabs.nodes.id && ( +
+ +
+ )} + { > {() => } -
- + +
) : ( diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 1aaaa7c0b..1905b0a48 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -24,7 +24,6 @@ export const ExecutionTab: React.FC = ({ tabType }) => { ); const { setShouldUpdate, shouldUpdate } = useNodeExecutionsById(); - const { filteredNodeExecutions } = useNodeExecutionsById(); return ( @@ -32,7 +31,6 @@ export const ExecutionTab: React.FC = ({ tabType }) => { {() => ( diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx index 296990831..e7a07113b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx @@ -104,14 +104,14 @@ interface ExecutionTabContentProps { } export const ExecutionTabContent: React.FC = ({ tabType, - filteredNodeExecutions, setShouldUpdate, shouldUpdate, }) => { const styles = useStyles(); const { compiledWorkflowClosure } = useNodeExecutionContext(); const { appliedFilters } = useNodeExecutionFiltersState(); - const { nodeExecutionsById } = useNodeExecutionsById(); + const { nodeExecutionsById, filteredNodeExecutions } = + useNodeExecutionsById(); const { staticExecutionIdsMap } = compiledWorkflowClosure ? transformerWorkflowToDag(compiledWorkflowClosure) : { staticExecutionIdsMap: {} }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index 8fac7d150..265512b3d 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -1,5 +1,4 @@ import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; -import { nodeExecutionQueryParams } from 'models/Execution/constants'; import { NodeExecution } from 'models/Execution/types'; import { QueryClient, UseQueryResult } from 'react-query'; From cbc1db388e79a12222120042973b46e6728e2b6b Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 3 Apr 2023 22:17:54 -0700 Subject: [PATCH 07/25] chore: some refactoring Signed-off-by: Carina Ursu --- .../ExecutionDetails/DetailsPanelContext.ts | 13 - .../ExecutionDetails/DetailsPanelContext.tsx | 126 ++++++++++ .../ExecutionDetails/ExecutionNodeViews.tsx | 72 +++--- .../ExecutionDetails/ExecutionTab.tsx | 234 ++++++++++++++++-- ...cutionTabContent.tsx => ExecutionTab2.tsx} | 154 ++++++------ .../Timeline/ExecutionTimeline.tsx | 17 +- .../Timeline/ExecutionTimelineContainer.tsx | 43 ++++ .../Timeline/NodeExecutionName.tsx | 5 +- .../Tables/NodeExecutionActions.tsx | 6 +- .../Executions/Tables/NodeExecutionRow.tsx | 14 +- .../Executions/Tables/NodeExecutionsTable.tsx | 129 ++++++++-- .../components/Workflow/workflowQueries.ts | 6 +- .../WorkflowGraph/WorkflowGraph.tsx | 59 +++-- 13 files changed, 663 insertions(+), 215 deletions(-) delete mode 100644 packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.ts create mode 100644 packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx rename packages/console/src/components/Executions/ExecutionDetails/{ExecutionTabContent.tsx => ExecutionTab2.tsx} (81%) create mode 100644 packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx diff --git a/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.ts b/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.ts deleted file mode 100644 index 471fb755f..000000000 --- a/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NodeExecutionIdentifier } from 'models/Execution/types'; -import { createContext } from 'react'; - -export interface DetailsPanelContextData { - selectedExecution?: NodeExecutionIdentifier | null; - setSelectedExecution: ( - selectedExecutionId: NodeExecutionIdentifier | null, - ) => void; -} - -export const DetailsPanelContext = createContext( - {} as DetailsPanelContextData, -); diff --git a/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx b/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx new file mode 100644 index 000000000..2acbb1841 --- /dev/null +++ b/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx @@ -0,0 +1,126 @@ +import React, { + PropsWithChildren, + useContext, + useEffect, + createContext, + useState, +} from 'react'; +import { NodeExecutionIdentifier } from 'models/Execution/types'; +import { DetailsPanel } from 'components/common/DetailsPanel'; +import { TaskExecutionPhase } from 'models'; +import { endNodeId, startNodeId } from 'models/Node/constants'; +import { Core } from '@flyteorg/flyteidl-types'; +import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; +import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; + +export interface DetailsPanelContextData { + selectedExecution?: NodeExecutionIdentifier | null; + setSelectedExecution: ( + selectedExecutionId: NodeExecutionIdentifier | null, + ) => void; + onNodeSelectionChanged: (newSelection: string[]) => void; + selectedPhase: Core.TaskExecution.Phase | undefined; + setSelectedPhase: ( + value: React.SetStateAction, + ) => void; + isDetailsTabClosed: boolean; + setIsDetailsTabClosed: (boolean) => void; +} + +export const DetailsPanelContext = createContext( + {} as DetailsPanelContextData, +); + +export interface DetailsPanelContextProviderProps { + selectedPhase?: TaskExecutionPhase; +} +export const DetailsPanelContextProvider = ({ + children, +}: PropsWithChildren) => { + const [selectedNodes, setSelectedNodes] = useState([]); + const { nodeExecutionsById } = useNodeExecutionsById(); + + const [selectedPhase, setSelectedPhase] = useState< + TaskExecutionPhase | undefined + >(undefined); + + // Note: flytegraph allows multiple selection, but we only support showing + // a single item in the details panel + const [selectedExecution, setSelectedExecution] = + useState( + selectedNodes.length + ? nodeExecutionsById[selectedNodes[0]] + ? nodeExecutionsById[selectedNodes[0]].id + : { + nodeId: selectedNodes[0], + executionId: + nodeExecutionsById[Object.keys(nodeExecutionsById)[0]].id + .executionId, + } + : null, + ); + + const [isDetailsTabClosed, setIsDetailsTabClosed] = useState( + !selectedExecution, + ); + + useEffect(() => { + setIsDetailsTabClosed(!selectedExecution); + }, [selectedExecution]); + + const onNodeSelectionChanged = (newSelection: string[]) => { + const validSelection = newSelection.filter(nodeId => { + if (nodeId === startNodeId || nodeId === endNodeId) { + return false; + } + return true; + }); + setSelectedNodes(validSelection); + const newSelectedExecution = validSelection.length + ? nodeExecutionsById[validSelection[0]] + ? nodeExecutionsById[validSelection[0]].id + : { + nodeId: validSelection[0], + executionId: + nodeExecutionsById[Object.keys(nodeExecutionsById)[0]].id + .executionId, + } + : null; + setSelectedExecution(newSelectedExecution); + }; + + const onCloseDetailsPanel = () => { + setSelectedExecution(null); + setSelectedPhase(undefined); + setSelectedNodes([]); + }; + + return ( + + {children} + + {!isDetailsTabClosed && selectedExecution && ( + + )} + + + ); +}; + +export const useDetailsPanel = () => { + return useContext(DetailsPanelContext); +}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 3ce1139db..d5bb2f712 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext } from 'react'; import { Tab, Tabs } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import { useTabState } from 'components/hooks/useTabState'; @@ -6,16 +6,16 @@ import { secondaryBackgroundColor } from 'components/Theme/constants'; import { WaitForQuery } from 'components/common'; import { DataError } from 'components/Errors/DataError'; import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; +import { useQuery, useQueryClient } from 'react-query'; +import { Workflow } from 'models/Workflow/types'; +import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; import { NodeExecutionDetailsContextProvider, NodeExecutionsByIdContextProvider, } from '../contextProvider/NodeExecutionDetails'; import { ExecutionContext } from '../contexts'; import { ExecutionFilters } from '../ExecutionFilters'; -import { - ExecutionFiltersState, - useNodeExecutionFiltersState, -} from '../filters/useExecutionFiltersState'; +import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { tabs } from './constants'; import { ExecutionTab } from './ExecutionTab'; import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; @@ -48,7 +48,9 @@ export const ExecutionNodeViews: React.FC = () => { const { closure: { workflowId }, } = execution; - + const workflowQuery = useQuery( + makeWorkflowQuery(useQueryClient(), workflowId), + ); // query to get all data to build Graph and Timeline const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); // query to get filtered data to narrow down Table outputs @@ -63,32 +65,40 @@ export const ExecutionNodeViews: React.FC = () => { - {filterState ? ( - -
- {tabState.value === tabs.nodes.id && ( -
- + + {() => + filterState ? ( + +
+ {tabState.value === tabs.nodes.id && ( +
+ +
+ )} + + + {() => ( + <> + + + )} + +
- )} - - - {() => } - - -
- - ) : ( - - )} + + ) : ( + + ) + } + ); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 1905b0a48..509bfea27 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -1,41 +1,233 @@ -import { WaitForQuery } from 'components/common/WaitForQuery'; -import { DataError } from 'components/Errors/DataError'; -import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; -import { Workflow } from 'models/Workflow/types'; import * as React from 'react'; -import { useQuery, useQueryClient } from 'react-query'; +import { + FilterOperation, + FilterOperationName, + FilterOperationValueList, + NodeExecution, + NodeExecutionsById, +} from 'models'; +import { dNode } from 'models/Graph/types'; +import { cloneDeep, isEqual } from 'lodash'; +import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; +import { useEffect, useState } from 'react'; +import { checkForDynamicExecutions } from 'components/common/utils'; +import { useQuery } from 'react-query'; +import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; +import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; +import { convertToPlainNodes } from './Timeline/helpers'; +import { tabs } from './constants'; +import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; +import { DetailsPanelContextProvider } from './DetailsPanelContext'; +import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; +import { nodeExecutionPhaseConstants } from '../constants'; +import { ScaleProvider } from './Timeline/scaleContext'; import { useNodeExecutionContext, useNodeExecutionsById, } from '../contextProvider/NodeExecutionDetails'; -import { ScaleProvider } from './Timeline/scaleContext'; -import { ExecutionTabContent } from './ExecutionTabContent'; +import { ExecutionTimelineContainer } from './Timeline/ExecutionTimelineContainer'; interface ExecutionTabProps { tabType: string; } +const executionMatchesPhaseFilter = ( + nodeExecution: NodeExecution, + { key, value, operation }: FilterOperation, +) => { + 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; + // phase check filters always return values in an array + const valuesArray = value as FilterOperationValueList; + return valuesArray.includes(itemValue); + } + return false; +}; + +const filterNodes = ( + initialNodes: dNode[], + nodeExecutionsById: NodeExecutionsById, + appliedFilters: FilterOperation[], +) => { + if (!initialNodes?.length) { + return []; + } + + let initialClone = cloneDeep(initialNodes); + + initialClone.forEach(n => { + n.nodes = filterNodes(n.nodes, nodeExecutionsById, appliedFilters); + }); + + initialClone = initialClone.filter(node => { + const hasFilteredChildren = node.nodes?.length; + const shouldBeIncluded = executionMatchesPhaseFilter( + nodeExecutionsById[node.scopedId], + appliedFilters[0], + ); + const result = hasFilteredChildren || shouldBeIncluded; + + if (hasFilteredChildren && !shouldBeIncluded) { + node.grayedOut = true; + } + + return result; + }); + + return initialClone; +}; + /** Contains the available ways to visualize the nodes of a WorkflowExecution */ export const ExecutionTab: React.FC = ({ tabType }) => { - const queryClient = useQueryClient(); - const { workflowId } = useNodeExecutionContext(); - const workflowQuery = useQuery( - makeWorkflowQuery(queryClient, workflowId), + const { compiledWorkflowClosure } = useNodeExecutionContext(); + const { appliedFilters } = useNodeExecutionFiltersState(); + const { + nodeExecutionsById, + filteredNodeExecutions, + setShouldUpdate, + shouldUpdate, + } = useNodeExecutionsById(); + const { staticExecutionIdsMap } = compiledWorkflowClosure + ? transformerWorkflowToDag(compiledWorkflowClosure) + : { staticExecutionIdsMap: {} }; + const [dynamicParents, setDynamicParents] = useState( + checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap), + ); + const { data: dynamicWorkflows } = useQuery( + makeNodeExecutionDynamicWorkflowQuery(dynamicParents), ); - const { setShouldUpdate, shouldUpdate } = useNodeExecutionsById(); + 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); + + useEffect(() => { + if (shouldUpdate) { + const newDynamicParents = checkForDynamicExecutions( + nodeExecutionsById, + staticExecutionIdsMap, + ); + setDynamicParents(prev => { + if (isEqual(prev, newDynamicParents)) { + return prev; + } + + return newDynamicParents; + }); + setShouldUpdate(false); + } + }, [shouldUpdate]); + + useEffect(() => { + 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); + + let newMergedDag = dag; + + for (const dynamicId in dynamicWorkflows) { + if (staticExecutionIdsMap[dynamicId]) { + if (compiledWorkflowClosure) { + 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, + dynamicParents, + nodeExecutionsById, + ]); + + useEffect(() => { + if (!isEqual(filters, appliedFilters)) { + setFilters(appliedFilters); + setIsFiltersChanged(true); + } else { + setIsFiltersChanged(false); + } + }, [appliedFilters]); + + useEffect(() => { + if (appliedFilters.length > 0) { + // if filter was apllied, but filteredNodeExecutions is empty, we only appliied Phase filter, + // and need to clear out items manually + if (!filteredNodeExecutions) { + // top level + const filteredNodes = filterNodes( + initialNodes, + nodeExecutionsById, + appliedFilters, + ); + + setInitialFilteredNodes(filteredNodes); + } else { + const filteredNodes = initialNodes.filter((node: dNode) => + filteredNodeExecutions.find( + (execution: NodeExecution) => execution.scopedId === node.scopedId, + ), + ); + setInitialFilteredNodes(filteredNodes); + } + } + }, [initialNodes, filteredNodeExecutions, isFiltersChanged]); + + const renderContent = () => { + switch (tabType) { + case tabs.nodes.id: + return ; + case tabs.graph.id: + return ( + + ); + case tabs.timeline.id: + return ; + default: + return null; + } + }; return ( - - {() => ( - - )} - + + {renderContent()} + + {/* Side panel, shows information for specific node */} ); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx similarity index 81% rename from packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx rename to packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx index e7a07113b..4bbaed076 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx @@ -1,51 +1,40 @@ -import { makeStyles } from '@material-ui/core'; -import { DetailsPanel } from 'components/common/DetailsPanel'; -import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; -import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; -import { TaskExecutionPhase } from 'models/Execution/enums'; +import * as React from 'react'; import { + FilterOperation, + FilterOperationName, + FilterOperationValueList, NodeExecution, NodeExecutionIdentifier, NodeExecutionsById, -} from 'models/Execution/types'; -import { startNodeId, endNodeId } from 'models/Node/constants'; -import React, { useEffect, useMemo, useState } from 'react'; + TaskExecutionPhase, +} from 'models'; +import { dNode } from 'models/Graph/types'; +import { cloneDeep, isEqual } from 'lodash'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; +import { useEffect, useMemo, useState } from 'react'; import { checkForDynamicExecutions } from 'components/common/utils'; -import { dNode } from 'models/Graph/types'; import { useQuery } from 'react-query'; -import { - FilterOperation, - FilterOperationName, - FilterOperationValueList, -} from 'models/AdminEntity/types'; -import { cloneDeep, isEqual } from 'lodash'; +import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; +import { endNodeId, startNodeId } from 'models/Node/constants'; +import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; +import { DetailsPanel } from 'components/common/DetailsPanel'; +import { convertToPlainNodes, TimeZone } from './Timeline/helpers'; +import { tabs } from './constants'; +import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; +import { DetailsPanelContext } from './DetailsPanelContext'; +import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; +import { nodeExecutionPhaseConstants } from '../constants'; +import { ScaleProvider } from './Timeline/scaleContext'; import { useNodeExecutionContext, useNodeExecutionsById, } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; -import { tabs } from './constants'; import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; -import { ExecutionTimeline } from './Timeline/ExecutionTimeline'; -import { ExecutionTimelineFooter } from './Timeline/ExecutionTimelineFooter'; -import { convertToPlainNodes, TimeZone } from './Timeline/helpers'; -import { DetailsPanelContext } from './DetailsPanelContext'; -import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { nodeExecutionPhaseConstants } from '../constants'; +import { ExecutionTimelineContainer } from './Timeline/ExecutionTimelineContainer'; -const useStyles = makeStyles(() => ({ - wrapper: { - display: 'flex', - flexDirection: 'column', - flex: '1 1 100%', - }, - container: { - display: 'flex', - flex: '1 1 0', - overflowY: 'auto', - }, -})); +interface ExecutionTabProps { + tabType: string; +} const executionMatchesPhaseFilter = ( nodeExecution: NodeExecution, @@ -96,22 +85,15 @@ const filterNodes = ( return initialClone; }; -interface ExecutionTabContentProps { - tabType: string; - filteredNodeExecutions?: NodeExecution[]; - setShouldUpdate: (boolean) => void; - shouldUpdate: boolean; -} -export const ExecutionTabContent: React.FC = ({ - tabType, - setShouldUpdate, - shouldUpdate, -}) => { - const styles = useStyles(); +export const useExecutionTabData = () => { const { compiledWorkflowClosure } = useNodeExecutionContext(); const { appliedFilters } = useNodeExecutionFiltersState(); - const { nodeExecutionsById, filteredNodeExecutions } = - useNodeExecutionsById(); + const { + nodeExecutionsById, + filteredNodeExecutions, + setShouldUpdate, + shouldUpdate, + } = useNodeExecutionsById(); const { staticExecutionIdsMap } = compiledWorkflowClosure ? transformerWorkflowToDag(compiledWorkflowClosure) : { staticExecutionIdsMap: {} }; @@ -260,10 +242,6 @@ export const ExecutionTabContent: React.FC = ({ setSelectedNodes([]); }; - const [chartTimezone, setChartTimezone] = useState(TimeZone.Local); - - const handleTimezoneChange = tz => setChartTimezone(tz); - const detailsPanelContext = useMemo( () => ({ selectedExecution, setSelectedExecution }), [selectedExecution, setSelectedExecution], @@ -290,6 +268,38 @@ export const ExecutionTabContent: React.FC = ({ setSelectedExecution(newSelectedExecution); }; + return { + dagError, + detailsPanelContext, + dynamicWorkflows, + initialFilteredNodes, + initialNodes, + isDetailsTabClosed, + mergedDag, + onCloseDetailsPanel, + onNodeSelectionChanged, + selectedExecution, + selectedPhase, + setSelectedPhase, + }; +}; +/** Contains the available ways to visualize the nodes of a WorkflowExecution */ +export const ExecutionTab2: React.FC = ({ tabType }) => { + const { + dagError, + detailsPanelContext, + dynamicWorkflows, + initialFilteredNodes, + initialNodes, + isDetailsTabClosed, + mergedDag, + onCloseDetailsPanel, + onNodeSelectionChanged, + selectedExecution, + selectedPhase, + setSelectedPhase, + } = useExecutionTabData(); + const renderContent = () => { switch (tabType) { case tabs.nodes.id: @@ -297,7 +307,6 @@ export const ExecutionTabContent: React.FC = ({ ); case tabs.graph.id: @@ -311,43 +320,30 @@ export const ExecutionTabContent: React.FC = ({ selectedPhase={selectedPhase} onPhaseSelectionChanged={setSelectedPhase} isDetailsTabClosed={isDetailsTabClosed} - shouldUpdate={shouldUpdate} - setShouldUpdate={setShouldUpdate} /> ); case tabs.timeline.id: - return ( -
-
- -
- -
- ); + return ; default: return null; } }; return ( - <> + {renderContent()} + + {!isDetailsTabClosed && selectedExecution && ( + + )} + {/* Side panel, shows information for specific node */} - - {!isDetailsTabClosed && selectedExecution && ( - - )} - - + ); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 2d8e98206..d4bee42fe 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -1,21 +1,15 @@ -import React, { - createRef, - useContext, - useEffect, - useRef, - useState, -} from 'react'; +import React, { createRef, useEffect, useRef, useState } from 'react'; import { makeStyles, Typography } from '@material-ui/core'; 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, searchNode, } from 'components/Executions/utils'; import { useQueryClient } from 'react-query'; import { eq, merge } from 'lodash'; +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { convertToPlainNodes } from './helpers'; import { ChartHeader } from './ChartHeader'; import { useScaleContext } from './scaleContext'; @@ -78,13 +72,11 @@ 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); @@ -96,9 +88,8 @@ export const ExecutionTimeline: React.FC = ({ const [showNodes, setShowNodes] = useState([]); const [startedAt, setStartedAt] = useState(new Date()); const queryClient = useQueryClient(); - const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( - NodeExecutionsByIdContext, - ); + const { nodeExecutionsById, setCurrentNodeExecutionsById, setShouldUpdate } = + useNodeExecutionsById(); const { chartInterval: chartTimeInterval } = useScaleContext(); useEffect(() => { diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx new file mode 100644 index 000000000..99e509ff6 --- /dev/null +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useState } from 'react'; +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { dNode } from 'models/Graph/types'; +import { ExecutionTimeline } from './ExecutionTimeline'; +import { ExecutionTimelineFooter } from './ExecutionTimelineFooter'; +import { TimeZone } from './helpers'; + +const useStyles = makeStyles(() => ({ + wrapper: { + display: 'flex', + flexDirection: 'column', + flex: '1 1 100%', + }, + container: { + display: 'flex', + flex: '1 1 0', + overflowY: 'auto', + }, +})); + +export interface ExecutionTimelineContainerProps { + initialNodes: dNode[]; +} +export const ExecutionTimelineContainer: React.FC< + ExecutionTimelineContainerProps +> = ({ initialNodes }) => { + const styles = useStyles(); + const [chartTimezone, setChartTimezone] = useState(TimeZone.Local); + const handleTimezoneChange = tz => setChartTimezone(tz); + return ( +
+
+ +
+ +
+ ); +}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx index 4adae5236..475870913 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/NodeExecutionName.tsx @@ -8,7 +8,7 @@ import { isEqual } from 'lodash'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { NodeExecution } from 'models/Execution/types'; import React, { useContext, useEffect, useState } from 'react'; -import { DetailsPanelContext } from '../DetailsPanelContext'; +import { DetailsPanelContext, useDetailsPanel } from '../DetailsPanelContext'; interface NodeExecutionTimelineNameData { name: string; @@ -39,8 +39,7 @@ export const NodeExecutionName: React.FC = ({ const styles = useStyles(); const { getNodeExecutionDetails } = useNodeExecutionContext(); - const { selectedExecution, setSelectedExecution } = - useContext(DetailsPanelContext); + const { selectedExecution, setSelectedExecution } = useDetailsPanel(); const [displayName, setDisplayName] = useState(); useEffect(() => { diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx index 6f24b5234..69a2a9d57 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx @@ -10,14 +10,14 @@ import { getTask } from 'models/Task/api'; import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; import { TaskInitialLaunchParameters } from 'components/Launch/LaunchForm/types'; import { literalsToLiteralValueMap } from 'components/Launch/LaunchForm/utils'; -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { extractCompiledNodes } from 'components/hooks/utils'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionDetails } from '../types'; import t from './strings'; import { getNodeFrontendPhase, isNodeGateNode } from '../utils'; -import { DetailsPanelContext } from '../ExecutionDetails/DetailsPanelContext'; +import { useDetailsPanel } from '../ExecutionDetails/DetailsPanelContext'; interface NodeExecutionActionsProps { execution: NodeExecution; @@ -30,7 +30,7 @@ export const NodeExecutionActions = ({ }: NodeExecutionActionsProps): JSX.Element => { const { compiledWorkflowClosure, getNodeExecutionDetails } = useNodeExecutionContext(); - const { setSelectedExecution } = useContext(DetailsPanelContext); + const { setSelectedExecution } = useDetailsPanel(); const [showLaunchForm, setShowLaunchForm] = useState(false); const [showResumeForm, setShowResumeForm] = useState(false); diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index aff4d93c9..38588b2b9 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -15,13 +15,17 @@ import { useExecutionTableStyles, } from './styles'; import { NodeExecutionColumnDefinition } from './types'; -import { DetailsPanelContext } from '../ExecutionDetails/DetailsPanelContext'; +import { + DetailsPanelContext, + useDetailsPanel, +} from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; import { isParentNode, nodeExecutionIsTerminal } from '../utils'; import { useNodeExecutionRow } from '../ExecutionDetails/useNodeExecutionRow'; import { NodeExecutionsById, NodeExecutionsByIdContext } from '../contexts'; import { ignoredNodeIds } from '../nodeExecutionQueries'; +import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; const useStyles = makeStyles(theme => ({ [`${grayedClassName}`]: { @@ -101,9 +105,8 @@ export const NodeExecutionRow: React.FC = ({ )}px`, }; - const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( - NodeExecutionsByIdContext, - ); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = + useNodeExecutionsById(); const childExecutions = useMemo(() => { const children = node?.nodes?.reduce((accumulator, currentValue) => { @@ -138,8 +141,7 @@ export const NodeExecutionRow: React.FC = ({ shouldForceFetchChildren, ); - const { selectedExecution, setSelectedExecution } = - useContext(DetailsPanelContext); + const { selectedExecution, setSelectedExecution } = useDetailsPanel(); const selected = selectedExecution ? isEqual(selectedExecution, nodeExecution) diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 380658976..ffdb3fe72 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -2,13 +2,18 @@ import classnames from 'classnames'; import { getCacheKey } from 'components/Cache/utils'; import { useCommonStyles } from 'components/common/styles'; import scrollbarSize from 'dom-helpers/scrollbarSize'; -import { NodeExecution } from 'models/Execution/types'; +import { NodeExecution, NodeExecutionsById } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { dateToTimestamp } from 'common/utils'; import React, { useMemo, useEffect, useState } from 'react'; import { merge, isEqual, cloneDeep } from 'lodash'; import { extractCompiledNodes } from 'components/hooks/utils'; +import { + FilterOperation, + FilterOperationName, + FilterOperationValueList, +} from 'models'; import { ExecutionsTableHeader } from './ExecutionsTableHeader'; import { generateColumns } from './nodeExecutionColumns'; import { NoExecutionsContent } from './NoExecutionsContent'; @@ -21,13 +26,7 @@ import { import { NodeExecutionRow } from './NodeExecutionRow'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { searchNode } from '../utils'; - -interface NodeExecutionsTableProps { - initialNodes: dNode[]; - filteredNodes?: dNode[]; - shouldUpdate?: boolean; - setShouldUpdate: (val: boolean) => void; -} +import { nodeExecutionPhaseConstants } from '../constants'; const scrollbarPadding = scrollbarSize(); @@ -49,6 +48,61 @@ const mergeOriginIntoNodes = (target: dNode[], origin: dNode[]) => { return newTarget; }; + +interface NodeExecutionsTableProps { + initialNodes: dNode[]; + filteredNodes?: dNode[]; +} + +const executionMatchesPhaseFilter = ( + nodeExecution: NodeExecution, + { key, value, operation }: FilterOperation, +) => { + 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; + // phase check filters always return values in an array + const valuesArray = value as FilterOperationValueList; + return valuesArray.includes(itemValue); + } + return false; +}; + +const filterNodes = ( + initialNodes: dNode[], + nodeExecutionsById: NodeExecutionsById, + appliedFilters: FilterOperation[], +) => { + if (!initialNodes?.length) { + return []; + } + + let initialClone = cloneDeep(initialNodes); + + initialClone.forEach(n => { + n.nodes = filterNodes(n.nodes, nodeExecutionsById, appliedFilters); + }); + + initialClone = initialClone.filter(node => { + const hasFilteredChildren = node.nodes?.length; + const shouldBeIncluded = executionMatchesPhaseFilter( + nodeExecutionsById[node.scopedId], + appliedFilters[0], + ); + const result = hasFilteredChildren || shouldBeIncluded; + + if (hasFilteredChildren && !shouldBeIncluded) { + node.grayedOut = true; + } + + return result; + }); + + return initialClone; +}; + /** Renders a table of NodeExecution records. Executions with errors will * have an expanadable container rendered as part of the table row. * NodeExecutions are expandable and will potentially render a list of child @@ -56,16 +110,28 @@ const mergeOriginIntoNodes = (target: dNode[], origin: dNode[]) => { */ export const NodeExecutionsTable: React.FC = ({ initialNodes, - filteredNodes, }) => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); - const { nodeExecutionsById } = useNodeExecutionsById(); + const { nodeExecutionsById, filteredNodeExecutions } = + useNodeExecutionsById(); const { appliedFilters } = useNodeExecutionFiltersState(); + + const [showNodes, setShowNodes] = useState([]); + const [initialFilteredNodes, setInitialFilteredNodes] = useState< + dNode[] | undefined + >(undefined); + const [originalNodes, setOriginalNodes] = useState( - appliedFilters.length > 0 && filteredNodes ? filteredNodes : initialNodes, + appliedFilters.length > 0 && initialFilteredNodes + ? initialFilteredNodes + : initialNodes, ); - const [showNodes, setShowNodes] = useState([]); + + const [filters, setFilters] = useState(appliedFilters); + + const [isFiltersChanged, setIsFiltersChanged] = useState(false); + const { compiledWorkflowClosure } = useNodeExecutionContext(); const columnStyles = useColumnStyles(); @@ -80,8 +146,8 @@ export const NodeExecutionsTable: React.FC = ({ const plainNodes = convertToPlainNodes(originalNodes || []); setOriginalNodes(ogn => { const newNodes = - appliedFilters.length > 0 && filteredNodes - ? mergeOriginIntoNodes(filteredNodes, plainNodes) + appliedFilters.length > 0 && initialFilteredNodes + ? mergeOriginIntoNodes(initialFilteredNodes, plainNodes) : merge(initialNodes, ogn); if (!isEqual(newNodes, ogn)) { @@ -100,7 +166,40 @@ export const NodeExecutionsTable: React.FC = ({ }; }); setShowNodes(updatedShownNodesMap); - }, [initialNodes, filteredNodes, originalNodes, nodeExecutionsById]); + }, [initialNodes, initialFilteredNodes, originalNodes, nodeExecutionsById]); + + useEffect(() => { + if (!isEqual(filters, appliedFilters)) { + setFilters(appliedFilters); + setIsFiltersChanged(true); + } else { + setIsFiltersChanged(false); + } + }, [appliedFilters]); + + useEffect(() => { + if (appliedFilters.length > 0) { + // if filter was apllied, but filteredNodeExecutions is empty, we only appliied Phase filter, + // and need to clear out items manually + if (!filteredNodeExecutions) { + // top level + const filteredNodes = filterNodes( + initialNodes, + nodeExecutionsById, + appliedFilters, + ); + + setInitialFilteredNodes(filteredNodes); + } else { + const filteredNodes = initialNodes.filter((node: dNode) => + filteredNodeExecutions.find( + (execution: NodeExecution) => execution.scopedId === node.scopedId, + ), + ); + setInitialFilteredNodes(filteredNodes); + } + } + }, [initialNodes, filteredNodeExecutions, isFiltersChanged]); const toggleNode = async (id: string, scopedId: string, level: number) => { searchNode(originalNodes, 0, id, scopedId, level); diff --git a/packages/console/src/components/Workflow/workflowQueries.ts b/packages/console/src/components/Workflow/workflowQueries.ts index 1fff7b10d..a6bfddd84 100644 --- a/packages/console/src/components/Workflow/workflowQueries.ts +++ b/packages/console/src/components/Workflow/workflowQueries.ts @@ -1,6 +1,7 @@ import { log } from 'common/log'; import { QueryInput, QueryType } from 'components/data/types'; import { extractTaskTemplates } from 'components/hooks/utils'; +import { ExecutionData } from 'models'; import { getNodeExecutionData } from 'models/Execution/api'; import { getWorkflow } from 'models/Workflow/api'; import { Workflow, WorkflowId } from 'models/Workflow/types'; @@ -29,9 +30,12 @@ export function makeWorkflowQuery( }; } +export interface NodeExecutionDynamicWorkflowQueryResult { + [key: string]: ExecutionData; +} export function makeNodeExecutionDynamicWorkflowQuery( parentsToFetch, -): QueryInput<{ [key: string]: any }> { +): QueryInput { return { queryKey: [QueryType.DynamicWorkflowFromNodeExecution, parentsToFetch], queryFn: async () => { diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index dfbc8da5c..66bd39e04 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -3,39 +3,36 @@ import { ReactFlowGraphComponent } from 'components/flytegraph/ReactFlow/ReactFl import { Error } from 'models/Common/types'; import { NonIdealState } from 'components/common/NonIdealState'; import { CompiledNode } from 'models/Node/types'; -import { TaskExecutionPhase } from 'models/Execution/enums'; import { dNode } from 'models/Graph/types'; +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; import t from './strings'; -export interface WorkflowGraphProps { - onNodeSelectionChanged: (selectedNodes: string[]) => void; - onPhaseSelectionChanged: (phase: TaskExecutionPhase) => void; - selectedPhase?: TaskExecutionPhase; - isDetailsTabClosed: boolean; - mergedDag: any; - error: Error | null; - dynamicWorkflows: any; - initialNodes: dNode[]; - shouldUpdate: boolean; - setShouldUpdate: (val: boolean) => void; -} export interface DynamicWorkflowMapping { rootGraphNodeId: CompiledNode; dynamicWorkflow: any; dynamicExecutions: any[]; } + +export interface WorkflowGraphProps { + mergedDag: any; + error?: Error; + initialNodes: dNode[]; +} export const WorkflowGraph: React.FC = ({ - onNodeSelectionChanged, - onPhaseSelectionChanged, - selectedPhase, - isDetailsTabClosed, mergedDag, error, - dynamicWorkflows, initialNodes, - shouldUpdate, - setShouldUpdate, }) => { + const { shouldUpdate, setShouldUpdate } = useNodeExecutionsById(); + + const { + onNodeSelectionChanged, + selectedPhase, + setSelectedPhase, + isDetailsTabClosed, + } = useDetailsPanel(); + if (error) { return ( @@ -53,15 +50,17 @@ export const WorkflowGraph: React.FC = ({ } return ( - + <> + + ); }; From 82b1a596731e2cd622d280a24882867a1b5d72bc Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 3 Apr 2023 22:21:07 -0700 Subject: [PATCH 08/25] chore: cleanup Signed-off-by: Carina Ursu --- .../ExecutionMetadataExtra.tsx | 3 +- .../ExecutionDetails/ExecutionTab.tsx | 47 +------------------ 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx index e59349b0e..e64dc16ef 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionMetadataExtra.tsx @@ -67,7 +67,8 @@ export const ExecutionMetadataExtra: React.FC<{ label: ExecutionMetadataLabels.rawOutputPrefix, value: rawOutputDataConfig?.outputLocationPrefix || - launchPlanSpec?.rawOutputDataConfig?.outputLocationPrefix, + launchPlanSpec?.rawOutputDataConfig?.outputLocationPrefix || + dashedValueString, }, { label: ExecutionMetadataLabels.parallelism, diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 509bfea27..62b16a0e2 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -83,13 +83,8 @@ const filterNodes = ( /** Contains the available ways to visualize the nodes of a WorkflowExecution */ export const ExecutionTab: React.FC = ({ tabType }) => { const { compiledWorkflowClosure } = useNodeExecutionContext(); - const { appliedFilters } = useNodeExecutionFiltersState(); - const { - nodeExecutionsById, - filteredNodeExecutions, - setShouldUpdate, - shouldUpdate, - } = useNodeExecutionsById(); + const { nodeExecutionsById, setShouldUpdate, shouldUpdate } = + useNodeExecutionsById(); const { staticExecutionIdsMap } = compiledWorkflowClosure ? transformerWorkflowToDag(compiledWorkflowClosure) : { staticExecutionIdsMap: {} }; @@ -101,13 +96,8 @@ export const ExecutionTab: React.FC = ({ tabType }) => { ); 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); useEffect(() => { if (shouldUpdate) { @@ -170,39 +160,6 @@ export const ExecutionTab: React.FC = ({ tabType }) => { nodeExecutionsById, ]); - useEffect(() => { - if (!isEqual(filters, appliedFilters)) { - setFilters(appliedFilters); - setIsFiltersChanged(true); - } else { - setIsFiltersChanged(false); - } - }, [appliedFilters]); - - useEffect(() => { - if (appliedFilters.length > 0) { - // if filter was apllied, but filteredNodeExecutions is empty, we only appliied Phase filter, - // and need to clear out items manually - if (!filteredNodeExecutions) { - // top level - const filteredNodes = filterNodes( - initialNodes, - nodeExecutionsById, - appliedFilters, - ); - - setInitialFilteredNodes(filteredNodes); - } else { - const filteredNodes = initialNodes.filter((node: dNode) => - filteredNodeExecutions.find( - (execution: NodeExecution) => execution.scopedId === node.scopedId, - ), - ); - setInitialFilteredNodes(filteredNodes); - } - } - }, [initialNodes, filteredNodeExecutions, isFiltersChanged]); - const renderContent = () => { switch (tabType) { case tabs.nodes.id: From 98b5b30ae2e0a466de67d3203a2d3207ff67a331 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Tue, 4 Apr 2023 12:46:56 -0700 Subject: [PATCH 09/25] chore: progress Signed-off-by: Carina Ursu --- .../ExecutionDetails/DetailsPanelContext.tsx | 4 +- .../ExecutionDetails/ExecutionNodeViews.tsx | 27 +- .../ExecutionDetails/ExecutionTab.tsx | 80 +--- .../ExecutionDetails/ExecutionTab2.tsx | 349 ------------------ .../TaskExecutionNodeRenderer.tsx | 2 +- .../ExecutionDetails/Timeline/TaskNames.tsx | 6 +- .../ExecutionDetails/Timeline/helpers.ts | 2 +- .../ExecutionDetails/useNodeExecutionRow.ts | 2 +- .../Executions/Tables/NodeExecutionRow.tsx | 82 ++-- .../Executions/Tables/NodeExecutionsTable.tsx | 5 +- .../TerminateExecutionForm.tsx | 4 +- .../NodeExecutionsByIdContextProvider.tsx | 22 +- .../createExecutionArray.tsx | 2 +- .../src/components/Executions/contexts.ts | 8 - .../Executions/nodeExecutionQueries.ts | 6 +- .../src/components/Executions/utils.ts | 6 +- .../Launch/LaunchForm/CollectionInput.tsx | 2 +- .../components/Launch/LaunchForm/MapInput.tsx | 2 - .../Launch/LaunchForm/StructInput.tsx | 2 +- .../WorkflowGraph/TaskNodeRenderer.tsx | 4 +- .../transformerWorkflowToDag.tsx | 2 +- .../src/components/WorkflowGraph/utils.ts | 17 +- .../ReactFlow/PausedTasksComponent.tsx | 5 +- .../ReactFlow/ReactFlowGraphComponent.tsx | 22 +- packages/console/src/models/Node/constants.ts | 2 + packages/console/src/models/Node/utils.ts | 17 + 26 files changed, 127 insertions(+), 555 deletions(-) delete mode 100644 packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx create mode 100644 packages/console/src/models/Node/utils.ts diff --git a/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx b/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx index 2acbb1841..25b78182d 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/DetailsPanelContext.tsx @@ -8,8 +8,8 @@ import React, { import { NodeExecutionIdentifier } from 'models/Execution/types'; import { DetailsPanel } from 'components/common/DetailsPanel'; import { TaskExecutionPhase } from 'models'; -import { endNodeId, startNodeId } from 'models/Node/constants'; import { Core } from '@flyteorg/flyteidl-types'; +import { isStartOrEndNode } from 'models/Node/utils'; import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; @@ -70,7 +70,7 @@ export const DetailsPanelContextProvider = ({ const onNodeSelectionChanged = (newSelection: string[]) => { const validSelection = newSelection.filter(nodeId => { - if (nodeId === startNodeId || nodeId === endNodeId) { + if (isStartOrEndNode(nodeId)) { return false; } return true; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index d5bb2f712..4dfb5ef5b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -69,17 +69,18 @@ export const ExecutionNodeViews: React.FC = () => { {() => filterState ? ( -
- {tabState.value === tabs.nodes.id && ( -
- -
- )} - + +
+ {tabState.value === tabs.nodes.id && ( +
+ +
+ )} + { )} - -
+
+
) : ( diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 62b16a0e2..86db88056 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -1,13 +1,6 @@ import * as React from 'react'; -import { - FilterOperation, - FilterOperationName, - FilterOperationValueList, - NodeExecution, - NodeExecutionsById, -} from 'models'; import { dNode } from 'models/Graph/types'; -import { cloneDeep, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { useEffect, useState } from 'react'; import { checkForDynamicExecutions } from 'components/common/utils'; @@ -18,8 +11,6 @@ import { convertToPlainNodes } from './Timeline/helpers'; import { tabs } from './constants'; import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { DetailsPanelContextProvider } from './DetailsPanelContext'; -import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { nodeExecutionPhaseConstants } from '../constants'; import { ScaleProvider } from './Timeline/scaleContext'; import { useNodeExecutionContext, @@ -31,55 +22,6 @@ interface ExecutionTabProps { tabType: string; } -const executionMatchesPhaseFilter = ( - nodeExecution: NodeExecution, - { key, value, operation }: FilterOperation, -) => { - 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; - // phase check filters always return values in an array - const valuesArray = value as FilterOperationValueList; - return valuesArray.includes(itemValue); - } - return false; -}; - -const filterNodes = ( - initialNodes: dNode[], - nodeExecutionsById: NodeExecutionsById, - appliedFilters: FilterOperation[], -) => { - if (!initialNodes?.length) { - return []; - } - - let initialClone = cloneDeep(initialNodes); - - initialClone.forEach(n => { - n.nodes = filterNodes(n.nodes, nodeExecutionsById, appliedFilters); - }); - - initialClone = initialClone.filter(node => { - const hasFilteredChildren = node.nodes?.length; - const shouldBeIncluded = executionMatchesPhaseFilter( - nodeExecutionsById[node.scopedId], - appliedFilters[0], - ); - const result = hasFilteredChildren || shouldBeIncluded; - - if (hasFilteredChildren && !shouldBeIncluded) { - node.grayedOut = true; - } - - return result; - }); - - return initialClone; -}; - /** Contains the available ways to visualize the nodes of a WorkflowExecution */ export const ExecutionTab: React.FC = ({ tabType }) => { const { compiledWorkflowClosure } = useNodeExecutionContext(); @@ -127,9 +69,6 @@ export const ExecutionTab: React.FC = ({ tabType }) => { const nodes = dag.nodes ?? []; - // we remove start/end node info in the root dNode list during first assignment - const plainNodes = convertToPlainNodes(nodes); - let newMergedDag = dag; for (const dynamicId in dynamicWorkflows) { @@ -145,14 +84,27 @@ export const ExecutionTab: React.FC = ({ tabType }) => { } } setDagError(error); - setMergedDag(newMergedDag); + setMergedDag(prev => { + if (isEqual(prev, newMergedDag)) { + return prev; + } + return newMergedDag; + }); + + // we remove start/end node info in the root dNode list during first assignment + const plainNodes = convertToPlainNodes(nodes); plainNodes.map(node => { const initialNode = initialNodes.find(n => n.scopedId === node.scopedId); if (initialNode) { node.expanded = initialNode.expanded; } }); - setInitialNodes(plainNodes); + setInitialNodes(prev => { + if (isEqual(prev, plainNodes)) { + return prev; + } + return plainNodes; + }); }, [ compiledWorkflowClosure, dynamicWorkflows, diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx deleted file mode 100644 index 4bbaed076..000000000 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab2.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import * as React from 'react'; -import { - FilterOperation, - FilterOperationName, - FilterOperationValueList, - NodeExecution, - NodeExecutionIdentifier, - NodeExecutionsById, - TaskExecutionPhase, -} from 'models'; -import { dNode } from 'models/Graph/types'; -import { cloneDeep, isEqual } from 'lodash'; -import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; -import { useEffect, useMemo, useState } from 'react'; -import { checkForDynamicExecutions } from 'components/common/utils'; -import { useQuery } from 'react-query'; -import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; -import { endNodeId, startNodeId } from 'models/Node/constants'; -import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; -import { DetailsPanel } from 'components/common/DetailsPanel'; -import { convertToPlainNodes, TimeZone } from './Timeline/helpers'; -import { tabs } from './constants'; -import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; -import { DetailsPanelContext } from './DetailsPanelContext'; -import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { nodeExecutionPhaseConstants } from '../constants'; -import { ScaleProvider } from './Timeline/scaleContext'; -import { - useNodeExecutionContext, - useNodeExecutionsById, -} from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; -import { ExecutionTimelineContainer } from './Timeline/ExecutionTimelineContainer'; - -interface ExecutionTabProps { - tabType: string; -} - -const executionMatchesPhaseFilter = ( - nodeExecution: NodeExecution, - { key, value, operation }: FilterOperation, -) => { - 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; - // phase check filters always return values in an array - const valuesArray = value as FilterOperationValueList; - return valuesArray.includes(itemValue); - } - return false; -}; - -const filterNodes = ( - initialNodes: dNode[], - nodeExecutionsById: NodeExecutionsById, - appliedFilters: FilterOperation[], -) => { - if (!initialNodes?.length) { - return []; - } - - let initialClone = cloneDeep(initialNodes); - - initialClone.forEach(n => { - n.nodes = filterNodes(n.nodes, nodeExecutionsById, appliedFilters); - }); - - initialClone = initialClone.filter(node => { - const hasFilteredChildren = node.nodes?.length; - const shouldBeIncluded = executionMatchesPhaseFilter( - nodeExecutionsById[node.scopedId], - appliedFilters[0], - ); - const result = hasFilteredChildren || shouldBeIncluded; - - if (hasFilteredChildren && !shouldBeIncluded) { - node.grayedOut = true; - } - - return result; - }); - - return initialClone; -}; - -export const useExecutionTabData = () => { - const { compiledWorkflowClosure } = useNodeExecutionContext(); - const { appliedFilters } = useNodeExecutionFiltersState(); - const { - nodeExecutionsById, - filteredNodeExecutions, - setShouldUpdate, - shouldUpdate, - } = useNodeExecutionsById(); - const { staticExecutionIdsMap } = compiledWorkflowClosure - ? transformerWorkflowToDag(compiledWorkflowClosure) - : { staticExecutionIdsMap: {} }; - const [dynamicParents, setDynamicParents] = useState( - checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap), - ); - const { data: dynamicWorkflows } = 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); - - useEffect(() => { - if (shouldUpdate) { - const newDynamicParents = checkForDynamicExecutions( - nodeExecutionsById, - staticExecutionIdsMap, - ); - setDynamicParents(prev => { - if (isEqual(prev, newDynamicParents)) { - return prev; - } - - return newDynamicParents; - }); - setShouldUpdate(false); - } - }, [shouldUpdate]); - - useEffect(() => { - 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); - - let newMergedDag = dag; - - for (const dynamicId in dynamicWorkflows) { - if (staticExecutionIdsMap[dynamicId]) { - if (compiledWorkflowClosure) { - 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, - dynamicParents, - nodeExecutionsById, - ]); - - useEffect(() => { - if (!isEqual(filters, appliedFilters)) { - setFilters(appliedFilters); - setIsFiltersChanged(true); - } else { - setIsFiltersChanged(false); - } - }, [appliedFilters]); - - useEffect(() => { - if (appliedFilters.length > 0) { - // if filter was apllied, but filteredNodeExecutions is empty, we only appliied Phase filter, - // and need to clear out items manually - if (!filteredNodeExecutions) { - // top level - const filteredNodes = filterNodes( - initialNodes, - nodeExecutionsById, - appliedFilters, - ); - - setInitialFilteredNodes(filteredNodes); - } else { - const filteredNodes = initialNodes.filter((node: dNode) => - filteredNodeExecutions.find( - (execution: NodeExecution) => execution.scopedId === node.scopedId, - ), - ); - setInitialFilteredNodes(filteredNodes); - } - } - }, [initialNodes, filteredNodeExecutions, isFiltersChanged]); - - const [selectedNodes, setSelectedNodes] = useState([]); - - // Note: flytegraph allows multiple selection, but we only support showing - // a single item in the details panel - const [selectedExecution, setSelectedExecution] = - useState( - selectedNodes.length - ? nodeExecutionsById[selectedNodes[0]] - ? nodeExecutionsById[selectedNodes[0]].id - : { - nodeId: selectedNodes[0], - executionId: - nodeExecutionsById[Object.keys(nodeExecutionsById)[0]].id - .executionId, - } - : null, - ); - - const [selectedPhase, setSelectedPhase] = useState< - TaskExecutionPhase | undefined - >(undefined); - const [isDetailsTabClosed, setIsDetailsTabClosed] = useState( - !selectedExecution, - ); - - useEffect(() => { - setIsDetailsTabClosed(!selectedExecution); - }, [selectedExecution]); - - const onCloseDetailsPanel = () => { - setSelectedExecution(null); - setSelectedPhase(undefined); - setSelectedNodes([]); - }; - - const detailsPanelContext = useMemo( - () => ({ selectedExecution, setSelectedExecution }), - [selectedExecution, setSelectedExecution], - ); - - const onNodeSelectionChanged = (newSelection: string[]) => { - const validSelection = newSelection.filter(nodeId => { - if (nodeId === startNodeId || nodeId === endNodeId) { - return false; - } - return true; - }); - setSelectedNodes(validSelection); - const newSelectedExecution = validSelection.length - ? nodeExecutionsById[validSelection[0]] - ? nodeExecutionsById[validSelection[0]].id - : { - nodeId: validSelection[0], - executionId: - nodeExecutionsById[Object.keys(nodeExecutionsById)[0]].id - .executionId, - } - : null; - setSelectedExecution(newSelectedExecution); - }; - - return { - dagError, - detailsPanelContext, - dynamicWorkflows, - initialFilteredNodes, - initialNodes, - isDetailsTabClosed, - mergedDag, - onCloseDetailsPanel, - onNodeSelectionChanged, - selectedExecution, - selectedPhase, - setSelectedPhase, - }; -}; -/** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionTab2: React.FC = ({ tabType }) => { - const { - dagError, - detailsPanelContext, - dynamicWorkflows, - initialFilteredNodes, - initialNodes, - isDetailsTabClosed, - mergedDag, - onCloseDetailsPanel, - onNodeSelectionChanged, - selectedExecution, - selectedPhase, - setSelectedPhase, - } = useExecutionTabData(); - - const renderContent = () => { - switch (tabType) { - case tabs.nodes.id: - return ( - - ); - case tabs.graph.id: - return ( - - ); - case tabs.timeline.id: - return ; - default: - return null; - } - }; - - return ( - - - {renderContent()} - - {!isDetailsTabClosed && selectedExecution && ( - - )} - - - {/* Side panel, shows information for specific node */} - - ); -}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNodeRenderer.tsx b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNodeRenderer.tsx index 53e4bc9dd..50ae08531 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNodeRenderer.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNodeRenderer.tsx @@ -1,6 +1,6 @@ import { NodeRendererProps } from 'components/flytegraph/types'; import { TaskNodeRenderer } from 'components/WorkflowGraph/TaskNodeRenderer'; -import { isEndNode, isStartNode } from 'components/WorkflowGraph/utils'; +import { isEndNode, isStartNode } from 'models/Node/utils'; import { DAGNode } from 'models/Graph/types'; import * as React from 'react'; import { TaskExecutionNode } from './TaskExecutionNode'; diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx index 82989ec31..32040a6f8 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -1,14 +1,12 @@ import React from 'react'; import { IconButton, makeStyles, Theme, Tooltip } from '@material-ui/core'; import { RowExpander } from 'components/Executions/Tables/RowExpander'; -import { - getNodeTemplateName, - isExpanded, -} from 'components/WorkflowGraph/utils'; +import { getNodeTemplateName } from 'components/WorkflowGraph/utils'; import { dNode } from 'models/Graph/types'; import { PlayCircleOutline } from '@material-ui/icons'; import { isParentNode } from 'components/Executions/utils'; import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { isExpanded } from 'models/Node/utils'; import { NodeExecutionName } from './NodeExecutionName'; import t from '../strings'; diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts b/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts index e6100ed73..c45449c60 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts @@ -1,6 +1,6 @@ import { endNodeId, startNodeId } from 'models/Node/constants'; -import { isExpanded } from 'components/WorkflowGraph/utils'; import { dNode } from 'models/Graph/types'; +import { isExpanded } from 'models/Node/utils'; export const TimeZone = { Local: 'local', diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index 265512b3d..4be4174ad 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -8,7 +8,7 @@ import { makeNodeExecutionQueryEnhanced } from '../nodeExecutionQueries'; export const useNodeExecutionRow = ( queryClient: QueryClient, execution: NodeExecution, - shouldEnableQuery: () => boolean, + shouldEnableQuery: (data: NodeExecution[]) => boolean, ): { nodeExecutionRowQuery: UseQueryResult; } => { diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index 38588b2b9..3715a3673 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -1,30 +1,26 @@ -import React, { useContext, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import classnames from 'classnames'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; -import { isExpanded } from 'components/WorkflowGraph/utils'; import { isEqual, keyBy } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; import { makeStyles } from '@material-ui/core'; import { useInView } from 'react-intersection-observer'; import { useQueryClient } from 'react-query'; +import { ignoredNodeIds } from 'models/Node/constants'; +import { isExpanded } from 'models/Node/utils'; import { grayedClassName, selectedClassName, useExecutionTableStyles, } from './styles'; import { NodeExecutionColumnDefinition } from './types'; -import { - DetailsPanelContext, - useDetailsPanel, -} from '../ExecutionDetails/DetailsPanelContext'; +import { useDetailsPanel } from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; import { isParentNode, nodeExecutionIsTerminal } from '../utils'; import { useNodeExecutionRow } from '../ExecutionDetails/useNodeExecutionRow'; -import { NodeExecutionsById, NodeExecutionsByIdContext } from '../contexts'; -import { ignoredNodeIds } from '../nodeExecutionQueries'; import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; const useStyles = makeStyles(theme => ({ @@ -41,34 +37,30 @@ const useStyles = makeStyles(theme => ({ }, })); -const checkEnableChildQuery = - ( - childExecutions: NodeExecution[], - nodeExecution: NodeExecution, - nodeExecutionsById: NodeExecutionsById, - node: dNode, - inView: boolean, - ) => - () => { - // check that we fetched all children otherwise force fetch - const missingChildren = - isParentNode(nodeExecution) && !childExecutions.length; - - const childrenStillRunning = childExecutions?.some( - c => !nodeExecutionIsTerminal(c), - ); +const checkEnableChildQuery = ( + childExecutions: NodeExecution[], + nodeExecution: NodeExecution, + inView: boolean, +) => { + // check that we fetched all children otherwise force fetch + const missingChildren = + isParentNode(nodeExecution) && !childExecutions.length; + + const childrenStillRunning = childExecutions?.some( + c => !nodeExecutionIsTerminal(c), + ); - const executionRunning = !nodeExecutionIsTerminal(nodeExecution); + const executionRunning = !nodeExecutionIsTerminal(nodeExecution); - const forceRefetch = - inView && (missingChildren || childrenStillRunning || executionRunning); + const forceRefetch = + inView && (missingChildren || childrenStillRunning || executionRunning); - // force fetch: - // if parent's children haven't been fetched - // if parent is still running or - // if any childExecutions are still running - return forceRefetch; - }; + // force fetch: + // if parent's children haven't been fetched + // if parent is still running or + // if any childExecutions are still running + return forceRefetch; +}; interface NodeExecutionRowProps { columns: NodeExecutionColumnDefinition[]; @@ -123,22 +115,22 @@ export const NodeExecutionRow: React.FC = ({ const expanderRef = React.useRef(); const { ref, inView } = useInView(); - const shouldForceFetchChildren = useMemo( - () => - checkEnableChildQuery( - childExecutions, - nodeExecution, - nodeExecutionsById, - node, - inView, - ), - [nodeExecution, nodeExecutionsById, node, inView], - ); const { nodeExecutionRowQuery } = useNodeExecutionRow( queryClient, nodeExecution, - shouldForceFetchChildren, + nodeExecutionList => { + if (!nodeExecutionList?.length) { + return true; + } + + const shouldRun = checkEnableChildQuery( + nodeExecutionList?.slice(1, nodeExecutionList.length - 1), + nodeExecution, + inView, + ); + return shouldRun; + }, ); const { selectedExecution, setSelectedExecution } = useDetailsPanel(); diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index ffdb3fe72..7722233ec 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -39,7 +39,10 @@ const mergeOriginIntoNodes = (target: dNode[], origin: dNode[]) => { const originalNode = origin.find( og => og.id === value.id && og.scopedId === value.scopedId, ); - const newNodes = mergeOriginIntoNodes(value.nodes, origin); + const newNodes = mergeOriginIntoNodes( + value.nodes, + originalNode?.nodes || [], + ); value = merge(value, originalNode); value.nodes = newNodes; diff --git a/packages/console/src/components/Executions/TerminateExecution/TerminateExecutionForm.tsx b/packages/console/src/components/Executions/TerminateExecution/TerminateExecutionForm.tsx index e5a8732f0..9474de719 100644 --- a/packages/console/src/components/Executions/TerminateExecution/TerminateExecutionForm.tsx +++ b/packages/console/src/components/Executions/TerminateExecution/TerminateExecutionForm.tsx @@ -67,8 +67,8 @@ export const TerminateExecutionForm: React.FC<{ multiline={true} onChange={onChange} placeholder={placeholderString} - rowsMax={4} - rows={4} + maxRows={4} + minRows={4} type="text" value={cause} /> diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx index d12de889f..dda71ab61 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx @@ -12,7 +12,7 @@ import { INodeExecutionsByIdContext, NodeExecutionsByIdContext, } from 'components/Executions/contexts'; -import { clone, isEqual, keyBy, merge } from 'lodash'; +import { cloneDeep, isEqual, keyBy, merge } from 'lodash'; import { FilterOperation } from 'models'; import { UseQueryResult } from 'react-query'; @@ -52,17 +52,12 @@ export const NodeExecutionsByIdContextProvider = ({ if (nodeExecutionsQuery.isFetching || !nodeExecutionsQuery.data) { return; } - const currentNodeExecutionsById = keyBy( + const fetchedNodeExecutionsById = keyBy( nodeExecutionsQuery.data, 'scopedId', ); - const prevNodeExecutionsById = clone(nodeExecutionsById); - const newNodeExecutionsById = merge( - prevNodeExecutionsById, - currentNodeExecutionsById, - ); - setCurrentNodeExecutionsById(newNodeExecutionsById); + setCurrentNodeExecutionsById(fetchedNodeExecutionsById); }, [nodeExecutionsQuery]); useEffect(() => { @@ -90,7 +85,7 @@ export const NodeExecutionsByIdContextProvider = ({ checkForDynamicParents?: boolean, ): void => { setNodeExecutionsById(prev => { - const newNodes = merge({ ...prev }, currentNodeExecutionsById); + const newNodes = merge(cloneDeep(prev), currentNodeExecutionsById); if (isEqual(prev, newNodes)) { return prev; } @@ -98,26 +93,19 @@ export const NodeExecutionsByIdContextProvider = ({ if (checkForDynamicParents) { setShouldUpdate(true); } + return newNodes; }); }, [], ); - const resetCurrentNodeExecutionsById = useCallback( - (currentNodeExecutionsById?: NodeExecutionsById): void => { - setNodeExecutionsById(currentNodeExecutionsById || DEFAULT_FALUE); - }, - [], - ); - return ( void; -export type ResetCurrentNodeExecutionsById = ( - currentNodeExecutionsById?: Dictionary, - checkForDynamicParents?: boolean, -) => void; export interface INodeExecutionsByIdContext { nodeExecutionsById: NodeExecutionsById; filteredNodeExecutions?: FilteredNodeExecutions; setCurrentNodeExecutionsById: SetCurrentNodeExecutionsById; - resetCurrentNodeExecutionsById: ResetCurrentNodeExecutionsById; shouldUpdate: boolean; setShouldUpdate: (val: boolean) => void; } @@ -40,9 +35,6 @@ export const NodeExecutionsByIdContext = setCurrentNodeExecutionsById: () => { throw new Error('Must use NodeExecutionsByIdContextProvider'); }, - resetCurrentNodeExecutionsById: () => { - throw new Error('Must use NodeExecutionsByIdContextProvider'); - }, shouldUpdate: false, setShouldUpdate: _val => { throw new Error('Must use NodeExecutionsByIdContextProvider'); diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index f44e430b2..938801c04 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -19,14 +19,13 @@ import { TaskExecutionIdentifier, WorkflowExecutionIdentifier, } from 'models/Execution/types'; -import { endNodeId, startNodeId } from 'models/Node/constants'; +import { ignoredNodeIds } from 'models/Node/constants'; import { QueryClient } from 'react-query'; import { fetchTaskExecutionList } from './taskExecutionQueries'; import { formatRetryAttempt } from './TaskExecutionsList/utils'; import { NodeExecutionGroup } from './types'; import { isParentNode } from './utils'; -export const ignoredNodeIds = [startNodeId, endNodeId]; function removeSystemNodes(nodeExecutions: NodeExecution[]): NodeExecution[] { return nodeExecutions.filter(ne => { if (ignoredNodeIds.includes(ne.id.nodeId)) { @@ -101,8 +100,9 @@ export function makeNodeExecutionQueryEnhanced( }); } - const finalExecutions = [nodeExecution, ...childExecutions]; + const finalExecutions = [{ ...nodeExecution }, ...childExecutions]; cacheNodeExecutions(queryClient, finalExecutions); + return finalExecutions; }, }; diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index 5416013ec..4d0f9a06b 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -1,9 +1,4 @@ import { durationToMilliseconds, timestampToDate } from 'common/utils'; -import { - isEndNode, - isExpanded, - isStartNode, -} from 'components/WorkflowGraph/utils'; import { clone, isEqual, keyBy, merge } from 'lodash'; import { runningExecutionStates, @@ -25,6 +20,7 @@ import { } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { CompiledNode } from 'models/Node/types'; +import { isEndNode, isExpanded, isStartNode } from 'models/Node/utils'; import { QueryClient } from 'react-query'; import { nodeExecutionPhaseConstants, diff --git a/packages/console/src/components/Launch/LaunchForm/CollectionInput.tsx b/packages/console/src/components/Launch/LaunchForm/CollectionInput.tsx index d3427dc82..34303f574 100644 --- a/packages/console/src/components/Launch/LaunchForm/CollectionInput.tsx +++ b/packages/console/src/components/Launch/LaunchForm/CollectionInput.tsx @@ -46,7 +46,7 @@ export const CollectionInput: React.FC = props => { label={label} multiline={true} onChange={makeStringChangeHandler(onChange)} - rowsMax={8} + maxRows={8} value={value} variant="outlined" /> diff --git a/packages/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/console/src/components/Launch/LaunchForm/MapInput.tsx index 3febaba75..7b2156564 100644 --- a/packages/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -166,8 +166,6 @@ export const MapInput = (props: InputProps) => { } = props; const classes = useStyles(); - console.log('MY FILTER: org error: ', error); - const [data, setData] = React.useState( parseMappedTypeValue(value), ); diff --git a/packages/console/src/components/Launch/LaunchForm/StructInput.tsx b/packages/console/src/components/Launch/LaunchForm/StructInput.tsx index 8777c97cf..c2dfe6434 100644 --- a/packages/console/src/components/Launch/LaunchForm/StructInput.tsx +++ b/packages/console/src/components/Launch/LaunchForm/StructInput.tsx @@ -120,7 +120,7 @@ export const StructInput: React.FC = props => { label={label} multiline={true} onChange={makeStringChangeHandler(onChange)} - rowsMax={8} + maxRows={8} value={value} variant="outlined" /> diff --git a/packages/console/src/components/WorkflowGraph/TaskNodeRenderer.tsx b/packages/console/src/components/WorkflowGraph/TaskNodeRenderer.tsx index bcdc5053b..461a47e21 100644 --- a/packages/console/src/components/WorkflowGraph/TaskNodeRenderer.tsx +++ b/packages/console/src/components/WorkflowGraph/TaskNodeRenderer.tsx @@ -1,11 +1,11 @@ +import * as React from 'react'; import { Node } from 'components/flytegraph/Node'; import { NodeRendererProps } from 'components/flytegraph/types'; import { taskColors } from 'components/Theme/constants'; import { DAGNode } from 'models/Graph/types'; +import { isEndNode, isStartNode } from 'models/Node/utils'; import { TaskType } from 'models/Task/constants'; -import * as React from 'react'; import { InputOutputNodeRenderer } from './InputOutputNodeRenderer'; -import { isEndNode, isStartNode } from './utils'; const TaskNode: React.FC> = props => { const { node, config } = props; diff --git a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx index bd173aacc..a7cedac0b 100644 --- a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx +++ b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -12,8 +12,8 @@ import { CompiledWorkflowClosure, } from 'models/Workflow/types'; import { isParentNode } from 'components/Executions/utils'; +import { isStartOrEndNode } from 'models/Node/utils'; import { - isStartOrEndNode, getDisplayName, getSubWorkflowFromId, getNodeTypeFromCompiledNode, diff --git a/packages/console/src/components/WorkflowGraph/utils.ts b/packages/console/src/components/WorkflowGraph/utils.ts index b789731af..a21274cb3 100644 --- a/packages/console/src/components/WorkflowGraph/utils.ts +++ b/packages/console/src/components/WorkflowGraph/utils.ts @@ -5,6 +5,7 @@ import { CompiledNode, TaskNode } from 'models/Node/types'; import { CompiledTask, TaskTemplate } from 'models/Task/types'; import { dTypes, dNode } from 'models/Graph/types'; import _ from 'lodash'; +import { isEndNode, isStartNode } from 'models/Node/utils'; import { transformerWorkflowToDag } from './transformerWorkflowToDag'; /** * TODO FC#393: these are dupes for testing, remove once tests fixed @@ -12,22 +13,6 @@ import { transformerWorkflowToDag } from './transformerWorkflowToDag'; export const DISPLAY_NAME_START = 'start'; export const DISPLAY_NAME_END = 'end'; -export const isStartOrEndNode = (node: any) => { - return node.id === startNodeId || node.id === endNodeId; -}; - -export function isStartNode(node: any) { - return node.id === startNodeId; -} - -export function isEndNode(node: any) { - return node.id === endNodeId; -} - -export function isExpanded(node: any) { - return !!node.expanded; -} - /** * Returns a display name from either workflows or nodes * @param context input can be either CompiledWorkflow or CompiledNode diff --git a/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx index 9347eb077..9a4c64a26 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import { useState, useContext } from 'react'; +import { useState } from 'react'; import { Badge, Button, withStyles } from '@material-ui/core'; import { TaskNames } from 'components/Executions/ExecutionDetails/Timeline/TaskNames'; import { dNode } from 'models/Graph/types'; -import { isExpanded } from 'components/WorkflowGraph/utils'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; import { nodeExecutionPhaseConstants } from 'components/Executions/constants'; @@ -12,8 +11,8 @@ import { useNodeExecutionContext, useNodeExecutionsById, } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { extractCompiledNodes } from 'components/hooks/utils'; +import { isExpanded } from 'models/Node/utils'; import { graphButtonContainer, graphButtonStyle, diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 0a7753a7c..22a3bb12a 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -104,11 +104,13 @@ export const ReactFlowGraphComponent = ({ setLoading(true); const nodeExecutionsWithResources = await Promise.all( baseNodeExecutions.map(async baseNodeExecution => { - if ( - !baseNodeExecution || - nodeExecutionsById?.[baseNodeExecution.scopedId]?.tasksFetched - ) { - return; + const shouldFetchTaskList = + baseNodeExecution && + baseNodeExecution.scopedId && + !nodeExecutionsById?.[baseNodeExecution.scopedId]?.tasksFetched; + + if (!shouldFetchTaskList) { + return Promise.resolve(baseNodeExecution); } const taskExecutions = await fetchTaskExecutionList( queryClient, @@ -148,11 +150,7 @@ export const ReactFlowGraphComponent = ({ nodeExecutionsWithResources, 'scopedId', ); - const newNodeExecutionsById = merge( - nodeExecutionsById, - nodeExecutionsWithResourcesMap, - ); - setCurrentNodeExecutionsById(newNodeExecutionsById); + setCurrentNodeExecutionsById(nodeExecutionsWithResourcesMap, true); setLoading(false); } } @@ -214,7 +212,7 @@ export const ReactFlowGraphComponent = ({ }; const renderGraph = () => { - const ReactFlowProps: RFWrapperProps = { + const reactFlowProps: RFWrapperProps = { backgroundStyle, rfGraphJson, type: RFGraphTypes.main, @@ -228,7 +226,7 @@ export const ReactFlowGraphComponent = ({ )} - +
); }; diff --git a/packages/console/src/models/Node/constants.ts b/packages/console/src/models/Node/constants.ts index 2d591ffc8..cbb46983a 100644 --- a/packages/console/src/models/Node/constants.ts +++ b/packages/console/src/models/Node/constants.ts @@ -1,2 +1,4 @@ export const startNodeId = 'start-node'; export const endNodeId = 'end-node'; + +export const ignoredNodeIds = [startNodeId, endNodeId]; diff --git a/packages/console/src/models/Node/utils.ts b/packages/console/src/models/Node/utils.ts new file mode 100644 index 000000000..adc92390b --- /dev/null +++ b/packages/console/src/models/Node/utils.ts @@ -0,0 +1,17 @@ +import { endNodeId, ignoredNodeIds, startNodeId } from './constants'; + +export const isStartOrEndNode = (node: any) => { + return ignoredNodeIds.includes(node.id); +}; + +export function isStartNode(node: any) { + return node.id === startNodeId; +} + +export function isEndNode(node: any) { + return node.id === endNodeId; +} + +export function isExpanded(node: any) { + return !!node.expanded; +} From 1f9b7cf4727bf35f04644a67b8829d802094eddd Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Wed, 5 Apr 2023 14:17:41 -0700 Subject: [PATCH 10/25] chore: progress Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionTab.tsx | 31 ++- .../useExecutionNodeViewsState.ts | 6 +- .../ExecutionDetails/useNodeExecutionRow.ts | 5 +- .../Executions/Tables/NodeExecutionRow.tsx | 94 ++------- .../Executions/Tables/NodeExecutionsTable.tsx | 48 ++--- .../src/components/Executions/constants.ts | 1 + .../NodeExecutionDynamicProvider.tsx | 181 ++++++++++++++++++ .../NodeExecutionsByIdContextProvider.tsx | 18 +- .../Executions/nodeExecutionQueries.ts | 104 +++++++--- .../src/components/Executions/utils.ts | 2 +- .../components/Workflow/workflowQueries.ts | 3 +- .../WorkflowGraph/WorkflowGraph.tsx | 22 +-- .../ReactFlow/ReactFlowGraphComponent.tsx | 89 +-------- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 68 +++++-- .../ReactFlow/customNodeComponents.tsx | 32 +++- .../ReactFlow/transformDAGToReactFlowV2.tsx | 1 + .../components/flytegraph/ReactFlow/types.ts | 1 + 17 files changed, 421 insertions(+), 285 deletions(-) create mode 100644 packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 86db88056..63cb89db3 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; import { dNode } from 'models/Graph/types'; -import { isEqual } from 'lodash'; +import { isEqual, merge } from 'lodash'; import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { useEffect, useState } from 'react'; import { checkForDynamicExecutions } from 'components/common/utils'; import { useQuery } from 'react-query'; -import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; +import { + makeNodeExecutionDynamicWorkflowQuery, + NodeExecutionDynamicWorkflowQueryResult, +} from 'components/Workflow/workflowQueries'; import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; import { convertToPlainNodes } from './Timeline/helpers'; import { tabs } from './constants'; @@ -33,14 +36,32 @@ export const ExecutionTab: React.FC = ({ tabType }) => { const [dynamicParents, setDynamicParents] = useState( checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap), ); - const { data: dynamicWorkflows } = useQuery( - makeNodeExecutionDynamicWorkflowQuery(dynamicParents), - ); + const [dynamicWorkflows, setDynamicWorkflows] = + useState(); + const { data: tempDynamicWorkflows, isFetching: isFetchingDynamicWorkflows } = + useQuery(makeNodeExecutionDynamicWorkflowQuery(dynamicParents)); const [initialNodes, setInitialNodes] = useState([]); const [dagError, setDagError] = useState(null); const [mergedDag, setMergedDag] = useState(null); + useEffect(() => { + if (isFetchingDynamicWorkflows) { + return; + } + setDynamicWorkflows(prev => { + const newDynamicWorkflows = merge( + { ...(prev || {}) }, + tempDynamicWorkflows, + ); + if (isEqual(prev, newDynamicWorkflows)) { + return prev; + } + + return newDynamicWorkflows; + }); + }, [tempDynamicWorkflows]); + useEffect(() => { if (shouldUpdate) { const newDynamicParents = checkForDynamicExecutions( diff --git a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts index 7a4e58062..96e6aa46d 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts @@ -37,10 +37,10 @@ export function useExecutionNodeViewsState( }; const shouldEnableQuery = (executions: NodeExecution[]) => { - return ( + const shouldEnable = !executionIsTerminal(execution) || - executions.some(ne => !nodeExecutionIsTerminal(ne)) - ); + executions.some(ne => !nodeExecutionIsTerminal(ne)); + return shouldEnable; }; const nodeExecutionsQuery = useConditionalQuery( diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index 4be4174ad..1bc4fff63 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -2,7 +2,7 @@ import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; import { NodeExecution } from 'models/Execution/types'; import { QueryClient, UseQueryResult } from 'react-query'; -import { executionRefreshIntervalMs } from '../constants'; +import { nodeExecutionRefreshIntervalMs } from '../constants'; import { makeNodeExecutionQueryEnhanced } from '../nodeExecutionQueries'; export const useNodeExecutionRow = ( @@ -15,7 +15,8 @@ export const useNodeExecutionRow = ( const nodeExecutionRowQuery = useConditionalQuery( { ...makeNodeExecutionQueryEnhanced(execution, queryClient), - refetchInterval: executionRefreshIntervalMs, + refetchInterval: nodeExecutionRefreshIntervalMs, + enabled: !!execution }, shouldEnableQuery, ); diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index 3715a3673..b460485dd 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -1,13 +1,11 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import classnames from 'classnames'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; -import { isEqual, keyBy } from 'lodash'; +import { isEqual } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; import { makeStyles } from '@material-ui/core'; -import { useInView } from 'react-intersection-observer'; -import { useQueryClient } from 'react-query'; import { ignoredNodeIds } from 'models/Node/constants'; import { isExpanded } from 'models/Node/utils'; import { @@ -19,9 +17,9 @@ import { NodeExecutionColumnDefinition } from './types'; import { useDetailsPanel } from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; -import { isParentNode, nodeExecutionIsTerminal } from '../utils'; -import { useNodeExecutionRow } from '../ExecutionDetails/useNodeExecutionRow'; +import { isParentNode } from '../utils'; import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; +import { useNodeExecutionDynamicContext } from '../contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; const useStyles = makeStyles(theme => ({ [`${grayedClassName}`]: { @@ -37,34 +35,8 @@ const useStyles = makeStyles(theme => ({ }, })); -const checkEnableChildQuery = ( - childExecutions: NodeExecution[], - nodeExecution: NodeExecution, - inView: boolean, -) => { - // check that we fetched all children otherwise force fetch - const missingChildren = - isParentNode(nodeExecution) && !childExecutions.length; - - const childrenStillRunning = childExecutions?.some( - c => !nodeExecutionIsTerminal(c), - ); - - const executionRunning = !nodeExecutionIsTerminal(nodeExecution); - - const forceRefetch = - inView && (missingChildren || childrenStillRunning || executionRunning); - - // force fetch: - // if parent's children haven't been fetched - // if parent is still running or - // if any childExecutions are still running - return forceRefetch; -}; - interface NodeExecutionRowProps { columns: NodeExecutionColumnDefinition[]; - nodeExecution: NodeExecution; level?: number; style?: React.CSSProperties; node: dNode; @@ -74,16 +46,16 @@ interface NodeExecutionRowProps { /** Renders a NodeExecution as a row inside a `NodeExecutionsTable` */ export const NodeExecutionRow: React.FC = ({ columns, - nodeExecution, node, style, onToggle, }) => { - const queryClient = useQueryClient(); const styles = useStyles(); const theme = useTheme(); const tableStyles = useExecutionTableStyles(); - + const { childCount, nodeExecution, componentProps } = + useNodeExecutionDynamicContext(); + // const key = getCacheKey(nodeExecution.id); const nodeLevel = node?.level ?? 0; // For the first level, we want the borders to span the entire table, @@ -97,41 +69,7 @@ export const NodeExecutionRow: React.FC = ({ )}px`, }; - const { nodeExecutionsById, setCurrentNodeExecutionsById } = - useNodeExecutionsById(); - - const childExecutions = useMemo(() => { - const children = node?.nodes?.reduce((accumulator, currentValue) => { - const potentialChild = nodeExecutionsById?.[currentValue?.scopedId]; - if (!ignoredNodeIds.includes(currentValue?.id) && potentialChild) { - accumulator.push(potentialChild); - } - - return accumulator; - }, [] as NodeExecution[]); - - return children; - }, [nodeExecutionsById, node]); - const expanderRef = React.useRef(); - const { ref, inView } = useInView(); - - const { nodeExecutionRowQuery } = useNodeExecutionRow( - queryClient, - nodeExecution, - nodeExecutionList => { - if (!nodeExecutionList?.length) { - return true; - } - - const shouldRun = checkEnableChildQuery( - nodeExecutionList?.slice(1, nodeExecutionList.length - 1), - nodeExecution, - inView, - ); - return shouldRun; - }, - ); const { selectedExecution, setSelectedExecution } = useDetailsPanel(); @@ -139,17 +77,6 @@ export const NodeExecutionRow: React.FC = ({ ? isEqual(selectedExecution, nodeExecution) : false; - useEffect(() => { - // don't update if still fetching - if (nodeExecutionRowQuery.isFetching || !nodeExecutionRowQuery.data) { - return; - } - - const currentNodeExecutions = nodeExecutionRowQuery.data; - const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'scopedId'); - setCurrentNodeExecutionsById(currentNodeExecutionsById, true); - }, [nodeExecutionRowQuery]); - const expanderContent = React.useMemo(() => { const isParent = isParentNode(nodeExecution); const isExpandedVal = isExpanded(node); @@ -161,12 +88,12 @@ export const NodeExecutionRow: React.FC = ({ onClick={() => { onToggle(node.id, node.scopedId, nodeLevel); }} - disabled={!childExecutions?.length} + disabled={!childCount} /> ) : (
); - }, [node, nodeLevel, nodeExecution, childExecutions]); + }, [node, nodeLevel, nodeExecution, childCount]); // 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 @@ -182,7 +109,8 @@ export const NodeExecutionRow: React.FC = ({ })} style={style} onClick={onClickRow} - ref={ref} + {...componentProps} + key={node.scopedId} >
diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 7722233ec..ce8a54f4f 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -1,11 +1,7 @@ import classnames from 'classnames'; -import { getCacheKey } from 'components/Cache/utils'; import { useCommonStyles } from 'components/common/styles'; import scrollbarSize from 'dom-helpers/scrollbarSize'; import { NodeExecution, NodeExecutionsById } from 'models/Execution/types'; -import { dNode } from 'models/Graph/types'; -import { NodeExecutionPhase } from 'models/Execution/enums'; -import { dateToTimestamp } from 'common/utils'; import React, { useMemo, useEffect, useState } from 'react'; import { merge, isEqual, cloneDeep } from 'lodash'; import { extractCompiledNodes } from 'components/hooks/utils'; @@ -14,6 +10,7 @@ import { FilterOperationName, FilterOperationValueList, } from 'models'; +import { dNode } from 'models/Graph/types'; import { ExecutionsTableHeader } from './ExecutionsTableHeader'; import { generateColumns } from './nodeExecutionColumns'; import { NoExecutionsContent } from './NoExecutionsContent'; @@ -27,6 +24,7 @@ import { NodeExecutionRow } from './NodeExecutionRow'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { searchNode } from '../utils'; import { nodeExecutionPhaseConstants } from '../constants'; +import { NodeExecutionDynamicProvider } from '../contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; const scrollbarPadding = scrollbarSize(); @@ -161,7 +159,7 @@ export const NodeExecutionsTable: React.FC = ({ }); const updatedShownNodesMap = plainNodes.map(node => { - const execution = nodeExecutionsById[node.scopedId]; + const execution = nodeExecutionsById?.[node?.scopedId]; return { ...node, startedAt: execution?.closure.startedAt, @@ -220,36 +218,16 @@ export const NodeExecutionsTable: React.FC = ({
{showNodes.length > 0 ? ( showNodes.map(node => { - let nodeExecution: NodeExecution; - if (nodeExecutionsById[node.scopedId]) { - nodeExecution = nodeExecutionsById[node.scopedId]; - } else { - nodeExecution = { - closure: { - createdAt: dateToTimestamp(new Date()), - outputUri: '', - phase: NodeExecutionPhase.UNDEFINED, - }, - id: { - executionId: { - domain: node.value?.taskNode?.referenceId?.domain, - name: node.value?.taskNode?.referenceId?.name, - project: node.value?.taskNode?.referenceId?.project, - }, - nodeId: node.id, - }, - inputUri: '', - scopedId: node.scopedId, - }; - } - return ( - + return node?.execution ? ( + + + + ) : ( + <> ); }) ) : ( diff --git a/packages/console/src/components/Executions/constants.ts b/packages/console/src/components/Executions/constants.ts index 8e14be5d6..c62b31373 100644 --- a/packages/console/src/components/Executions/constants.ts +++ b/packages/console/src/components/Executions/constants.ts @@ -16,6 +16,7 @@ import t from './strings'; import { ExecutionPhaseConstants, NodeExecutionDisplayType } from './types'; export const executionRefreshIntervalMs = 10000; +export const nodeExecutionRefreshIntervalMs = 2000; export const noLogsFoundString = t('noLogsFoundString'); /** Shared values for color/text/etc for each execution phase */ diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx new file mode 100644 index 000000000..aa2baa174 --- /dev/null +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -0,0 +1,181 @@ +import React, { + createContext, + PropsWithChildren, + useContext, + useEffect, + useMemo, + Ref, + useState, +} from 'react'; +import { dateToTimestamp } from 'common/utils'; +import { WorkflowNodeExecution } from 'components/Executions/contexts'; +import { useNodeExecutionRow } from 'components/Executions/ExecutionDetails/useNodeExecutionRow'; +import { + isParentNode, + nodeExecutionIsTerminal, +} from 'components/Executions/utils'; +import { keyBy } from 'lodash'; +import { NodeExecution, NodeExecutionPhase } from 'models'; +import { dNode } from 'models/Graph/types'; + +import { useInView } from 'react-intersection-observer'; +import { useQueryClient } from 'react-query'; +import { RFNode } from 'components/flytegraph/ReactFlow/types'; +import { useNodeExecutionsById } from './NodeExecutionsByIdContextProvider'; + +export type RefType = Ref; +export interface INodeExecutionDynamicContext { + node: dNode; + nodeExecution: WorkflowNodeExecution; + childCount: number; + inView: boolean; + componentProps: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >; +} + +export const NodeExecutionDynamicContext = + createContext({ + node: {} as dNode, + nodeExecution: undefined as any, + childCount: 0, + inView: false, + componentProps: { + ref: null, + }, + }); + +const checkEnableChildQuery = ( + childExecutions: NodeExecution[], + nodeExecution: NodeExecution, + inView: boolean, +) => { + // check that we fetched all children otherwise force fetch + const missingChildren = + isParentNode(nodeExecution) && !childExecutions.length; + + const childrenStillRunning = childExecutions?.some( + c => !nodeExecutionIsTerminal(c), + ); + + const executionRunning = !nodeExecutionIsTerminal(nodeExecution); + + const forceRefetch = + inView && (missingChildren || childrenStillRunning || executionRunning); + + // force fetch: + // if parent's children haven't been fetched + // if parent is still running or + // if any childExecutions are still running + return forceRefetch; +}; + +export type NodeExecutionDynamicProviderProps = PropsWithChildren<{ + node: dNode; + context?: string; +}>; +/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ +export const NodeExecutionDynamicProvider = ({ + node, + context, + children, +}: NodeExecutionDynamicProviderProps) => { + const queryClient = useQueryClient(); + const { ref, inView } = useInView(); + + const [fetchedChildCount, setFetchedChildCount] = useState(0); + // get running data + const { setCurrentNodeExecutionsById, nodeExecutionsById } = + useNodeExecutionsById(); + + // get the node execution + const nodeExecution: WorkflowNodeExecution | undefined = useMemo(() => { + if (nodeExecutionsById[node.scopedId]) { + return nodeExecutionsById[node.scopedId]; + } + + return; + }, [nodeExecutionsById, node]); + + const { nodeExecutionRowQuery } = useNodeExecutionRow( + queryClient, + nodeExecution!, + nodeExecutionList => { + if (!nodeExecutionList?.length) { + return true; + } + + const shouldRun = checkEnableChildQuery( + nodeExecutionList?.slice(1, nodeExecutionList.length - 1), + nodeExecution!, + inView, + ); + + if (shouldRun) { + console.log( + `Fetching node execution data for context ${context} for node `, + nodeExecution?.id?.nodeId, + ); + } + + return shouldRun; + }, + ); + + useEffect(() => { + // don't update if still fetching + if (nodeExecutionRowQuery.isFetching || !nodeExecutionRowQuery.data) { + return; + } + + const currentNodeExecutions = nodeExecutionRowQuery.data; + const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'scopedId'); + const newChildCount = currentNodeExecutions?.filter( + e => e.fromUniqueParentId === nodeExecution?.scopedId, + )?.length; + + setCurrentNodeExecutionsById(currentNodeExecutionsById, true); + setFetchedChildCount(prev => { + if (prev === newChildCount) { + return prev; + } + + return newChildCount; + }); + }, [nodeExecutionRowQuery]); + + return ( + + {children} + + ); +}; + +export const useNodeExecutionDynamicContext = + (): INodeExecutionDynamicContext => { + return useContext(NodeExecutionDynamicContext); + }; + +export const withNodeExecutionDynamicProvider = ( + WrappedComponent: React.FC, + context: string, +) => { + return (props: RFNode, ...rest: any) => { + return ( + + + + ); + }; +}; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx index dda71ab61..a69c41e1c 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx @@ -12,12 +12,10 @@ import { INodeExecutionsByIdContext, NodeExecutionsByIdContext, } from 'components/Executions/contexts'; -import { cloneDeep, isEqual, keyBy, merge } from 'lodash'; +import { cloneDeep, isEqual, keyBy, keys, merge, mergeWith } from 'lodash'; import { FilterOperation } from 'models'; import { UseQueryResult } from 'react-query'; -const DEFAULT_FALUE = {}; - const isPhaseFilter = (appliedFilters: FilterOperation[]) => { if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { return true; @@ -34,7 +32,6 @@ export type NodeExecutionsByIdContextProviderProps = PropsWithChildren<{ /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ export const NodeExecutionsByIdContextProvider = ({ - initialNodeExecutionsById, filterState, nodeExecutionsQuery, filteredNodeExecutionsQuery, @@ -43,7 +40,7 @@ export const NodeExecutionsByIdContextProvider = ({ const [shouldUpdate, setShouldUpdate] = useState(false); const [nodeExecutionsById, setNodeExecutionsById] = - useState(initialNodeExecutionsById ?? DEFAULT_FALUE); + useState(); const [filteredNodeExecutions, setFilteredNodeExecutions] = useState(); @@ -85,7 +82,7 @@ export const NodeExecutionsByIdContextProvider = ({ checkForDynamicParents?: boolean, ): void => { setNodeExecutionsById(prev => { - const newNodes = merge(cloneDeep(prev), currentNodeExecutionsById); + const newNodes = merge({ ...prev }, currentNodeExecutionsById); if (isEqual(prev, newNodes)) { return prev; } @@ -94,6 +91,13 @@ export const NodeExecutionsByIdContextProvider = ({ setShouldUpdate(true); } + console.log({ + oldCount: Object.keys(prev || {}).length, + newCount: Object.keys(newNodes).length, + old: prev, + new: newNodes, + }); + return newNodes; }); }, @@ -103,7 +107,7 @@ export const NodeExecutionsByIdContextProvider = ({ return ( { + const taskExecutions = await fetchTaskExecutionList( + // request listTaskExecutions + queryClient, + nodeExecution.id as any, + ); + + const useNewMapTaskView = taskExecutions.every(taskExecution => { + const { + closure: { taskType, metadata, eventVersion = 0 }, + } = taskExecution; + return isMapTaskV1( + eventVersion, + metadata?.externalResources?.length ?? 0, + taskType ?? undefined, + ); + }); + + const externalResources: ExternalResource[] = taskExecutions + .map(taskExecution => taskExecution.closure.metadata?.externalResources) + .flat() + .filter((resource): resource is ExternalResource => !!resource); + + const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); + + return { + ...nodeExecution, + ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), + tasksFetched: true, + }; +}; /** A query for fetching a single `NodeExecution` by id. */ export function makeNodeExecutionQueryEnhanced( nodeExecution: NodeExecution, queryClient: QueryClient, ): QueryInput { - const { id } = nodeExecution; + const { id } = nodeExecution || {}; return { queryKey: [QueryType.NodeExecutionAndChilList, id], queryFn: async () => { - // const parentNode = await getNodeExecution(id); - // if (parentNode.metadata?.specNodeId) { - // parentNode.scopedId = retriesToZero(parentNode.metadata.specNodeId); - // } else { - // parentNode.scopedId = retriesToZero(parentNode.id.nodeId); - // } - + if (!nodeExecution) { + return []; + } + // complexity: + // +1 for parent node tasks + // +1 for node execution list + // +n= executionList.length + const parent = await getNodeExecutionWithTaskExecutions( + nodeExecution, + queryClient, + ); const parentScopeId = nodeExecution.scopedId ?? nodeExecution.metadata?.specNodeId; @@ -73,10 +115,13 @@ export function makeNodeExecutionQueryEnhanced( const parentNodeID = nodeExecution.id.nodeId; const isParent = isParentNode(nodeExecution); - let childExecutions: NodeExecution[] = []; + let childExecutions: WorkflowNodeExecution[] = await Promise.resolve([ + parent, + ]); if (isParent) { - const rawChildExecutions = await fetchNodeExecutionList( + childExecutions = await fetchNodeExecutionList( + // requests listNodeExecutions queryClient, id.executionId, { @@ -84,23 +129,30 @@ export function makeNodeExecutionQueryEnhanced( [nodeExecutionQueryParams.parentNodeId]: parentNodeID, }, }, - ); - - childExecutions = rawChildExecutions?.map(childExecution => { - // TODO @jason: why are there two different wayt of generating these? - const scopedId = childExecution.metadata?.specNodeId - ? retriesToZero(childExecution?.metadata?.specNodeId) - : retriesToZero(childExecution?.id?.nodeId); - childExecution['scopedId'] = `${parentScopeId}-0-${scopedId}`; - - // childExecution['scopedId'] = ; - childExecution['fromUniqueParentId'] = parentNodeID; - - return childExecution; - }); + ) + .then(childExecutions => { + return childExecutions.map(childExecution => { + const scopedId = childExecution.metadata?.specNodeId + ? retriesToZero(childExecution?.metadata?.specNodeId) + : retriesToZero(childExecution?.id?.nodeId); + childExecution['scopedId'] = `${parentScopeId}-0-${scopedId}`; + + // childExecution['scopedId'] = ; + childExecution['fromUniqueParentId'] = parentNodeID; + + return childExecution; + }); + }) + .then(rawChildExecutions => { + return Promise.all( + rawChildExecutions?.map(childExecution => + getNodeExecutionWithTaskExecutions(childExecution, queryClient), + ), + ); + }); } - const finalExecutions = [{ ...nodeExecution }, ...childExecutions]; + const finalExecutions = [parent, ...childExecutions]; cacheNodeExecutions(queryClient, finalExecutions); return finalExecutions; diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index 4d0f9a06b..b39eb431e 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -153,7 +153,7 @@ export function isParentNode( nodeExecution: NodeExecution, ): nodeExecution is ParentNodeExecution { return ( - nodeExecution.metadata != null && !!nodeExecution.metadata.isParentNode + !!nodeExecution?.metadata?.isParentNode ); } diff --git a/packages/console/src/components/Workflow/workflowQueries.ts b/packages/console/src/components/Workflow/workflowQueries.ts index a6bfddd84..af3e5290e 100644 --- a/packages/console/src/components/Workflow/workflowQueries.ts +++ b/packages/console/src/components/Workflow/workflowQueries.ts @@ -49,10 +49,9 @@ export function makeNodeExecutionDynamicWorkflowQuery( // when Branch node support would be added log.error(`Graph missing info for ${id}`); } - const data = getNodeExecutionData(executionId.id).then(value => { + return getNodeExecutionData(executionId.id).then(value => { return { key: id, value: value }; }); - return data; }), ).then(values => { const output: { [key: string]: any } = {}; diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index 66bd39e04..251994e17 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -50,17 +50,15 @@ export const WorkflowGraph: React.FC = ({ } return ( - <> - - + ); }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 22a3bb12a..19c261a01 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,7 +1,9 @@ -import React, { useState, useEffect, useContext, useMemo } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; -import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { + useNodeExecutionContext, + useNodeExecutionsById, +} from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { fetchChildrenExecutions, @@ -15,7 +17,7 @@ import { extractCompiledNodes } from 'components/hooks/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 { keyBy } from 'lodash'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground, isUnFetchedDynamicNode } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; @@ -33,12 +35,11 @@ export const ReactFlowGraphComponent = ({ setShouldUpdate, }) => { const queryClient = useQueryClient(); - const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( - NodeExecutionsByIdContext, - ); + const { nodeExecutionsById, setCurrentNodeExecutionsById } = + useNodeExecutionsById(); const { compiledWorkflowClosure } = useNodeExecutionContext(); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [pausedNodes, setPausedNodes] = useState([]); const [currentNestedView, setcurrentNestedView] = useState({}); @@ -96,78 +97,6 @@ export const ReactFlowGraphComponent = ({ currentNestedView, ]); - 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 => { - const shouldFetchTaskList = - baseNodeExecution && - baseNodeExecution.scopedId && - !nodeExecutionsById?.[baseNodeExecution.scopedId]?.tasksFetched; - - if (!shouldFetchTaskList) { - return Promise.resolve(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 }), - tasksFetched: true, - }; - }), - ); - - if (isCurrent) { - const nodeExecutionsWithResourcesMap = keyBy( - nodeExecutionsWithResources, - 'scopedId', - ); - setCurrentNodeExecutionsById(nodeExecutionsWithResourcesMap, true); - setLoading(false); - } - } - - 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; useEffect(() => { diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index b304c1b35..1283418d0 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect, useCallback, useContext } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import ReactFlow, { Background } from 'react-flow-renderer'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; -import { useQueryClient } from 'react-query'; -import { fetchChildrenExecutions } from 'components/Executions/utils'; +import { withNodeExecutionDynamicProvider } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; +import { isEqual } from 'lodash'; import { getPositionedNodes, ReactFlowIdHash } from './utils'; import { ReactFlowCustomEndNode, @@ -21,16 +20,35 @@ import { RFWrapperProps } from './types'; * Mapping for using custom nodes inside ReactFlow */ const CustomNodeTypes = { - FlyteNode_task: ReactFlowCustomTaskNode, - FlyteNode_subworkflow: ReactFlowSubWorkflowContainer, + FlyteNode_task: withNodeExecutionDynamicProvider( + ReactFlowCustomTaskNode, + 'graph', + ), + FlyteNode_subworkflow: withNodeExecutionDynamicProvider( + ReactFlowSubWorkflowContainer, + 'graph', + ), FlyteNode_start: ReactFlowCustomStartNode, FlyteNode_end: ReactFlowCustomEndNode, FlyteNode_nestedStart: ReactFlowCustomNestedPoint, + FlyteNode_nestedEnd: ReactFlowCustomNestedPoint, - FlyteNode_nestedMaxDepth: ReactFlowCustomMaxNested, + FlyteNode_nestedMaxDepth: withNodeExecutionDynamicProvider( + ReactFlowCustomMaxNested, + 'graph', + ), FlyteNode_staticNode: ReactFlowStaticNode, FlyteNode_staticNestedNode: ReactFlowStaticNested, - FlyteNode_gateNode: ReactFlowGateNode, + FlyteNode_gateNode: withNodeExecutionDynamicProvider( + ReactFlowGateNode, + 'graph', + ), +}; + +const reactFlowStyle: React.CSSProperties = { + display: 'flex', + flex: `1 1 100%`, + flexDirection: 'column', }; export const ReactFlowWrapper: React.FC = ({ @@ -48,17 +66,31 @@ export const ReactFlowWrapper: React.FC = ({ needFitView: false, }); + const setStateDeduped = (newState: typeof state) => { + setState(prevState => { + if (JSON.stringify(prevState) === JSON.stringify(newState)) { + return prevState; + } + return newState; + }); + }; useEffect(() => { - setState(state => ({ + if (!rfGraphJson) { + return; + } + setStateDeduped({ ...state, shouldUpdate: true, nodes: rfGraphJson?.nodes, - edges: rfGraphJson?.edges?.map(edge => ({ ...edge, zIndex: 0 })), - })); + edges: rfGraphJson?.edges?.map(edge => ({ + ...edge, + zIndex: 0, + })), + }); }, [rfGraphJson]); const onLoad = (rf: any) => { - setState({ ...state, needFitView: true, reactFlowInstance: rf }); + setStateDeduped({ ...state, needFitView: true, reactFlowInstance: rf }); }; const onNodesChange = useCallback( @@ -82,12 +114,12 @@ export const ReactFlowWrapper: React.FC = ({ state.edges, ); - setState(state => ({ + setStateDeduped({ ...state, shouldUpdate: false, nodes: hashGraph, edges: hashEdges, - })); + }); } if ( changes.length === state.nodes.length && @@ -100,14 +132,8 @@ export const ReactFlowWrapper: React.FC = ({ [state.shouldUpdate, state.reactFlowInstance, state.needFitView], ); - const reactFlowStyle: React.CSSProperties = { - display: 'flex', - flex: `1 1 100%`, - flexDirection: 'column', - }; - const onNodeClick = async _event => { - setState(state => ({ ...state, needFitView: false })); + setStateDeduped({ ...state, needFitView: false }); }; return ( diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 88ed4db13..f9649a4b4 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { Handle, Position } from 'react-flow-renderer'; +import React, { useState, useEffect } from 'react'; +import { Handle, Position, ReactFlowProps } 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'; @@ -14,8 +14,8 @@ import { useNodeExecutionContext, useNodeExecutionsById, } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { extractCompiledNodes } from 'components/hooks/utils'; +import { useNodeExecutionDynamicContext } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; import { COLOR_GRAPH_BACKGROUND, getGraphHandleStyle, @@ -57,9 +57,13 @@ const renderBasicNode = ( scopedId: string, styles: React.CSSProperties, onClick?: () => void, + componentProps?: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >, ) => { return ( -
+
{renderTaskType(taskType)}
{text}
{renderDefaultHandles( @@ -149,12 +153,20 @@ export const ReactFlowCustomNestedPoint = ({ data }: RFNode) => { export const ReactFlowCustomMaxNested = ({ data }: RFNode) => { const { text, taskType, scopedId, onAddNestedView } = data; const styles = getGraphNodeStyle(dTypes.nestedMaxDepth); + const { componentProps } = useNodeExecutionDynamicContext(); const onClick = () => { onAddNestedView(); }; - return renderBasicNode(taskType, text, scopedId, styles, onClick); + return renderBasicNode( + taskType, + text, + scopedId, + styles, + onClick, + componentProps, + ); }; export const ReactFlowStaticNested = ({ data }: RFNode) => { @@ -295,8 +307,11 @@ export const ReactFlowGateNode = ({ data }: RFNode) => { * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ - -export const ReactFlowCustomTaskNode = ({ data }: RFNode) => { +export type ReactFlowCustomTaskNodeProps = ReactFlowProps & RFNode; +export const ReactFlowCustomTaskNode = ( + props: ReactFlowCustomTaskNodeProps, +) => { + const { data } = props; const { nodeType, nodeExecutionStatus, @@ -314,6 +329,7 @@ export const ReactFlowCustomTaskNode = ({ data }: RFNode) => { const [selectedPhase, setSelectedPhase] = useState< TaskExecutionPhase | undefined >(initialPhase); + const { componentProps } = useNodeExecutionDynamicContext(); useEffect(() => { if (selectedNode === true) { @@ -400,7 +416,7 @@ export const ReactFlowCustomTaskNode = ({ data }: RFNode) => { }; return ( -
+
{nodeLogsByPhase ? renderTaskName() : renderTaskType(taskType)}
{nodeLogsByPhase ? renderTaskPhases(nodeLogsByPhase) : text} diff --git a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index c7becd4c2..fc834e33b 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -103,6 +103,7 @@ const buildReactFlowDataProps = ({ : nodeExecutionsById?.[scopedId]?.closure.taskNodeMetadata?.cacheStatus; const dataProps = { + node, nodeExecutionStatus, text: displayName, handles: [], diff --git a/packages/console/src/components/flytegraph/ReactFlow/types.ts b/packages/console/src/components/flytegraph/ReactFlow/types.ts index 68e71d025..8c39c8f3a 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/types.ts +++ b/packages/console/src/components/flytegraph/ReactFlow/types.ts @@ -71,6 +71,7 @@ export interface DagToReactFlowProps extends ConvertDagProps { } interface RFCustomData { + node: dNode; nodeExecutionStatus: NodeExecutionPhase; text: string; handles: []; From 7a96164e78a9be94fc191d26453b3f15d3521d27 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Wed, 5 Apr 2023 23:55:59 -0700 Subject: [PATCH 11/25] chore: progress Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionDetails.tsx | 14 + .../ExecutionDetailsActions.tsx | 2 +- .../ExecutionDetails/ExecutionNodeViews.tsx | 6 +- .../Timeline/ExecutionTimeline.tsx | 3 +- .../useExecutionNodeViewsState.ts | 10 +- .../ExecutionDetails/useNodeExecutionRow.ts | 2 +- .../Executions/Tables/NodeExecutionRow.tsx | 2 +- .../Executions/Tables/NodeExecutionsTable.tsx | 16 +- .../NodeExecutionDynamicProvider.tsx | 48 ++-- .../NodeExecutionsByIdContextProvider.tsx | 16 +- .../NodeExecutionDetails/utils.ts | 17 ++ .../Executions/nodeExecutionQueries.ts | 91 +++---- .../src/components/Executions/utils.ts | 14 +- .../Launch/LaunchForm/LaunchFormActions.tsx | 10 +- .../Launch/LaunchForm/ResumeSignalForm.tsx | 3 +- .../WorkflowGraph/WorkflowGraph.tsx | 14 +- .../src/components/common/DetailsPanel.tsx | 1 + .../ReactFlow/ReactFlowGraphComponent.tsx | 56 +--- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 1 - .../ReactFlow/customNodeComponents.tsx | 249 +++++++++--------- .../components/flytegraph/RenderedGraph.tsx | 8 +- .../src/components/hooks/useNodeExecution.ts | 5 +- 22 files changed, 300 insertions(+), 288 deletions(-) create mode 100644 packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx index a6945e962..2061dcdf8 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx @@ -6,8 +6,10 @@ import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { WaitForQuery } from 'components/common/WaitForQuery'; import { withRouteParams } from 'components/common/withRouteParams'; import { DataError } from 'components/Errors/DataError'; +import { isEqual } from 'lodash'; import { Execution } from 'models/Execution/types'; import * as React from 'react'; +import { useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { ExecutionContext } from '../contexts'; import { useWorkflowExecutionQuery } from '../useWorkflowExecution'; @@ -55,6 +57,18 @@ const RenderExecutionDetails: React.FC = ({ const styles = useStyles(); const [metadataExpanded, setMetadataExpanded] = React.useState(true); const toggleMetadata = () => setMetadataExpanded(!metadataExpanded); + + const [localExecution, setLocalExecution] = useState(); + + React.useEffect(() => { + setLocalExecution(prev => { + if (isEqual(prev, execution)) { + return prev; + } + + return execution; + }); + }, [execution]); const contextValue = { execution, }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx index fd19094d9..2752c4dbf 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetailsActions.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Dialog, IconButton } from '@material-ui/core'; import { ResourceIdentifier, Identifier } from 'models/Common/types'; import { makeStyles, Theme } from '@material-ui/core/styles'; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 4dfb5ef5b..0c9c4e342 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -86,11 +86,7 @@ export const ExecutionNodeViews: React.FC = () => { query={nodeExecutionsQuery} loadingComponent={LoadingComponent} > - {() => ( - <> - - - )} + {() => }
diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index d4bee42fe..c7423da39 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -88,7 +88,7 @@ export const ExecutionTimeline: React.FC = ({ const [showNodes, setShowNodes] = useState([]); const [startedAt, setStartedAt] = useState(new Date()); const queryClient = useQueryClient(); - const { nodeExecutionsById, setCurrentNodeExecutionsById, setShouldUpdate } = + const { nodeExecutionsById, setCurrentNodeExecutionsById } = useNodeExecutionsById(); const { chartInterval: chartTimeInterval } = useScaleContext(); @@ -173,7 +173,6 @@ export const ExecutionTimeline: React.FC = ({ scopedId, nodeExecutionsById, setCurrentNodeExecutionsById, - setShouldUpdate, ); searchNode(originalNodes, 0, id, scopedId, level); setOriginalNodes([...originalNodes]); diff --git a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts index 96e6aa46d..f3b177d97 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts @@ -21,15 +21,15 @@ export interface UseExecutionNodeViewsState { limit: number; }; } +const sort = { + key: executionSortFields.createdAt, + direction: SortDirection.ASCENDING, +}; + export function useExecutionNodeViewsState( execution: Execution, filter: FilterOperation[] = [], ): UseExecutionNodeViewsState { - const sort = { - key: executionSortFields.createdAt, - direction: SortDirection.ASCENDING, - }; - const nodeExecutionsRequestConfig = { filter, sort, diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index 1bc4fff63..b1bdfceb9 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -16,7 +16,7 @@ export const useNodeExecutionRow = ( { ...makeNodeExecutionQueryEnhanced(execution, queryClient), refetchInterval: nodeExecutionRefreshIntervalMs, - enabled: !!execution + enabled: !!execution, }, shouldEnableQuery, ); diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index b460485dd..b899e9632 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import classnames from 'classnames'; import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index ce8a54f4f..6d653250d 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -214,24 +214,28 @@ export const NodeExecutionsTable: React.FC = ({ -
+
{showNodes.length > 0 ? ( showNodes.map(node => { - return node?.execution ? ( - + return ( + - ) : ( - <> ); }) ) : ( - + )}
diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index aa2baa174..7d814025d 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -8,7 +8,10 @@ import React, { useState, } from 'react'; import { dateToTimestamp } from 'common/utils'; -import { WorkflowNodeExecution } from 'components/Executions/contexts'; +import { + ExecutionContext, + WorkflowNodeExecution, +} from 'components/Executions/contexts'; import { useNodeExecutionRow } from 'components/Executions/ExecutionDetails/useNodeExecutionRow'; import { isParentNode, @@ -33,6 +36,7 @@ export interface INodeExecutionDynamicContext { React.HTMLAttributes, HTMLDivElement >; + // setSkipChildList: (childList: NodeExecution[]) => void; } export const NodeExecutionDynamicContext = @@ -83,6 +87,7 @@ export const NodeExecutionDynamicProvider = ({ }: NodeExecutionDynamicProviderProps) => { const queryClient = useQueryClient(); const { ref, inView } = useInView(); + const { execution } = useContext(ExecutionContext); const [fetchedChildCount, setFetchedChildCount] = useState(0); // get running data @@ -91,11 +96,23 @@ export const NodeExecutionDynamicProvider = ({ // get the node execution const nodeExecution: WorkflowNodeExecution | undefined = useMemo(() => { - if (nodeExecutionsById[node.scopedId]) { + if (nodeExecutionsById?.[node.scopedId]) { return nodeExecutionsById[node.scopedId]; } - return; + return { + closure: { + createdAt: dateToTimestamp(new Date()), + outputUri: '', + phase: NodeExecutionPhase.UNDEFINED, + }, + id: { + executionId: execution.id, + nodeId: node.scopedId, + }, + inputUri: '', + scopedId: node.scopedId, + }; }, [nodeExecutionsById, node]); const { nodeExecutionRowQuery } = useNodeExecutionRow( @@ -112,13 +129,6 @@ export const NodeExecutionDynamicProvider = ({ inView, ); - if (shouldRun) { - console.log( - `Fetching node execution data for context ${context} for node `, - nodeExecution?.id?.nodeId, - ); - } - return shouldRun; }, ); @@ -129,28 +139,32 @@ export const NodeExecutionDynamicProvider = ({ return; } - const currentNodeExecutions = nodeExecutionRowQuery.data; - const currentNodeExecutionsById = keyBy(currentNodeExecutions, 'scopedId'); - const newChildCount = currentNodeExecutions?.filter( + const parentAndChildren = nodeExecutionRowQuery.data; + + const executionChildren = parentAndChildren?.filter( e => e.fromUniqueParentId === nodeExecution?.scopedId, - )?.length; + ); + const newChildCount = executionChildren.length; + // update parent context with tnew executions data + const parentAndChildrenById = keyBy(parentAndChildren, 'scopedId'); + setCurrentNodeExecutionsById(parentAndChildrenById, true); - setCurrentNodeExecutionsById(currentNodeExecutionsById, true); + // update known children count setFetchedChildCount(prev => { if (prev === newChildCount) { return prev; } - return newChildCount; }); }, [nodeExecutionRowQuery]); return ( { if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { @@ -78,12 +79,19 @@ export const NodeExecutionsByIdContextProvider = ({ const setCurrentNodeExecutionsById = useCallback( ( - currentNodeExecutionsById: NodeExecutionsById, + newNodeExecutionsById: NodeExecutionsById, checkForDynamicParents?: boolean, ): void => { setNodeExecutionsById(prev => { - const newNodes = merge({ ...prev }, currentNodeExecutionsById); - if (isEqual(prev, newNodes)) { + const newNodes = mergeWith( + { ...prev }, + newNodeExecutionsById, + mergeNodeExecutions, + ); + if ( + JSON.stringify(prev, mapStringifyReplacer) === + JSON.stringify(newNodes, mapStringifyReplacer) + ) { return prev; } diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts new file mode 100644 index 000000000..cfe4c7cef --- /dev/null +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts @@ -0,0 +1,17 @@ +import { merge } from 'lodash'; + +export const mapStringifyReplacer = (key: string, value: any) => { + if (value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()), // or with spread: value: [...value] + }; + } else { + return value; + } +}; + +export const mergeNodeExecutions = (val, srcVal, _key) => { + const retVal = merge(val, srcVal); + return retVal; +}; diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index bd15799f5..24e48d034 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -53,7 +53,7 @@ export function makeNodeExecutionQuery( }; } -const getNodeExecutionWithTaskExecutions = async ( +export const getNodeExecutionWithTaskExecutions = async ( nodeExecution: WorkflowNodeExecution, queryClient, ) => { @@ -89,7 +89,7 @@ const getNodeExecutionWithTaskExecutions = async ( }; /** A query for fetching a single `NodeExecution` by id. */ export function makeNodeExecutionQueryEnhanced( - nodeExecution: NodeExecution, + nodeExecution: WorkflowNodeExecution, queryClient: QueryClient, ): QueryInput { const { id } = nodeExecution || {}; @@ -97,65 +97,54 @@ export function makeNodeExecutionQueryEnhanced( return { queryKey: [QueryType.NodeExecutionAndChilList, id], queryFn: async () => { - if (!nodeExecution) { - return []; - } // complexity: // +1 for parent node tasks // +1 for node execution list // +n= executionList.length - const parent = await getNodeExecutionWithTaskExecutions( - nodeExecution, - queryClient, - ); + const isParent = isParentNode(nodeExecution); + const parentNodeID = nodeExecution.id.nodeId; const parentScopeId = nodeExecution.scopedId ?? nodeExecution.metadata?.specNodeId; - nodeExecution.scopedId = parentScopeId; - const parentNodeID = nodeExecution.id.nodeId; - const isParent = isParentNode(nodeExecution); - let childExecutions: WorkflowNodeExecution[] = await Promise.resolve([ - parent, - ]); - - if (isParent) { - childExecutions = await fetchNodeExecutionList( - // requests listNodeExecutions - queryClient, - id.executionId, - { - params: { - [nodeExecutionQueryParams.parentNodeId]: parentNodeID, + // if the node is a parent, force refetch its children + const parentExecutionsPromise = isParent + ? fetchNodeExecutionList( + // requests listNodeExecutions + queryClient, + id.executionId, + { + params: { + [nodeExecutionQueryParams.parentNodeId]: parentNodeID, + }, }, - }, - ) - .then(childExecutions => { - return childExecutions.map(childExecution => { - const scopedId = childExecution.metadata?.specNodeId - ? retriesToZero(childExecution?.metadata?.specNodeId) - : retriesToZero(childExecution?.id?.nodeId); - childExecution['scopedId'] = `${parentScopeId}-0-${scopedId}`; - - // childExecution['scopedId'] = ; - childExecution['fromUniqueParentId'] = parentNodeID; - - return childExecution; - }); - }) - .then(rawChildExecutions => { - return Promise.all( - rawChildExecutions?.map(childExecution => - getNodeExecutionWithTaskExecutions(childExecution, queryClient), - ), - ); - }); - } - - const finalExecutions = [parent, ...childExecutions]; - cacheNodeExecutions(queryClient, finalExecutions); + ) + : Promise.resolve([]); + + const result = await parentExecutionsPromise.then(childExecutions => { + const children = childExecutions.map(e => { + const scopedId = e.metadata?.specNodeId + ? retriesToZero(e?.metadata?.specNodeId) + : retriesToZero(e?.id?.nodeId); + e['scopedId'] = `${parentScopeId}-0-${scopedId}`; + e['fromUniqueParentId'] = parentNodeID; + + return e; + }); + + // we don't need to fetch the childrens task executions, this is handled separately + // by each row component + const childPromises = children.map(c => Promise.resolve(c)); + + const parentAndChildren = [ + getNodeExecutionWithTaskExecutions(nodeExecution, queryClient), + ...childPromises, + ]; + // const childrenToSkip = nodeExecution.tasksFetched ? children.map(c => c.scopedId as string) : [] + return Promise.all(parentAndChildren); + }); - return finalExecutions; + return result; }, }; } diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index b39eb431e..1ec2fbb24 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -152,9 +152,7 @@ function getExecutionTimingMS({ export function isParentNode( nodeExecution: NodeExecution, ): nodeExecution is ParentNodeExecution { - return ( - !!nodeExecution?.metadata?.isParentNode - ); + return !!nodeExecution?.metadata?.isParentNode; } export function flattenBranchNodes(node: CompiledNode): CompiledNode[] { @@ -246,7 +244,6 @@ export async function fetchChildrenExecutions( scopedId: string, nodeExecutionsById: Dictionary, setCurrentNodeExecutionsById: SetCurrentNodeExecutionsById, - setShouldUpdate?: (val: boolean) => void, skipCache = false, ) { const cachedParentNode = nodeExecutionsById[scopedId]; @@ -266,19 +263,12 @@ export async function fetchChildrenExecutions( ); }); if (childGroupsExecutionsById) { - const prevNodeExecutionsById = clone(nodeExecutionsById); const currentNodeExecutionsById = merge( nodeExecutionsByIdAdapted, childGroupsExecutionsById, ); - if ( - setShouldUpdate && - !isEqual(prevNodeExecutionsById, currentNodeExecutionsById) - ) { - setShouldUpdate(true); - } - setCurrentNodeExecutionsById(currentNodeExecutionsById); + setCurrentNodeExecutionsById(currentNodeExecutionsById, true); } } } diff --git a/packages/console/src/components/Launch/LaunchForm/LaunchFormActions.tsx b/packages/console/src/components/Launch/LaunchForm/LaunchFormActions.tsx index d71728dc3..d79b177c9 100644 --- a/packages/console/src/components/Launch/LaunchForm/LaunchFormActions.tsx +++ b/packages/console/src/components/Launch/LaunchForm/LaunchFormActions.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useEffect } from 'react'; import { history } from 'routes/history'; import { Routes } from 'routes/routes'; +import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; import t from './strings'; import { LaunchState, TaskResumeContext } from './launchMachine'; import { useStyles } from './styles'; @@ -28,6 +29,7 @@ export const LaunchFormActions: React.FC = ({ }) => { const styles = useStyles(); const submissionInFlight = state.matches(LaunchState.SUBMITTING); + const { setIsDetailsTabClosed } = useDetailsPanel(); const canSubmit = [ LaunchState.ENTER_INPUTS, LaunchState.VALIDATING_INPUTS, @@ -59,9 +61,11 @@ export const LaunchFormActions: React.FC = ({ // if (state.matches({ submit: 'succeeded' })) { if (newState.matches(LaunchState.SUBMIT_SUCCEEDED)) { if (newState.context.resultExecutionId) { - history.push( - Routes.ExecutionDetails.makeUrl(newState.context.resultExecutionId), - ); + onClose(); + setIsDetailsTabClosed && setIsDetailsTabClosed(true); + // history.push( + // Routes.ExecutionDetails.makeUrl(newState.context.resultExecutionId), + // ); } const context = newState.context as TaskResumeContext; if (context.compiledNode) { diff --git a/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx b/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx index faf6d5dc3..cc784ec89 100644 --- a/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx +++ b/packages/console/src/components/Launch/LaunchForm/ResumeSignalForm.tsx @@ -1,9 +1,8 @@ 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 { useState, useEffect, useMemo } from 'react'; import { NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { useNodeExecutionData } from 'components/hooks/useNodeExecution'; import { LiteralMapViewer } from 'components/Literals/LiteralMapViewer'; import { WaitForData } from 'components/common/WaitForData'; diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index 251994e17..6380fd2bc 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -4,7 +4,6 @@ import { Error } from 'models/Common/types'; import { NonIdealState } from 'components/common/NonIdealState'; import { CompiledNode } from 'models/Node/types'; import { dNode } from 'models/Graph/types'; -import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; import t from './strings'; @@ -24,14 +23,8 @@ export const WorkflowGraph: React.FC = ({ error, initialNodes, }) => { - const { shouldUpdate, setShouldUpdate } = useNodeExecutionsById(); - - const { - onNodeSelectionChanged, - selectedPhase, - setSelectedPhase, - isDetailsTabClosed, - } = useDetailsPanel(); + const { onNodeSelectionChanged, selectedPhase, setSelectedPhase } = + useDetailsPanel(); if (error) { return ( @@ -55,10 +48,7 @@ export const WorkflowGraph: React.FC = ({ onNodeSelectionChanged={onNodeSelectionChanged} onPhaseSelectionChanged={setSelectedPhase} selectedPhase={selectedPhase} - isDetailsTabClosed={isDetailsTabClosed} initialNodes={initialNodes} - shouldUpdate={shouldUpdate} - setShouldUpdate={setShouldUpdate} /> ); }; diff --git a/packages/console/src/components/common/DetailsPanel.tsx b/packages/console/src/components/common/DetailsPanel.tsx index 144da77e6..81d15e17f 100644 --- a/packages/console/src/components/common/DetailsPanel.tsx +++ b/packages/console/src/components/common/DetailsPanel.tsx @@ -49,6 +49,7 @@ export const DetailsPanel: React.FC = ({ }} onClose={onClose} open={open} + key="detailsPanel" >
diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 19c261a01..313bec239 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -11,35 +11,34 @@ import { } 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 { extractCompiledNodes } from 'components/hooks/utils'; -import { ExternalResource, LogsByPhase } from 'models/Execution/types'; -import { getGroupedLogs } from 'components/Executions/TaskExecutionsList/utils'; -import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; -import { keyBy } from 'lodash'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground, isUnFetchedDynamicNode } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; import { PausedTasksComponent } from './PausedTasksComponent'; +const containerStyle: React.CSSProperties = { + display: 'flex', + flex: `1 1 100%`, + flexDirection: 'column', + minHeight: '100px', + minWidth: '200px', + height: '100%', +}; + export const ReactFlowGraphComponent = ({ data, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, - isDetailsTabClosed, initialNodes, - shouldUpdate, - setShouldUpdate, }) => { const queryClient = useQueryClient(); const { nodeExecutionsById, setCurrentNodeExecutionsById } = useNodeExecutionsById(); const { compiledWorkflowClosure } = useNodeExecutionContext(); - const [loading, setLoading] = useState(false); const [pausedNodes, setPausedNodes] = useState([]); const [currentNestedView, setcurrentNestedView] = useState({}); @@ -50,7 +49,6 @@ export const ReactFlowGraphComponent = ({ sourceNode.scopedId, nodeExecutionsById, setCurrentNodeExecutionsById, - setShouldUpdate, ); } @@ -73,7 +71,7 @@ export const ReactFlowGraphComponent = ({ }; const rfGraphJson = useMemo(() => { - return ConvertFlyteDagToReactFlows({ + const a = ConvertFlyteDagToReactFlows({ root: data, nodeExecutionsById, onNodeSelectionChanged, @@ -84,18 +82,8 @@ export const ReactFlowGraphComponent = ({ currentNestedView, maxRenderDepth: 1, } as ConvertDagProps); - }, [ - data, - isDetailsTabClosed, - shouldUpdate, - nodeExecutionsById, - onNodeSelectionChanged, - onPhaseSelectionChanged, - selectedPhase, - onAddNestedView, - onRemoveNestedView, - currentNestedView, - ]); + return a; + }, [data, nodeExecutionsById, selectedPhase, currentNestedView]); const backgroundStyle = getRFBackground().nested; @@ -121,24 +109,7 @@ export const ReactFlowGraphComponent = ({ }; }); setPausedNodes(nodesWithExecutions); - }, [initialNodes]); - - if (loading) { - return ( -
- -
- ); - } - - const containerStyle: React.CSSProperties = { - display: 'flex', - flex: `1 1 100%`, - flexDirection: 'column', - minHeight: '100px', - minWidth: '200px', - height: '100%', - }; + }, [initialNodes, nodeExecutionsById]); const renderGraph = () => { const reactFlowProps: RFWrapperProps = { @@ -147,7 +118,6 @@ export const ReactFlowGraphComponent = ({ type: RFGraphTypes.main, nodeExecutionsById, currentNestedView: currentNestedView, - setShouldUpdate, }; return (
diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index 1283418d0..135f67c27 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import ReactFlow, { Background } from 'react-flow-renderer'; import { withNodeExecutionDynamicProvider } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; -import { isEqual } from 'lodash'; import { getPositionedNodes, ReactFlowIdHash } from './utils'; import { ReactFlowCustomEndNode, diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index f9649a4b4..8dfe44e80 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -246,7 +246,11 @@ export const ReactFlowGateNode = ({ data }: RFNode) => { scopedId, onNodeSelectionChanged, } = data; - const phase = getNodeFrontendPhase(nodeExecutionStatus, true); + const { componentProps, nodeExecution } = useNodeExecutionDynamicContext(); + const phase = getNodeFrontendPhase( + nodeExecution?.closure?.phase || nodeExecutionStatus, + true, + ); const styles = getGraphNodeStyle(nodeType, phase); const [showResumeForm, setShowResumeForm] = useState(false); @@ -275,7 +279,7 @@ export const ReactFlowGateNode = ({ data }: RFNode) => { }; return ( -
+
{text} {phase === NodeExecutionPhase.PAUSED && ( @@ -440,134 +444,141 @@ export const ReactFlowCustomTaskNode = ( * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ -export const ReactFlowSubWorkflowContainer = ({ data }: RFNode) => { - const { - nodeExecutionStatus, - text, - scopedId, - currentNestedView, - onRemoveNestedView, - } = data; - const BREAD_FONT_SIZE = '9px'; - const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; - const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color; - const borderStyle = getNestedContainerStyle(nodeExecutionStatus); - - const handleNestedViewClick = e => { - const index = e.target.id.substr( - e.target.id.indexOf('_') + 1, - e.target.id.length, - ); - onRemoveNestedView(scopedId, index); - }; - - const handleRootClick = () => { - onRemoveNestedView(scopedId, -1); - }; - - const currentNestedDepth = currentNestedView?.length || 0; - - const BreadElement = ({ nestedView, index }) => { - const liStyles: React.CSSProperties = { - cursor: 'pointer', - fontSize: BREAD_FONT_SIZE, - color: BREAD_COLOR_ACTIVE, +export const ReactFlowSubWorkflowContainer = React.forwardRef( + ({ data }: RFNode, ref) => { + const { + nodeExecutionStatus, + text, + scopedId, + currentNestedView, + onRemoveNestedView, + } = data; + const BREAD_FONT_SIZE = '9px'; + const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; + const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color; + const borderStyle = getNestedContainerStyle(nodeExecutionStatus); + const { componentProps } = useNodeExecutionDynamicContext(); + + const handleNestedViewClick = e => { + const index = e.target.id.substr( + e.target.id.indexOf('_') + 1, + e.target.id.length, + ); + onRemoveNestedView(scopedId, index); }; - const liStyleInactive: React.CSSProperties = { ...liStyles }; - liStyleInactive['color'] = BREAD_COLOR_INACTIVE; - - const beforeStyle: React.CSSProperties = { - cursor: 'pointer', - color: BREAD_COLOR_ACTIVE, - padding: '0 .2rem', - fontSize: BREAD_FONT_SIZE, + const handleRootClick = () => { + onRemoveNestedView(scopedId, -1); }; - const onClick = - currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; - return ( -
  • - {index === 0 ? {'>'} : null} - {nestedView} - {index < currentNestedDepth - 1 ? ( - {'>'} - ) : null} -
  • - ); - }; - const BorderElement = props => { - return
    {props.children}
    ; - }; - - const BorderContainer = props => { - let output = BorderElement(props); - for (let i = 0; i < currentNestedDepth; i++) { - output = {output}; - } - return output; - }; + const currentNestedDepth = currentNestedView?.length || 0; + + const BreadElement = ({ nestedView, index }) => { + const liStyles: React.CSSProperties = { + cursor: 'pointer', + fontSize: BREAD_FONT_SIZE, + color: BREAD_COLOR_ACTIVE, + }; + + const liStyleInactive: React.CSSProperties = { ...liStyles }; + liStyleInactive['color'] = BREAD_COLOR_INACTIVE; + + const beforeStyle: React.CSSProperties = { + cursor: 'pointer', + color: BREAD_COLOR_ACTIVE, + padding: '0 .2rem', + fontSize: BREAD_FONT_SIZE, + }; + const onClick = + currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; + return ( +
  • + {index === 0 ? {'>'} : null} + {nestedView} + {index < currentNestedDepth - 1 ? ( + {'>'} + ) : null} +
  • + ); + }; - const renderBreadCrumb = () => { - const breadContainerStyle: React.CSSProperties = { - position: 'absolute', - display: 'flex', - width: '100%', - marginTop: '-1rem', + const BorderElement = props => { + return ( +
    + {props.children} +
    + ); }; - const olStyles: React.CSSProperties = { - margin: 0, - padding: 0, - display: 'flex', - listStyle: 'none', - listStyleImage: 'none', - minWidth: '1rem', + + const BorderContainer = props => { + let output = BorderElement(props); + for (let i = 0; i < currentNestedDepth; i++) { + output = {output}; + } + return output; }; - const headerStyle: React.CSSProperties = { - color: BREAD_COLOR_ACTIVE, - fontSize: BREAD_FONT_SIZE, - margin: 0, - padding: 0, + + const renderBreadCrumb = () => { + const breadContainerStyle: React.CSSProperties = { + position: 'absolute', + display: 'flex', + width: '100%', + marginTop: '-1rem', + }; + const olStyles: React.CSSProperties = { + margin: 0, + padding: 0, + display: 'flex', + listStyle: 'none', + listStyleImage: 'none', + minWidth: '1rem', + }; + const headerStyle: React.CSSProperties = { + color: BREAD_COLOR_ACTIVE, + fontSize: BREAD_FONT_SIZE, + margin: 0, + padding: 0, + }; + + const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; + return ( +
    +
    + {text} +
    +
      + {currentNestedView?.map((nestedView, i) => { + return ( + + ); + })} +
    +
    + ); }; - const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; return ( -
    -
    - {text} -
    -
      - {currentNestedView?.map((nestedView, i) => { - return ( - - ); - })} -
    -
    + <> + {renderBreadCrumb()} + + {renderDefaultHandles( + scopedId, + getGraphHandleStyle('source'), + getGraphHandleStyle('target'), + )} + + ); - }; - - return ( - <> - {renderBreadCrumb()} - - {renderDefaultHandles( - scopedId, - getGraphHandleStyle('source'), - getGraphHandleStyle('target'), - )} - - - ); -}; + }, +); /** * Custom component renders start node diff --git a/packages/console/src/components/flytegraph/RenderedGraph.tsx b/packages/console/src/components/flytegraph/RenderedGraph.tsx index 8ff399ade..fc64c9a6e 100644 --- a/packages/console/src/components/flytegraph/RenderedGraph.tsx +++ b/packages/console/src/components/flytegraph/RenderedGraph.tsx @@ -33,10 +33,14 @@ export class RenderedGraph extends React.Component< private clearSelection = () => { // Don't need to trigger a re-render if selection is already empty - if (!this.props.selectedNodes || this.props.selectedNodes.length === 0) { + if ( + !this.props.selectedNodes || + this.props.selectedNodes.length === 0 || + !this.props.onNodeSelectionChanged + ) { return; } - this.props.onNodeSelectionChanged && this.props.onNodeSelectionChanged([]); + this.props.onNodeSelectionChanged([]); }; private onNodeEnter = (id: string) => { diff --git a/packages/console/src/components/hooks/useNodeExecution.ts b/packages/console/src/components/hooks/useNodeExecution.ts index b1394c4a3..21e33470e 100644 --- a/packages/console/src/components/hooks/useNodeExecution.ts +++ b/packages/console/src/components/hooks/useNodeExecution.ts @@ -31,7 +31,10 @@ export function useNodeExecutionData( { debugName: 'NodeExecutionData', defaultValue: {} as ExecutionData, - doFetch: id => getNodeExecutionData(id), + doFetch: id => + getNodeExecutionData(id).catch(e => { + return {} as ExecutionData; + }), }, id, ); From 1310cfe066c8f0cb0419c269f9ecf1ddf110b549 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 6 Apr 2023 10:53:41 -0700 Subject: [PATCH 12/25] chore: progress Signed-off-by: Carina Ursu --- .../NodeExecutionDynamicProvider.tsx | 33 ++- .../ReactFlow/ReactFlowGraphComponent.tsx | 38 ++- .../ReactFlow/customNodeComponents.tsx | 246 +++++++++--------- 3 files changed, 172 insertions(+), 145 deletions(-) diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index 7d814025d..faa55c308 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -6,6 +6,7 @@ import React, { useMemo, Ref, useState, + forwardRef, } from 'react'; import { dateToTimestamp } from 'common/utils'; import { @@ -17,7 +18,7 @@ import { isParentNode, nodeExecutionIsTerminal, } from 'components/Executions/utils'; -import { keyBy } from 'lodash'; +import { keyBy, values } from 'lodash'; import { NodeExecution, NodeExecutionPhase } from 'models'; import { dNode } from 'models/Graph/types'; @@ -100,6 +101,16 @@ export const NodeExecutionDynamicProvider = ({ return nodeExecutionsById[node.scopedId]; } + const splitScope = node.scopedId.split('-'); + const fromUniqueParentId = + splitScope.length > 2 + ? { + fromUniqueParentId: splitScope + .slice(0, splitScope.length - 2) + .join('-'), + } + : {}; + return { closure: { createdAt: dateToTimestamp(new Date()), @@ -112,9 +123,18 @@ export const NodeExecutionDynamicProvider = ({ }, inputUri: '', scopedId: node.scopedId, + ...fromUniqueParentId, }; }, [nodeExecutionsById, node]); + const childExecutions = useMemo(() => { + const children = values(nodeExecutionsById).filter(execution => { + return execution.fromUniqueParentId === node.scopedId; + }); + + return children; + }, [nodeExecutionsById]); + const { nodeExecutionRowQuery } = useNodeExecutionRow( queryClient, nodeExecution!, @@ -124,7 +144,7 @@ export const NodeExecutionDynamicProvider = ({ } const shouldRun = checkEnableChildQuery( - nodeExecutionList?.slice(1, nodeExecutionList.length - 1), + childExecutions, nodeExecution!, inView, ); @@ -141,10 +161,7 @@ export const NodeExecutionDynamicProvider = ({ const parentAndChildren = nodeExecutionRowQuery.data; - const executionChildren = parentAndChildren?.filter( - e => e.fromUniqueParentId === nodeExecution?.scopedId, - ); - const newChildCount = executionChildren.length; + const newChildCount = childExecutions?.length; // update parent context with tnew executions data const parentAndChildrenById = keyBy(parentAndChildren, 'scopedId'); setCurrentNodeExecutionsById(parentAndChildrenById, true); @@ -185,11 +202,11 @@ export const withNodeExecutionDynamicProvider = ( WrappedComponent: React.FC, context: string, ) => { - return (props: RFNode, ...rest: any) => { + return forwardRef((props: RFNode, ...rest: any) => { return ( ); - }; + }); }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 313bec239..24a5a5667 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -12,6 +12,7 @@ import { import { dNode } from 'models/Graph/types'; import { useQueryClient } from 'react-query'; import { extractCompiledNodes } from 'components/hooks/utils'; +import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground, isUnFetchedDynamicNode } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; @@ -34,23 +35,24 @@ export const ReactFlowGraphComponent = ({ selectedPhase, initialNodes, }) => { - const queryClient = useQueryClient(); - const { nodeExecutionsById, setCurrentNodeExecutionsById } = + const { nodeExecutionsById, shouldUpdate } = useNodeExecutionsById(); + const { isDetailsTabClosed } = useDetailsPanel(); + const { compiledWorkflowClosure } = useNodeExecutionContext(); const [pausedNodes, setPausedNodes] = useState([]); const [currentNestedView, setcurrentNestedView] = useState({}); const onAddNestedView = async (view, sourceNode: any = null) => { - if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { - await fetchChildrenExecutions( - queryClient, - sourceNode.scopedId, - nodeExecutionsById, - setCurrentNodeExecutionsById, - ); - } + // if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { + // await fetchChildrenExecutions( + // queryClient, + // sourceNode.scopedId, + // nodeExecutionsById, + // setCurrentNodeExecutionsById, + // ); + // } const currentView = currentNestedView[view.parent] || []; const newView = { @@ -71,7 +73,7 @@ export const ReactFlowGraphComponent = ({ }; const rfGraphJson = useMemo(() => { - const a = ConvertFlyteDagToReactFlows({ + return ConvertFlyteDagToReactFlows({ root: data, nodeExecutionsById, onNodeSelectionChanged, @@ -82,8 +84,18 @@ export const ReactFlowGraphComponent = ({ currentNestedView, maxRenderDepth: 1, } as ConvertDagProps); - return a; - }, [data, nodeExecutionsById, selectedPhase, currentNestedView]); + }, [ + data, + isDetailsTabClosed, + shouldUpdate, + nodeExecutionsById, + onNodeSelectionChanged, + onPhaseSelectionChanged, + selectedPhase, + onAddNestedView, + onRemoveNestedView, + currentNestedView, + ]); const backgroundStyle = getRFBackground().nested; diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 8dfe44e80..0542e892c 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -444,141 +444,139 @@ export const ReactFlowCustomTaskNode = ( * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ -export const ReactFlowSubWorkflowContainer = React.forwardRef( - ({ data }: RFNode, ref) => { - const { - nodeExecutionStatus, - text, - scopedId, - currentNestedView, - onRemoveNestedView, - } = data; - const BREAD_FONT_SIZE = '9px'; - const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; - const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color; - const borderStyle = getNestedContainerStyle(nodeExecutionStatus); - const { componentProps } = useNodeExecutionDynamicContext(); - - const handleNestedViewClick = e => { - const index = e.target.id.substr( - e.target.id.indexOf('_') + 1, - e.target.id.length, - ); - onRemoveNestedView(scopedId, index); - }; +export const ReactFlowSubWorkflowContainer = ({ data }: RFNode) => { + const { + nodeExecutionStatus, + text, + scopedId, + currentNestedView, + onRemoveNestedView, + } = data; + const BREAD_FONT_SIZE = '9px'; + const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; + const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color; + const borderStyle = getNestedContainerStyle(nodeExecutionStatus); + const { componentProps } = useNodeExecutionDynamicContext(); - const handleRootClick = () => { - onRemoveNestedView(scopedId, -1); - }; + const handleNestedViewClick = e => { + const index = e.target.id.substr( + e.target.id.indexOf('_') + 1, + e.target.id.length, + ); + onRemoveNestedView(scopedId, index); + }; - const currentNestedDepth = currentNestedView?.length || 0; - - const BreadElement = ({ nestedView, index }) => { - const liStyles: React.CSSProperties = { - cursor: 'pointer', - fontSize: BREAD_FONT_SIZE, - color: BREAD_COLOR_ACTIVE, - }; - - const liStyleInactive: React.CSSProperties = { ...liStyles }; - liStyleInactive['color'] = BREAD_COLOR_INACTIVE; - - const beforeStyle: React.CSSProperties = { - cursor: 'pointer', - color: BREAD_COLOR_ACTIVE, - padding: '0 .2rem', - fontSize: BREAD_FONT_SIZE, - }; - const onClick = - currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; - return ( -
  • - {index === 0 ? {'>'} : null} - {nestedView} - {index < currentNestedDepth - 1 ? ( - {'>'} - ) : null} -
  • - ); - }; + const handleRootClick = () => { + onRemoveNestedView(scopedId, -1); + }; - const BorderElement = props => { - return ( -
    - {props.children} -
    - ); + const currentNestedDepth = currentNestedView?.length || 0; + + const BreadElement = ({ nestedView, index }) => { + const liStyles: React.CSSProperties = { + cursor: 'pointer', + fontSize: BREAD_FONT_SIZE, + color: BREAD_COLOR_ACTIVE, }; - const BorderContainer = props => { - let output = BorderElement(props); - for (let i = 0; i < currentNestedDepth; i++) { - output = {output}; - } - return output; + const liStyleInactive: React.CSSProperties = { ...liStyles }; + liStyleInactive['color'] = BREAD_COLOR_INACTIVE; + + const beforeStyle: React.CSSProperties = { + cursor: 'pointer', + color: BREAD_COLOR_ACTIVE, + padding: '0 .2rem', + fontSize: BREAD_FONT_SIZE, }; + // const onClick = + // currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; + return ( +
  • + {index === 0 ? {'>'} : null} + {nestedView} + {index < currentNestedDepth - 1 ? ( + {'>'} + ) : null} +
  • + ); + }; + + const BorderElement = props => { + return ( +
    + {props.children} +
    + ); + }; - const renderBreadCrumb = () => { - const breadContainerStyle: React.CSSProperties = { - position: 'absolute', - display: 'flex', - width: '100%', - marginTop: '-1rem', - }; - const olStyles: React.CSSProperties = { - margin: 0, - padding: 0, - display: 'flex', - listStyle: 'none', - listStyleImage: 'none', - minWidth: '1rem', - }; - const headerStyle: React.CSSProperties = { - color: BREAD_COLOR_ACTIVE, - fontSize: BREAD_FONT_SIZE, - margin: 0, - padding: 0, - }; - - const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; - return ( -
    -
    - {text} -
    -
      - {currentNestedView?.map((nestedView, i) => { - return ( - - ); - })} -
    -
    - ); + const BorderContainer = props => { + let output = BorderElement(props); + for (let i = 0; i < currentNestedDepth; i++) { + output = {output}; + } + return output; + }; + + const renderBreadCrumb = () => { + const breadContainerStyle: React.CSSProperties = { + position: 'absolute', + display: 'flex', + width: '100%', + marginTop: '-1rem', + }; + const olStyles: React.CSSProperties = { + margin: 0, + padding: 0, + display: 'flex', + listStyle: 'none', + listStyleImage: 'none', + minWidth: '1rem', + }; + const headerStyle: React.CSSProperties = { + color: BREAD_COLOR_ACTIVE, + fontSize: BREAD_FONT_SIZE, + margin: 0, + padding: 0, }; + const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; return ( - <> - {renderBreadCrumb()} - - {renderDefaultHandles( - scopedId, - getGraphHandleStyle('source'), - getGraphHandleStyle('target'), - )} - - +
    +
    + {text} +
    +
      + {currentNestedView?.map((nestedView, i) => { + return ( + + ); + })} +
    +
    ); - }, -); + }; + + return ( + <> + {renderBreadCrumb()} + + {renderDefaultHandles( + scopedId, + getGraphHandleStyle('source'), + getGraphHandleStyle('target'), + )} + + + ); +}; /** * Custom component renders start node From a92a054966f9fc03bb1e522744bdb22487a2044c Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 10 Apr 2023 07:40:36 -0700 Subject: [PATCH 13/25] chore: fixes Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionDetails.tsx | 117 +-- .../ExecutionDetails/ExecutionNodeViews.tsx | 109 +-- .../ExecutionDetails/ExecutionTab.tsx | 157 +--- .../ExecutionDetails/ExecutionTabView.tsx | 40 + .../NodeExecutionDetailsPanelContent.tsx | 4 +- .../TaskExecutionNode.tsx | 3 +- .../Timeline/ExecutionTimelineContainer.tsx | 10 +- .../ExecutionDetails/Timeline/helpers.ts | 6 +- .../test/ExecutionTabContent.test.tsx | 6 +- .../ExecutionDetails/test/TaskNames.test.tsx | 6 +- .../useExecutionNodeViewsState.ts | 4 +- .../Executions/ExecutionFilters.tsx | 3 +- .../Executions/Tables/NodeExecutionsTable.tsx | 115 ++- .../components/Executions/Tables/styles.ts | 3 + .../Tables/test/NodeExecutionsTable.test.tsx | 6 +- .../NodeExecutionDetailsContextProvider.tsx | 45 +- .../NodeExecutionDynamicProvider.tsx | 29 +- .../NodeExecutionsByIdContextProvider.tsx | 132 --- .../WorkflowNodeExecutionsProvider.tsx | 227 ++++++ .../NodeExecutionDetails/index.tsx | 2 +- .../src/components/Executions/contexts.ts | 19 +- .../Executions/nodeExecutionQueries.ts | 55 +- .../Executions/useWorkflowExecution.ts | 5 +- .../LaunchForm/test/ResumeSignalForm.test.tsx | 6 +- .../WorkflowGraph/WorkflowGraph.tsx | 53 +- .../transformerWorkflowToDag.tsx | 764 +++++++++++------- .../src/components/common/LoadingSpinner.tsx | 8 + .../flytegraph/ReactFlow/BreadCrumb.tsx | 138 ++++ .../ReactFlow/ReactFlowBreadCrumbProvider.tsx | 97 +++ .../ReactFlow/ReactFlowGraphComponent.tsx | 91 +-- .../flytegraph/ReactFlow/ReactFlowWrapper.tsx | 22 +- .../ReactFlow/customNodeComponents.tsx | 245 +++--- .../test/PausedTasksComponent.test.tsx | 6 +- .../ReactFlow/transformDAGToReactFlowV2.tsx | 19 +- .../components/flytegraph/ReactFlow/types.ts | 7 +- .../console/src/components/hooks/utils.ts | 11 +- 36 files changed, 1441 insertions(+), 1129 deletions(-) create mode 100644 packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx delete mode 100644 packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx create mode 100644 packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx create mode 100644 packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx create mode 100644 packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx index 2061dcdf8..d7b31dffb 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; +import { useContext } from 'react'; import { Collapse, IconButton } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import ExpandMore from '@material-ui/icons/ExpandMore'; @@ -6,16 +8,17 @@ import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { WaitForQuery } from 'components/common/WaitForQuery'; import { withRouteParams } from 'components/common/withRouteParams'; import { DataError } from 'components/Errors/DataError'; -import { isEqual } from 'lodash'; import { Execution } from 'models/Execution/types'; -import * as React from 'react'; -import { useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; +import { useQuery, useQueryClient } from 'react-query'; +import { Workflow } from 'models/Workflow/types'; +import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; import { ExecutionContext } from '../contexts'; import { useWorkflowExecutionQuery } from '../useWorkflowExecution'; import { ExecutionDetailsAppBarContent } from './ExecutionDetailsAppBarContent'; import { ExecutionMetadata } from './ExecutionMetadata'; import { ExecutionNodeViews } from './ExecutionNodeViews'; +import { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails'; const useStyles = makeStyles((theme: Theme) => ({ expandCollapseButton: { @@ -40,63 +43,60 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -export interface ExecutionDetailsRouteParams { - domainId: string; - executionId: string; - projectId: string; -} -export type ExecutionDetailsProps = ExecutionDetailsRouteParams; - -interface RenderExecutionDetailsProps { - execution: Execution; -} - -const RenderExecutionDetails: React.FC = ({ - execution, -}) => { +const RenderExecutionContainer: React.FC<{}> = () => { const styles = useStyles(); const [metadataExpanded, setMetadataExpanded] = React.useState(true); const toggleMetadata = () => setMetadataExpanded(!metadataExpanded); - const [localExecution, setLocalExecution] = useState(); + const { execution } = useContext(ExecutionContext); - React.useEffect(() => { - setLocalExecution(prev => { - if (isEqual(prev, execution)) { - return prev; - } - - return execution; - }); - }, [execution]); - const contextValue = { - execution, - }; + const { + closure: { workflowId }, + } = execution; + const workflowQuery = useQuery( + makeWorkflowQuery(useQueryClient(), workflowId), + ); return ( - - -
    - - - -
    - - - -
    -
    - -
    + <> + {/* Fetches the current workflow to build the execution tree inside NodeExecutionDetailsContextProvider */} + + {workflow => ( + <> + {/* Provides a node execution tree for the current workflow */} + + +
    + + + +
    + + + +
    +
    + + +
    + + )} +
    + ); }; +export interface ExecutionDetailsRouteParams { + domainId: string; + executionId: string; + projectId: string; +} /** The view component for the Execution Details page */ -export const ExecutionDetailsContainer: React.FC = ({ +export const ExecutionDetailsWrapper: React.FC = ({ executionId, domainId, projectId, @@ -107,21 +107,28 @@ export const ExecutionDetailsContainer: React.FC = ({ name: executionId, }; - const renderExecutionDetails = (execution: Execution) => ( - - ); + const workflowExecutionQuery = useWorkflowExecutionQuery(id); return ( + // get the workflow execution query to get the current workflow id - {renderExecutionDetails} + {(execution: Execution) => ( + + + + )} ); }; export const ExecutionDetails: React.FunctionComponent< RouteComponentProps -> = withRouteParams(ExecutionDetailsContainer); +> = withRouteParams(ExecutionDetailsWrapper); diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 0c9c4e342..91c379b59 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -1,109 +1,32 @@ import React, { useContext } from 'react'; -import { Tab, Tabs } from '@material-ui/core'; -import { makeStyles, Theme } from '@material-ui/core/styles'; -import { useTabState } from 'components/hooks/useTabState'; -import { secondaryBackgroundColor } from 'components/Theme/constants'; import { WaitForQuery } from 'components/common'; import { DataError } from 'components/Errors/DataError'; -import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; -import { useQuery, useQueryClient } from 'react-query'; -import { Workflow } from 'models/Workflow/types'; -import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; -import { - NodeExecutionDetailsContextProvider, - NodeExecutionsByIdContextProvider, -} from '../contextProvider/NodeExecutionDetails'; +import { LargeLoadingComponent } from 'components/common/LoadingSpinner'; import { ExecutionContext } from '../contexts'; -import { ExecutionFilters } from '../ExecutionFilters'; -import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; -import { tabs } from './constants'; -import { ExecutionTab } from './ExecutionTab'; -import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; - -const useStyles = makeStyles((theme: Theme) => ({ - filters: { - paddingLeft: theme.spacing(3), - }, - nodesContainer: { - borderTop: `1px solid ${theme.palette.divider}`, - display: 'flex', - flex: '1 1 100%', - flexDirection: 'column', - minHeight: 0, - }, - tabs: { - background: secondaryBackgroundColor, - paddingLeft: theme.spacing(3.5), - }, -})); +import { useExecutionNodeViewsStatePoll } from './useExecutionNodeViewsState'; +import { ExecutionTabView } from './ExecutionTabView'; +import { WorkflowNodeExecutionsProvider } from '../contextProvider/NodeExecutionDetails'; /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionNodeViews: React.FC = () => { - const defaultTab = tabs.nodes.id; - const styles = useStyles(); - const filterState = useNodeExecutionFiltersState(); - const tabState = useTabState(tabs, defaultTab); +export const ExecutionNodeViews: React.FC<{}> = () => { const { execution } = useContext(ExecutionContext); - const { - closure: { workflowId }, - } = execution; - const workflowQuery = useQuery( - makeWorkflowQuery(useQueryClient(), workflowId), - ); // query to get all data to build Graph and Timeline - const { nodeExecutionsQuery } = useExecutionNodeViewsState(execution); - // query to get filtered data to narrow down Table outputs - const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = - useExecutionNodeViewsState(execution, filterState?.appliedFilters); + const { nodeExecutionsQuery } = useExecutionNodeViewsStatePoll(execution); return ( <> - - - - - - - - {() => - filterState ? ( - - -
    - {tabState.value === tabs.nodes.id && ( -
    - -
    - )} - - - {() => } - -
    -
    -
    - ) : ( - - ) - } + + {data => ( + + + + )} ); }; - -const LoadingComponent = () => { - return ( -
    - -
    - ); -}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 63cb89db3..ae4174519 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -1,152 +1,43 @@ import * as React from 'react'; -import { dNode } from 'models/Graph/types'; -import { isEqual, merge } from 'lodash'; -import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; -import { useEffect, useState } from 'react'; -import { checkForDynamicExecutions } from 'components/common/utils'; -import { useQuery } from 'react-query'; -import { - makeNodeExecutionDynamicWorkflowQuery, - NodeExecutionDynamicWorkflowQueryResult, -} from 'components/Workflow/workflowQueries'; import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; -import { convertToPlainNodes } from './Timeline/helpers'; +import { Theme, makeStyles } from '@material-ui/core/styles'; import { tabs } from './constants'; import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { DetailsPanelContextProvider } from './DetailsPanelContext'; import { ScaleProvider } from './Timeline/scaleContext'; -import { - useNodeExecutionContext, - useNodeExecutionsById, -} from '../contextProvider/NodeExecutionDetails'; import { ExecutionTimelineContainer } from './Timeline/ExecutionTimelineContainer'; +import { IWorkflowNodeExecutionsContext } from '../contexts'; + +const useStyles = makeStyles((theme: Theme) => ({ + nodesContainer: { + borderTop: `1px solid ${theme.palette.divider}`, + display: 'flex', + flex: '1 1 100%', + flexDirection: 'column', + minHeight: 0, + }, +})); interface ExecutionTabProps { + executionsContext: IWorkflowNodeExecutionsContext; tabType: string; } /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionTab: React.FC = ({ tabType }) => { - const { compiledWorkflowClosure } = useNodeExecutionContext(); - const { nodeExecutionsById, setShouldUpdate, shouldUpdate } = - useNodeExecutionsById(); - const { staticExecutionIdsMap } = compiledWorkflowClosure - ? transformerWorkflowToDag(compiledWorkflowClosure) - : { staticExecutionIdsMap: {} }; - const [dynamicParents, setDynamicParents] = useState( - checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap), - ); - const [dynamicWorkflows, setDynamicWorkflows] = - useState(); - const { data: tempDynamicWorkflows, isFetching: isFetchingDynamicWorkflows } = - useQuery(makeNodeExecutionDynamicWorkflowQuery(dynamicParents)); - - const [initialNodes, setInitialNodes] = useState([]); - const [dagError, setDagError] = useState(null); - const [mergedDag, setMergedDag] = useState(null); - - useEffect(() => { - if (isFetchingDynamicWorkflows) { - return; - } - setDynamicWorkflows(prev => { - const newDynamicWorkflows = merge( - { ...(prev || {}) }, - tempDynamicWorkflows, - ); - if (isEqual(prev, newDynamicWorkflows)) { - return prev; - } - - return newDynamicWorkflows; - }); - }, [tempDynamicWorkflows]); - - useEffect(() => { - if (shouldUpdate) { - const newDynamicParents = checkForDynamicExecutions( - nodeExecutionsById, - staticExecutionIdsMap, - ); - setDynamicParents(prev => { - if (isEqual(prev, newDynamicParents)) { - return prev; - } - - return newDynamicParents; - }); - setShouldUpdate(false); - } - }, [shouldUpdate]); - - useEffect(() => { - const { dag, staticExecutionIdsMap, error } = compiledWorkflowClosure - ? transformerWorkflowToDag( - compiledWorkflowClosure, - dynamicWorkflows, - nodeExecutionsById, - ) - : { dag: {}, staticExecutionIdsMap: {}, error: null }; - - const nodes = dag.nodes ?? []; - - let newMergedDag = dag; - - for (const dynamicId in dynamicWorkflows) { - if (staticExecutionIdsMap[dynamicId]) { - if (compiledWorkflowClosure) { - const dynamicWorkflow = transformerWorkflowToDag( - compiledWorkflowClosure, - dynamicWorkflows, - nodeExecutionsById, - ); - newMergedDag = dynamicWorkflow.dag; - } - } - } - setDagError(error); - setMergedDag(prev => { - if (isEqual(prev, newMergedDag)) { - return prev; - } - return newMergedDag; - }); - - // we remove start/end node info in the root dNode list during first assignment - const plainNodes = convertToPlainNodes(nodes); - plainNodes.map(node => { - const initialNode = initialNodes.find(n => n.scopedId === node.scopedId); - if (initialNode) { - node.expanded = initialNode.expanded; - } - }); - setInitialNodes(prev => { - if (isEqual(prev, plainNodes)) { - return prev; - } - return plainNodes; - }); - }, [ - compiledWorkflowClosure, - dynamicWorkflows, - dynamicParents, - nodeExecutionsById, - ]); +export const ExecutionTab: React.FC = ({ + tabType, + executionsContext, +}) => { + const styles = useStyles(); - const renderContent = () => { + const renderContent = (executionsContext: IWorkflowNodeExecutionsContext) => { switch (tabType) { case tabs.nodes.id: - return ; + return ; case tabs.graph.id: - return ( - - ); + return ; case tabs.timeline.id: - return ; + return ; default: return null; } @@ -155,7 +46,9 @@ export const ExecutionTab: React.FC = ({ tabType }) => { return ( - {renderContent()} +
    + {renderContent(executionsContext)} +
    {/* Side panel, shows information for specific node */}
    diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx new file mode 100644 index 000000000..32d00cc50 --- /dev/null +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Tab, Tabs } from '@material-ui/core'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import { useTabState } from 'components/hooks/useTabState'; +import { secondaryBackgroundColor } from 'components/Theme/constants'; +import { tabs } from './constants'; +import { ExecutionTab } from './ExecutionTab'; +import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; + +const useStyles = makeStyles((theme: Theme) => ({ + tabs: { + background: secondaryBackgroundColor, + paddingLeft: theme.spacing(3.5), + }, +})); + +const DEFAULT_TAB = tabs.nodes.id; + +/** Contains the available ways to visualize the nodes of a WorkflowExecution */ +export const ExecutionTabView: React.FC<{}> = () => { + const styles = useStyles(); + const tabState = useTabState(tabs, DEFAULT_TAB); + + const executionsContext = useNodeExecutionsById(); + + return ( + <> + + + + + + + + + ); +}; diff --git a/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index a6f23cb7d..d8fe5d3b1 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -54,7 +54,7 @@ import { ExecutionDetailsActions } from './ExecutionDetailsActions'; import { getNodeFrontendPhase, isNodeGateNode } from '../utils'; import { fetchTaskExecutionList } from '../taskExecutionQueries'; import { getGroupedLogs } from '../TaskExecutionsList/utils'; -import { NodeExecutionsByIdContext } from '../contexts'; +import { WorkflowNodeExecutionsContext } from '../contexts'; const useStyles = makeStyles((theme: Theme) => { const paddingVertical = `${theme.spacing(2)}px`; @@ -262,7 +262,7 @@ export const NodeExecutionDetailsPanelContent: React.FC< const { getNodeExecutionDetails, compiledWorkflowClosure } = useNodeExecutionContext(); const { nodeExecutionsById, setCurrentNodeExecutionsById } = useContext( - NodeExecutionsByIdContext, + WorkflowNodeExecutionsContext, ); const isGateNode = isNodeGateNode( extractCompiledNodes(compiledWorkflowClosure), diff --git a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx index 14851c26a..b6fadc3f6 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx @@ -4,8 +4,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 React, { useContext } from 'react'; -import { NodeExecutionsByIdContext } from '../../contexts'; +import React from 'react'; import { StatusIndicator } from './StatusIndicator'; /** Renders DAGNodes with colors based on their node type, as well as dots to diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx index 99e509ff6..efcd92e00 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimelineContainer.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { useState } from 'react'; import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { dNode } from 'models/Graph/types'; import { ExecutionTimeline } from './ExecutionTimeline'; import { ExecutionTimelineFooter } from './ExecutionTimelineFooter'; import { TimeZone } from './helpers'; @@ -20,15 +19,12 @@ const useStyles = makeStyles(() => ({ }, })); -export interface ExecutionTimelineContainerProps { - initialNodes: dNode[]; -} -export const ExecutionTimelineContainer: React.FC< - ExecutionTimelineContainerProps -> = ({ initialNodes }) => { +export const ExecutionTimelineContainer: React.FC<{}> = () => { const styles = useStyles(); const [chartTimezone, setChartTimezone] = useState(TimeZone.Local); const handleTimezoneChange = tz => setChartTimezone(tz); + + const { initialDNodes: initialNodes } = useNodeExecutionsById(); return (
    diff --git a/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts b/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts index c45449c60..53bbb6c97 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/Timeline/helpers.ts @@ -1,4 +1,4 @@ -import { endNodeId, startNodeId } from 'models/Node/constants'; +import { ignoredNodeIds } from 'models/Node/constants'; import { dNode } from 'models/Graph/types'; import { isExpanded } from 'models/Node/utils'; @@ -8,8 +8,8 @@ export const TimeZone = { }; export function isTransitionNode(node: dNode) { - // In case of bracnhNode childs, start and end nodes could be present as 'n0-start-node' etc. - return node?.id?.includes(startNodeId) || node?.id?.includes(endNodeId); + // In case of branchNode childs, start and end nodes could be present as 'n0-start-node' etc. + return ignoredNodeIds.includes(node?.id); } export function convertToPlainNodes(nodes: dNode[], level = 0): dNode[] { diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx index a6d8c73fd..6877e51d3 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx @@ -1,6 +1,6 @@ import { render, waitFor } from '@testing-library/react'; import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { mockWorkflowId } from 'mocks/data/fixtures/types'; import { insertFixture } from 'mocks/data/insertFixture'; @@ -64,12 +64,12 @@ describe('Executions > ExecutionDetails > ExecutionTabContent', () => { return render( - + - + , ); 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 7a8807b39..de85fb80f 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx @@ -3,7 +3,7 @@ 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 { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import { mockWorkflowId } from 'mocks/data/fixtures/types'; import { createTestQueryClient } from 'test/utils'; import { dateToTimestamp } from 'common/utils'; @@ -74,14 +74,14 @@ describe('ExecutionDetails > Timeline > TaskNames', () => { return render( - {}, }} > - + , ); diff --git a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts index f3b177d97..b913e1936 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useExecutionNodeViewsState.ts @@ -1,10 +1,8 @@ import { useConditionalQuery } from 'components/hooks/useConditionalQuery'; -import { isEqual } from 'lodash'; import { limits } from 'models/AdminEntity/constants'; import { FilterOperation, SortDirection } from 'models/AdminEntity/types'; import { executionSortFields } from 'models/Execution/constants'; import { Execution, NodeExecution } from 'models/Execution/types'; -import { useEffect, useState } from 'react'; import { useQueryClient, UseQueryResult } from 'react-query'; import { executionRefreshIntervalMs } from '../constants'; import { makeNodeExecutionListQuery } from '../nodeExecutionQueries'; @@ -26,7 +24,7 @@ const sort = { direction: SortDirection.ASCENDING, }; -export function useExecutionNodeViewsState( +export function useExecutionNodeViewsStatePoll( execution: Execution, filter: FilterOperation[] = [], ): UseExecutionNodeViewsState { diff --git a/packages/console/src/components/Executions/ExecutionFilters.tsx b/packages/console/src/components/Executions/ExecutionFilters.tsx index b9e08894b..a2ae877fe 100644 --- a/packages/console/src/components/Executions/ExecutionFilters.tsx +++ b/packages/console/src/components/Executions/ExecutionFilters.tsx @@ -116,7 +116,6 @@ export const ExecutionFilters: React.FC = ({ /> ); } - const renderContent = () => ; return ( = ({ onReset={filter.onReset} buttonText={filter.label} className={styles.filterButton} - renderContent={renderContent} + renderContent={() => } /> ); })} diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 6d653250d..4041cb40f 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -1,8 +1,8 @@ +import React, { useMemo, useEffect, useState, useContext } from 'react'; import classnames from 'classnames'; import { useCommonStyles } from 'components/common/styles'; import scrollbarSize from 'dom-helpers/scrollbarSize'; import { NodeExecution, NodeExecutionsById } from 'models/Execution/types'; -import React, { useMemo, useEffect, useState } from 'react'; import { merge, isEqual, cloneDeep } from 'lodash'; import { extractCompiledNodes } from 'components/hooks/utils'; import { @@ -25,6 +25,9 @@ import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersStat import { searchNode } from '../utils'; import { nodeExecutionPhaseConstants } from '../constants'; import { NodeExecutionDynamicProvider } from '../contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; +import { ExecutionFilters } from '../ExecutionFilters'; +import { ExecutionContext, FilteredNodeExecutions } from '../contexts'; +import { useExecutionNodeViewsStatePoll } from '../ExecutionDetails/useExecutionNodeViewsState'; const scrollbarPadding = scrollbarSize(); @@ -50,11 +53,6 @@ const mergeOriginIntoNodes = (target: dNode[], origin: dNode[]) => { return newTarget; }; -interface NodeExecutionsTableProps { - initialNodes: dNode[]; - filteredNodes?: dNode[]; -} - const executionMatchesPhaseFilter = ( nodeExecution: NodeExecution, { key, value, operation }: FilterOperation, @@ -104,18 +102,30 @@ const filterNodes = ( return initialClone; }; +const isPhaseFilter = (appliedFilters: FilterOperation[]) => { + if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { + return true; + } + return false; +}; + /** Renders a table of NodeExecution records. Executions with errors will * have an expanadable container rendered as part of the table row. * NodeExecutions are expandable and will potentially render a list of child * TaskExecutions */ -export const NodeExecutionsTable: React.FC = ({ - initialNodes, -}) => { +export const NodeExecutionsTable: React.FC<{}> = () => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); - const { nodeExecutionsById, filteredNodeExecutions } = + const { execution } = useContext(ExecutionContext); + + const filterState = useNodeExecutionFiltersState(); + const { nodeExecutionsById, initialDNodes: initialNodes } = useNodeExecutionsById(); + + // query to get filtered data to narrow down Table outputs + const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = + useExecutionNodeViewsStatePoll(execution, filterState?.appliedFilters); const { appliedFilters } = useNodeExecutionFiltersState(); const [showNodes, setShowNodes] = useState([]); @@ -143,6 +153,27 @@ export const NodeExecutionsTable: React.FC = ({ [columnStyles, compiledNodes], ); + const [filteredNodeExecutions, setFilteredNodeExecutions] = + useState(); + + useEffect(() => { + if (filteredNodeExecutionsQuery.isFetching) { + return; + } + + const newFilteredNodeExecutions = isPhaseFilter(filterState.appliedFilters) + ? undefined + : filteredNodeExecutionsQuery.data; + + setFilteredNodeExecutions(prev => { + if (isEqual(prev, newFilteredNodeExecutions)) { + return prev; + } + + return newFilteredNodeExecutions; + }); + }, [filteredNodeExecutionsQuery]); + useEffect(() => { const plainNodes = convertToPlainNodes(originalNodes || []); setOriginalNodes(ogn => { @@ -208,36 +239,44 @@ export const NodeExecutionsTable: React.FC = ({ }; return ( -
    - -
    - {showNodes.length > 0 ? ( - showNodes.map(node => { - return ( - - +
    + +
    +
    + +
    + {showNodes.length > 0 ? ( + showNodes.map(node => { + return ( + - - ); - }) - ) : ( - - )} + > + + + ); + }) + ) : ( + + )} +
    -
    + ); }; diff --git a/packages/console/src/components/Executions/Tables/styles.ts b/packages/console/src/components/Executions/Tables/styles.ts index 65c426fe8..19543adbd 100644 --- a/packages/console/src/components/Executions/Tables/styles.ts +++ b/packages/console/src/components/Executions/Tables/styles.ts @@ -19,6 +19,9 @@ export const grayedClassName = 'grayed'; // the columns styles in some cases. So the column styles should be defined // last. export const useExecutionTableStyles = makeStyles((theme: Theme) => ({ + filters: { + paddingLeft: theme.spacing(3), + }, [grayedClassName]: { color: theme.palette.grey[300], }, 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 8460dc5e1..6b14449c8 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -1,6 +1,6 @@ import { render, waitFor } from '@testing-library/react'; import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { noExecutionsFoundString } from 'common/constants'; import { mockWorkflowId } from 'mocks/data/fixtures/types'; @@ -91,7 +91,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { render( - {}, @@ -101,7 +101,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { initialNodes={initialNodes} filteredNodes={filteredNodes} /> - + , ); diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx index 63497f8c1..46b9d8545 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx @@ -1,4 +1,5 @@ import React, { + PropsWithChildren, createContext, useContext, useEffect, @@ -8,7 +9,7 @@ import React, { import { log } from 'common/log'; import { Identifier } from 'models/Common/types'; import { NodeExecution } from 'models/Execution/types'; -import { CompiledWorkflowClosure } from 'models/Workflow/types'; +import { CompiledWorkflowClosure, Workflow } from 'models/Workflow/types'; import { useQueryClient } from 'react-query'; import { fetchWorkflow } from 'components/Workflow/workflowQueries'; import { NodeExecutionDetails } from '../../types'; @@ -57,24 +58,30 @@ export const useNodeExecutionDetails = (nodeExecution?: NodeExecution) => export const useNodeExecutionContext = (): NodeExecutionDetailsState => useContext(NodeExecutionDetailsContext); -interface ProviderProps { - workflowId: Identifier; - children?: React.ReactNode; -} +export type ProviderProps = PropsWithChildren<{ + workflow: Workflow; +}>; /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ -export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { +export const NodeExecutionDetailsContextProvider = ({ + workflow, + children, +}: ProviderProps) => { // workflow Identifier - separated to parameters, to minimize re-render count // as useEffect doesn't know how to do deep comparison - const { resourceType, project, domain, name, version } = props.workflowId; + const { resourceType, project, domain, name, version } = workflow.id; - const [executionTree, setExecutionTree] = - useState(null); + const [executionTree, setExecutionTree] = useState( + {} as CurrentExecutionDetails, + ); const [tasks, setTasks] = useState(new Map()); - const [closure, setClosure] = useState(null); + const [closure, setClosure] = useState( + {} as CompiledWorkflowClosure, + ); const resetState = () => { - setExecutionTree(null); + setExecutionTree({} as CurrentExecutionDetails); + setClosure({} as CompiledWorkflowClosure); }; const queryClient = useQueryClient(); @@ -101,10 +108,10 @@ export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { resetState(); return; } - const workflow = JSON.parse(JSON.stringify(result)); - const tree = createExecutionDetails(workflow); + const fetchedWorkflow = JSON.parse(JSON.stringify(result)); + const tree = createExecutionDetails(fetchedWorkflow); if (isCurrent) { - setClosure(workflow.closure?.compiledWorkflow ?? null); + setClosure(fetchedWorkflow.closure?.compiledWorkflow ?? null); setExecutionTree(tree); } } @@ -166,11 +173,17 @@ export const NodeExecutionDetailsContextProvider = (props: ProviderProps) => { - {props.children} + {children} ); }; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index faa55c308..55cf2869c 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -6,7 +6,6 @@ import React, { useMemo, Ref, useState, - forwardRef, } from 'react'; import { dateToTimestamp } from 'common/utils'; import { @@ -21,16 +20,16 @@ import { import { keyBy, values } from 'lodash'; import { NodeExecution, NodeExecutionPhase } from 'models'; import { dNode } from 'models/Graph/types'; - import { useInView } from 'react-intersection-observer'; import { useQueryClient } from 'react-query'; -import { RFNode } from 'components/flytegraph/ReactFlow/types'; -import { useNodeExecutionsById } from './NodeExecutionsByIdContextProvider'; +import { useNodeExecutionsById } from './WorkflowNodeExecutionsProvider'; export type RefType = Ref; export interface INodeExecutionDynamicContext { + context: string; node: dNode; nodeExecution: WorkflowNodeExecution; + childExecutions: WorkflowNodeExecution[]; childCount: number; inView: boolean; componentProps: React.DetailedHTMLProps< @@ -42,8 +41,10 @@ export interface INodeExecutionDynamicContext { export const NodeExecutionDynamicContext = createContext({ + context: 'none', node: {} as dNode, nodeExecution: undefined as any, + childExecutions: [], childCount: 0, inView: false, componentProps: { @@ -89,8 +90,8 @@ export const NodeExecutionDynamicProvider = ({ const queryClient = useQueryClient(); const { ref, inView } = useInView(); const { execution } = useContext(ExecutionContext); - const [fetchedChildCount, setFetchedChildCount] = useState(0); + // get running data const { setCurrentNodeExecutionsById, nodeExecutionsById } = useNodeExecutionsById(); @@ -161,11 +162,12 @@ export const NodeExecutionDynamicProvider = ({ const parentAndChildren = nodeExecutionRowQuery.data; - const newChildCount = childExecutions?.length; // update parent context with tnew executions data const parentAndChildrenById = keyBy(parentAndChildren, 'scopedId'); setCurrentNodeExecutionsById(parentAndChildrenById, true); + const newChildCount = (parentAndChildren?.length || 1) - 1; + // update known children count setFetchedChildCount(prev => { if (prev === newChildCount) { @@ -179,9 +181,11 @@ export const NodeExecutionDynamicProvider = ({ { return useContext(NodeExecutionDynamicContext); }; - -export const withNodeExecutionDynamicProvider = ( - WrappedComponent: React.FC, - context: string, -) => { - return forwardRef((props: RFNode, ...rest: any) => { - return ( - - - - ); - }); -}; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx deleted file mode 100644 index 9633c5ab2..000000000 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionsByIdContextProvider.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, { - PropsWithChildren, - useCallback, - useContext, - useEffect, - useState, -} from 'react'; -import { NodeExecution, NodeExecutionsById } from 'models/Execution/types'; -import { ExecutionFiltersState } from 'components/Executions/filters/useExecutionFiltersState'; -import { - FilteredNodeExecutions, - INodeExecutionsByIdContext, - NodeExecutionsByIdContext, -} from 'components/Executions/contexts'; -import { isEqual, keyBy, mergeWith } from 'lodash'; -import { FilterOperation } from 'models'; -import { UseQueryResult } from 'react-query'; -import { mapStringifyReplacer, mergeNodeExecutions } from './utils'; - -const isPhaseFilter = (appliedFilters: FilterOperation[]) => { - if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { - return true; - } - return false; -}; - -export type NodeExecutionsByIdContextProviderProps = PropsWithChildren<{ - initialNodeExecutionsById?: NodeExecutionsById; - filterState: ExecutionFiltersState; - nodeExecutionsQuery: UseQueryResult; - filteredNodeExecutionsQuery: UseQueryResult; -}>; - -/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ -export const NodeExecutionsByIdContextProvider = ({ - filterState, - nodeExecutionsQuery, - filteredNodeExecutionsQuery, - children, -}: NodeExecutionsByIdContextProviderProps) => { - const [shouldUpdate, setShouldUpdate] = useState(false); - - const [nodeExecutionsById, setNodeExecutionsById] = - useState(); - - const [filteredNodeExecutions, setFilteredNodeExecutions] = - useState(); - - useEffect(() => { - if (nodeExecutionsQuery.isFetching || !nodeExecutionsQuery.data) { - return; - } - const fetchedNodeExecutionsById = keyBy( - nodeExecutionsQuery.data, - 'scopedId', - ); - - setCurrentNodeExecutionsById(fetchedNodeExecutionsById); - }, [nodeExecutionsQuery]); - - useEffect(() => { - if (filteredNodeExecutionsQuery.isFetching) { - return; - } - - const newFilteredNodeExecutions = isPhaseFilter(filterState.appliedFilters) - ? undefined - : filteredNodeExecutionsQuery.data; - - setFilteredNodeExecutions(prev => { - if (isEqual(prev, newFilteredNodeExecutions)) { - return prev; - } - - setShouldUpdate(true); - return newFilteredNodeExecutions; - }); - }, [filteredNodeExecutionsQuery]); - - const setCurrentNodeExecutionsById = useCallback( - ( - newNodeExecutionsById: NodeExecutionsById, - checkForDynamicParents?: boolean, - ): void => { - setNodeExecutionsById(prev => { - const newNodes = mergeWith( - { ...prev }, - newNodeExecutionsById, - mergeNodeExecutions, - ); - if ( - JSON.stringify(prev, mapStringifyReplacer) === - JSON.stringify(newNodes, mapStringifyReplacer) - ) { - return prev; - } - - if (checkForDynamicParents) { - setShouldUpdate(true); - } - - console.log({ - oldCount: Object.keys(prev || {}).length, - newCount: Object.keys(newNodes).length, - old: prev, - new: newNodes, - }); - - return newNodes; - }); - }, - [], - ); - - return ( - - {children} - - ); -}; - -export const useNodeExecutionsById = (): INodeExecutionsByIdContext => { - return useContext(NodeExecutionsByIdContext); -}; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx new file mode 100644 index 000000000..5bc6073bf --- /dev/null +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx @@ -0,0 +1,227 @@ +import React, { + PropsWithChildren, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; +import { NodeExecution, NodeExecutionsById } from 'models/Execution/types'; +import { + IWorkflowNodeExecutionsContext, + WorkflowNodeExecutionsContext, +} from 'components/Executions/contexts'; +import { isEqual, keyBy, merge, mergeWith } from 'lodash'; +import { dNode } from 'models/Graph/types'; +import { + NodeExecutionDynamicWorkflowQueryResult, + makeNodeExecutionDynamicWorkflowQuery, +} from 'components/Workflow/workflowQueries'; +import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; +import { checkForDynamicExecutions } from 'components/common/utils'; +import { useQuery } from 'react-query'; +import { convertToPlainNodes } from 'components/Executions/ExecutionDetails/Timeline/helpers'; +import { useNodeExecutionContext } from './NodeExecutionDetailsContextProvider'; +import { mapStringifyReplacer, mergeNodeExecutions } from './utils'; + +export type WorkflowNodeExecutionsProviderProps = PropsWithChildren<{ + initialNodeExecutions?: NodeExecution[]; +}>; + +/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ +export const WorkflowNodeExecutionsProvider = ({ + initialNodeExecutions, + children, +}: WorkflowNodeExecutionsProviderProps) => { + const [shouldUpdate, setShouldUpdate] = useState(false); + const { compiledWorkflowClosure } = useNodeExecutionContext(); + + const [nodeExecutionsById, setNodeExecutionsById] = + useState({}); + + const [dagError, setDagError] = useState(null); + const [mergedDag, setMergedDag] = useState({}); + const [initialDNodes, setInitialDNodes] = useState([]); + + const [dynamicWorkflows, setDynamicWorkflows] = + useState({}); + const [staticExecutionIdsMap, setstaticExecutionIdsMap] = useState({}); + + const [dynamicParents, setDynamicParents] = useState({}); + + const nodeExecutionDynamicWorkflowQuery = useQuery( + makeNodeExecutionDynamicWorkflowQuery(dynamicParents), + ); + + useEffect(() => { + const initialNodeExecutionsById = keyBy(initialNodeExecutions, 'scopedId'); + + setCurrentNodeExecutionsById(initialNodeExecutionsById, true); + }, [initialNodeExecutions]); + + useEffect(() => { + const { staticExecutionIdsMap: newstaticExecutionIdsMap } = + compiledWorkflowClosure + ? transformerWorkflowToDag(compiledWorkflowClosure) + : { staticExecutionIdsMap: {} }; + + setstaticExecutionIdsMap(prev => { + if (isEqual(prev, newstaticExecutionIdsMap)) { + return prev; + } + + return newstaticExecutionIdsMap; + }); + }, [compiledWorkflowClosure]); + + useEffect(() => { + const newdynamicParents = checkForDynamicExecutions( + nodeExecutionsById, + staticExecutionIdsMap, + ); + setDynamicParents(prev => { + if (isEqual(prev, newdynamicParents)) { + return prev; + } + + return newdynamicParents; + }); + }, [nodeExecutionsById]); + + useEffect(() => { + const dagData = compiledWorkflowClosure + ? transformerWorkflowToDag( + compiledWorkflowClosure, + dynamicWorkflows, + nodeExecutionsById, + ) + : { dag: {}, staticExecutionIdsMap: {}, error: null }; + const { dag, staticExecutionIdsMap, error } = dagData; + const nodes = dag.nodes ?? []; + + let newMergedDag = dag; + + for (const dynamicId in dynamicWorkflows) { + if (staticExecutionIdsMap[dynamicId]) { + if (compiledWorkflowClosure) { + const dynamicWorkflow = transformerWorkflowToDag( + compiledWorkflowClosure, + dynamicWorkflows, + nodeExecutionsById, + ); + newMergedDag = dynamicWorkflow.dag; + } + } + } + setDagError(error); + setMergedDag(prev => { + if (isEqual(prev, newMergedDag)) { + return prev; + } + return newMergedDag; + }); + + // we remove start/end node info in the root dNode list during first assignment + const plainNodes = convertToPlainNodes(nodes); + plainNodes.map(node => { + const initialNode = initialDNodes.find(n => n.scopedId === node.scopedId); + if (initialNode) { + node.expanded = initialNode.expanded; + } + }); + setInitialDNodes(prev => { + if (isEqual(prev, plainNodes)) { + return prev; + } + return plainNodes; + }); + }, [ + compiledWorkflowClosure, + dynamicWorkflows, + dynamicParents, + nodeExecutionsById, + ]); + + useEffect(() => { + if (nodeExecutionDynamicWorkflowQuery.isFetching) { + return; + } + setDynamicWorkflows(prev => { + const newDynamicWorkflows = merge( + { ...(prev || {}) }, + nodeExecutionDynamicWorkflowQuery.data, + ); + if (isEqual(prev, newDynamicWorkflows)) { + return prev; + } + + return newDynamicWorkflows; + }); + }, [nodeExecutionDynamicWorkflowQuery]); + + useEffect(() => { + if (shouldUpdate) { + const newDynamicParents = checkForDynamicExecutions( + nodeExecutionsById, + staticExecutionIdsMap, + ); + setDynamicParents(prev => { + if (isEqual(prev, newDynamicParents)) { + return prev; + } + + return newDynamicParents; + }); + setShouldUpdate(false); + } + }, [shouldUpdate]); + + const setCurrentNodeExecutionsById = useCallback( + ( + newNodeExecutionsById: NodeExecutionsById, + checkForDynamicParents?: boolean, + ): void => { + setNodeExecutionsById(prev => { + const newNodes = mergeWith( + { ...prev }, + { ...newNodeExecutionsById }, + mergeNodeExecutions, + ); + if ( + JSON.stringify(prev, mapStringifyReplacer) === + JSON.stringify(newNodes, mapStringifyReplacer) + ) { + return prev; + } + + if (checkForDynamicParents) { + setShouldUpdate(true); + } + + return newNodes; + }); + }, + [], + ); + + return ( + + {children} + + ); +}; + +export const useNodeExecutionsById = (): IWorkflowNodeExecutionsContext => { + return useContext(WorkflowNodeExecutionsContext); +}; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx index 71baaf304..fc899112f 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx @@ -1,2 +1,2 @@ export * from './NodeExecutionDetailsContextProvider'; -export * from './NodeExecutionsByIdContextProvider'; +export * from './WorkflowNodeExecutionsProvider'; diff --git a/packages/console/src/components/Executions/contexts.ts b/packages/console/src/components/Executions/contexts.ts index 4a9be156f..25d9a639f 100644 --- a/packages/console/src/components/Executions/contexts.ts +++ b/packages/console/src/components/Executions/contexts.ts @@ -1,4 +1,5 @@ import { Execution, LogsByPhase, NodeExecution } from 'models/Execution/types'; +import { dNode } from 'models/Graph/types'; import { createContext } from 'react'; export interface ExecutionContextData { @@ -21,16 +22,21 @@ export type SetCurrentNodeExecutionsById = ( checkForDynamicParents?: boolean, ) => void; -export interface INodeExecutionsByIdContext { +export interface IWorkflowNodeExecutionsContext { nodeExecutionsById: NodeExecutionsById; - filteredNodeExecutions?: FilteredNodeExecutions; setCurrentNodeExecutionsById: SetCurrentNodeExecutionsById; shouldUpdate: boolean; setShouldUpdate: (val: boolean) => void; + // Tabs + initialDNodes: dNode[]; + dagData: { + mergedDag: any; + dagError: any; + }; } -export const NodeExecutionsByIdContext = - createContext({ +export const WorkflowNodeExecutionsContext = + createContext({ nodeExecutionsById: {}, setCurrentNodeExecutionsById: () => { throw new Error('Must use NodeExecutionsByIdContextProvider'); @@ -39,4 +45,9 @@ export const NodeExecutionsByIdContext = setShouldUpdate: _val => { throw new Error('Must use NodeExecutionsByIdContextProvider'); }, + initialDNodes: [], + dagData: { + mergedDag: {}, + dagError: null, + }, }); diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 24e48d034..5dc9ecb16 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -28,7 +28,7 @@ import { WorkflowNodeExecution } from './contexts'; import { fetchTaskExecutionList } from './taskExecutionQueries'; import { formatRetryAttempt, getGroupedLogs } from './TaskExecutionsList/utils'; import { NodeExecutionGroup } from './types'; -import { isParentNode } from './utils'; +import { executionIsTerminal, isParentNode } from './utils'; function removeSystemNodes(nodeExecutions: NodeExecution[]): NodeExecution[] { return nodeExecutions.filter(ne => { @@ -53,39 +53,42 @@ export function makeNodeExecutionQuery( }; } -export const getNodeExecutionWithTaskExecutions = async ( +export const getTaskExecutions = async ( nodeExecution: WorkflowNodeExecution, queryClient, ) => { - const taskExecutions = await fetchTaskExecutionList( + if (executionIsTerminal(nodeExecution as any) && nodeExecution.tasksFetched) { + return Promise.resolve(nodeExecution); + } + return await fetchTaskExecutionList( // request listTaskExecutions queryClient, nodeExecution.id as any, - ); - - const useNewMapTaskView = taskExecutions.every(taskExecution => { - const { - closure: { taskType, metadata, eventVersion = 0 }, - } = taskExecution; - return isMapTaskV1( - eventVersion, - metadata?.externalResources?.length ?? 0, - taskType ?? undefined, - ); - }); + ).then(taskExecutions => { + 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 externalResources: ExternalResource[] = taskExecutions + .map(taskExecution => taskExecution.closure.metadata?.externalResources) + .flat() + .filter((resource): resource is ExternalResource => !!resource); - const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); + const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); - return { - ...nodeExecution, - ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), - tasksFetched: true, - }; + return { + ...nodeExecution, + ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), + tasksFetched: true, + }; + }); }; /** A query for fetching a single `NodeExecution` by id. */ export function makeNodeExecutionQueryEnhanced( @@ -137,7 +140,7 @@ export function makeNodeExecutionQueryEnhanced( const childPromises = children.map(c => Promise.resolve(c)); const parentAndChildren = [ - getNodeExecutionWithTaskExecutions(nodeExecution, queryClient), + getTaskExecutions(nodeExecution, queryClient), ...childPromises, ]; // const childrenToSkip = nodeExecution.tasksFetched ? children.map(c => c.scopedId as string) : [] diff --git a/packages/console/src/components/Executions/useWorkflowExecution.ts b/packages/console/src/components/Executions/useWorkflowExecution.ts index 84b54baed..0bf9b26c5 100644 --- a/packages/console/src/components/Executions/useWorkflowExecution.ts +++ b/packages/console/src/components/Executions/useWorkflowExecution.ts @@ -24,7 +24,10 @@ export function makeWorkflowExecutionQuery( ): QueryInput { return { queryKey: [QueryType.WorkflowExecution, id], - queryFn: () => getExecution(id), + queryFn: async () => { + const result = await getExecution(id); + return result; + }, }; } 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 f21a350bb..91b8f9354 100644 --- a/packages/console/src/components/Launch/LaunchForm/test/ResumeSignalForm.test.tsx +++ b/packages/console/src/components/Launch/LaunchForm/test/ResumeSignalForm.test.tsx @@ -11,7 +11,7 @@ import { getMuiTheme } from 'components/Theme/muiTheme'; import { SimpleType } from 'models/Common/types'; import { resumeSignalNode } from 'models/Execution/api'; import * as React from 'react'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import { dateToTimestamp } from 'common/utils'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { createTestQueryClient } from 'test/utils'; @@ -111,7 +111,7 @@ describe('ResumeSignalForm', () => { compiledWorkflowClosure: mockCompiledWorkflowClosure, }} > - { compiledNode={mockCompiledNode} nodeExecutionId={mockNodeExecutionId} /> - + diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index 6380fd2bc..18915bd66 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -1,10 +1,10 @@ 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'; -import { dNode } from 'models/Graph/types'; import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; +import { ReactFlowBreadCrumbProvider } from 'components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider'; +import { IWorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import t from './strings'; export interface DynamicWorkflowMapping { @@ -13,22 +13,26 @@ export interface DynamicWorkflowMapping { dynamicExecutions: any[]; } -export interface WorkflowGraphProps { - mergedDag: any; - error?: Error; - initialNodes: dNode[]; -} -export const WorkflowGraph: React.FC = ({ - mergedDag, - error, - initialNodes, -}) => { - const { onNodeSelectionChanged, selectedPhase, setSelectedPhase } = - useDetailsPanel(); +export const WorkflowGraph: React.FC<{ + executionsContext: IWorkflowNodeExecutionsContext; +}> = ({ executionsContext }) => { + const { + selectedPhase, + isDetailsTabClosed, + onNodeSelectionChanged, + setSelectedPhase: onPhaseSelectionChanged, + } = useDetailsPanel(); + const { + initialDNodes: initialNodes, + dagData: { mergedDag, dagError }, + } = executionsContext; - if (error) { + if (dagError) { return ( - + ); } @@ -43,12 +47,15 @@ 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 a7cedac0b..aec7079c6 100644 --- a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx +++ b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -13,6 +13,7 @@ import { } from 'models/Workflow/types'; import { isParentNode } from 'components/Executions/utils'; import { isStartOrEndNode } from 'models/Node/utils'; +import { NodeExecutionsById } from 'models'; import { getDisplayName, getSubWorkflowFromId, @@ -26,367 +27,506 @@ export interface staticNodeExecutionIds { const debug = createDebugLogger('@transformerWorkflowToDag'); -/** - * Returns a DAG from Flyte workflow request data - * @param context input can be either CompiledWorkflow or CompiledNode - * @returns Display name - */ -export const transformerWorkflowToDag = ( - workflow: CompiledWorkflowClosure, - dynamicToMerge: any | null = null, - nodeExecutionsById = {}, -): any => { - const { primary } = workflow; - const staticExecutionIdsMap = {}; - - interface CreateDEdgeProps { - sourceId: string; - targetId: string; - } - const createDEdge = ({ sourceId, targetId }: CreateDEdgeProps): dEdge => { - const id = `${sourceId}->${targetId}`; - const edge: dEdge = { - sourceId: sourceId, - targetId: targetId, - id: id, - }; - return edge; - }; +interface CreateDNodeProps { + compiledNode: CompiledNode; + parentDNode?: dNode; + taskTemplate?: CompiledTask; + typeOverride?: dTypes; + nodeExecutionsById?: NodeExecutionsById; + staticExecutionIdsMap?: any; +} +const createDNode = ({ + compiledNode, + parentDNode, + taskTemplate, + typeOverride, + nodeExecutionsById, + staticExecutionIdsMap, +}: CreateDNodeProps): dNode => { + const nodeValue = + taskTemplate == null ? compiledNode : { ...compiledNode, ...taskTemplate }; - interface CreateDNodeProps { - compiledNode: CompiledNode; - parentDNode?: dNode; - taskTemplate?: CompiledTask; - typeOverride?: dTypes; - } - const createDNode = ({ - compiledNode, - parentDNode, - taskTemplate, - typeOverride, - }: CreateDNodeProps): dNode => { - const nodeValue = - taskTemplate == null - ? compiledNode - : { ...compiledNode, ...taskTemplate }; - - /** - * Note on scopedId: - * We need to be able to map nodeExecution's to their corresponding nodes. The problem is that nodeExecutions come - * back with a scoped id's (eg, {parentId}-{retry}-{childId}) while nodes are contextual (eg, 'n3' vs 'n0-0-n1-0-n3'). - * Further, even if we try to construct these values here we cannot know the actual retry value until run-time. - * - * To mitigate this we've added a new property on NodeExecutions that is the same as an executions scopedId but - * assuming '0' for each retry. We then construct that same scopedId here with the same solution of '0' for retries - * which allows us to map them regardless of what the actual retry value is. - */ - let scopedId = ''; + /** + * Note on scopedId: + * We need to be able to map nodeExecution's to their corresponding nodes. The problem is that nodeExecutions come + * back with a scoped id's (eg, {parentId}-{retry}-{childId}) while nodes are contextual (eg, 'n3' vs 'n0-0-n1-0-n3'). + * Further, even if we try to construct these values here we cannot know the actual retry value until run-time. + * + * To mitigate this we've added a new property on NodeExecutions that is the same as an executions scopedId but + * assuming '0' for each retry. We then construct that same scopedId here with the same solution of '0' for retries + * which allows us to map them regardless of what the actual retry value is. + */ + let scopedId = ''; + if ( + isStartOrEndNode(compiledNode) && + parentDNode && + !isStartOrEndNode(parentDNode) + ) { + scopedId = `${parentDNode.scopedId}-${compiledNode.id}`; + } else if (parentDNode && parentDNode.type !== dTypes.start) { if ( - isStartOrEndNode(compiledNode) && - parentDNode && - !isStartOrEndNode(parentDNode) + parentDNode.type === dTypes.branch || + parentDNode.type === dTypes.subworkflow ) { - scopedId = `${parentDNode.scopedId}-${compiledNode.id}`; - } else if (parentDNode && parentDNode.type !== dTypes.start) { - if ( - parentDNode.type === dTypes.branch || - parentDNode.type === dTypes.subworkflow - ) { - scopedId = `${parentDNode.scopedId}-0-${compiledNode.id}`; - } else { - scopedId = `${parentDNode.scopedId}-${compiledNode.id}`; - } + scopedId = `${parentDNode.scopedId}-0-${compiledNode.id}`; } else { - /* Case: primary workflow nodes won't have parents */ - scopedId = compiledNode.id; + scopedId = `${parentDNode.scopedId}-${compiledNode.id}`; } - const type = - typeOverride == null - ? getNodeTypeFromCompiledNode(compiledNode) - : typeOverride; - - const nodeExecution = nodeExecutionsById[scopedId]; - const isParent = nodeExecution && isParentNode(nodeExecution); - - const output = { - id: compiledNode.id, - scopedId: scopedId, - value: nodeValue, - type: type, - name: getDisplayName(compiledNode), - nodes: [], - edges: [], - gateNode: compiledNode.gateNode, - isParentNode: isParent, - } as dNode; - - staticExecutionIdsMap[output.scopedId] = compiledNode; - return output; - }; + } else { + /* Case: primary workflow nodes won't have parents */ + scopedId = compiledNode.id; + } + const type = + typeOverride == null + ? getNodeTypeFromCompiledNode(compiledNode) + : typeOverride; + + const nodeExecution = nodeExecutionsById?.[scopedId]; + const isParent = nodeExecution && isParentNode(nodeExecution); + + const output = { + id: compiledNode.id, + scopedId: scopedId, + value: nodeValue, + type: type, + name: getDisplayName(compiledNode), + nodes: [], + edges: [], + gateNode: compiledNode.gateNode, + isParentNode: isParent, + ...(nodeExecution ? { execution: nodeExecution } : {}), + } as dNode; + + staticExecutionIdsMap[output.scopedId] = compiledNode; + return output; +}; - const buildBranchStartEndNodes = (root: dNode) => { - const startNode = createDNode({ - compiledNode: { - id: `${root.id}-${startNodeId}`, - metadata: { - name: DISPLAY_NAME_START, - }, - } as CompiledNode, - typeOverride: dTypes.nestedStart, - }); +const buildBranchStartEndNodes = ( + root: dNode, + nodeExecutionsById: NodeExecutionsById = {}, + staticExecutionIdsMap: any = {}, +) => { + const startNode = createDNode({ + compiledNode: { + id: `${root.id}-${startNodeId}`, + metadata: { + name: DISPLAY_NAME_START, + }, + } as CompiledNode, + typeOverride: dTypes.nestedStart, + nodeExecutionsById, + staticExecutionIdsMap, + }); - const endNode = createDNode({ - compiledNode: { - id: `${root.id}-${endNodeId}`, - metadata: { - name: DISPLAY_NAME_END, - }, - } as CompiledNode, - typeOverride: dTypes.nestedEnd, - }); + const endNode = createDNode({ + compiledNode: { + id: `${root.id}-${endNodeId}`, + metadata: { + name: DISPLAY_NAME_END, + }, + } as CompiledNode, + typeOverride: dTypes.nestedEnd, + nodeExecutionsById, + staticExecutionIdsMap, + }); - return { - startNode, - endNode, - }; + return { + startNode, + endNode, }; +}; - const buildWorkflowEdges = ( - root, - context: ConnectionSet, - ingress, - nodeMap, - ) => { - const list = context.downstream[ingress].ids; - - for (let i = 0; i < list.length; i++) { - const source = nodeMap[ingress]?.dNode.scopedId; - const target = nodeMap[list[i]]?.dNode.scopedId; - if (source && target) { - const edge: dEdge = createDEdge({ - sourceId: source, - targetId: target, - }); - root.edges.push(edge); - if (context.downstream[list[i]]) { - buildWorkflowEdges(root, context, list[i], nodeMap); - } +interface CreateDEdgeProps { + sourceId: string; + targetId: string; +} +const createDEdge = ({ sourceId, targetId }: CreateDEdgeProps): dEdge => { + const id = `${sourceId}->${targetId}`; + const edge: dEdge = { + sourceId: sourceId, + targetId: targetId, + id: id, + }; + return edge; +}; + +const buildWorkflowEdges = (root, context: ConnectionSet, ingress, nodeMap) => { + const downstreamIds = context.downstream[ingress].ids; + + for (let i = 0; i < downstreamIds.length; i++) { + const source = nodeMap[ingress]?.dNode.scopedId; + const target = nodeMap[downstreamIds[i]]?.dNode.scopedId; + if (source && target) { + const edge: dEdge = createDEdge({ + sourceId: source, + targetId: target, + }); + root.edges.push(edge); + if (context.downstream[downstreamIds[i]]) { + buildWorkflowEdges(root, context, downstreamIds[i], nodeMap); } } - }; + } +}; +/** + * Handles parsing CompiledNode + * + * @param node CompiledNode to parse + * @param root Root node for the graph that will be rendered + * @param workflow Main/root workflow + */ +interface ParseNodeProps { + node: CompiledNode; + root?: dNode; + nodeExecutionsById: NodeExecutionsById; + staticExecutionIdsMap: any; + dynamicToMerge: any | null; + workflow: any; +} +const parseNode = ({ + node, + root, + nodeExecutionsById, + staticExecutionIdsMap, + dynamicToMerge, + workflow, +}: ParseNodeProps) => { + let dNode; /** - * Handles parsing CompiledNode - * - * @param node CompiledNode to parse - * @param root Root node for the graph that will be rendered - * @param workflow Main/root workflow + * Note: if node is dynamic we must add dynamicWorkflow + * as a subworkflow on the root workflow. We also need to check + * if the dynamic workflow has any subworkflows and add them too. */ - interface ParseNodeProps { - node: CompiledNode; - root?: dNode; - } - const parseNode = ({ node, root }: ParseNodeProps) => { - let dNode; - /** - * Note: if node is dynamic we must add dynamicWorkflow - * as a subworkflow on the root workflow. We also need to check - * if the dynamic workflow has any subworkflows and add them too. - */ - if (dynamicToMerge) { - const scopedId = `${root?.scopedId}-0-${node.id}`; - const id = dynamicToMerge[scopedId] != null ? scopedId : node.id; - if (dynamicToMerge[id]) { - const dynamicWorkflow = dynamicToMerge[id].dynamicWorkflow; - - if (dynamicWorkflow) { - const dWorkflowId = - dynamicWorkflow.metadata?.specNodeId || dynamicWorkflow.id; - const dPrimaryWorkflow = dynamicWorkflow.compiledWorkflow.primary; - - node['workflowNode'] = { - subWorkflowRef: dWorkflowId, - }; - - /* 1. Add primary workflow as subworkflow on root */ - if (getSubWorkflowFromId(dWorkflowId, workflow) === false) { - workflow.subWorkflows?.push(dPrimaryWorkflow); - } + if (dynamicToMerge) { + const scopedId = `${root?.scopedId}-0-${node.id}`; + const id = dynamicToMerge[scopedId] != null ? scopedId : node.id; + if (dynamicToMerge[id]) { + const dynamicWorkflow = dynamicToMerge[id].dynamicWorkflow; + + if (dynamicWorkflow) { + const dWorkflowId = + dynamicWorkflow.metadata?.specNodeId || dynamicWorkflow.id; + const dPrimaryWorkflow = dynamicWorkflow.compiledWorkflow.primary; + + node['workflowNode'] = { + subWorkflowRef: dWorkflowId, + }; + + /* 1. Add primary workflow as subworkflow on root */ + if (getSubWorkflowFromId(dWorkflowId, workflow) === false) { + workflow.subWorkflows?.push(dPrimaryWorkflow); + } - /* 2. Add subworkflows as subworkflows on root */ - const dSubWorkflows = dynamicWorkflow.compiledWorkflow.subWorkflows; + /* 2. Add subworkflows as subworkflows on root */ + const dSubWorkflows = dynamicWorkflow.compiledWorkflow.subWorkflows; - for (let i = 0; i < dSubWorkflows.length; i++) { - const subworkflow = dSubWorkflows[i]; - const subId = subworkflow.template.id; - if (getSubWorkflowFromId(subId, workflow) === false) { - workflow.subWorkflows?.push(subworkflow); - } + for (let i = 0; i < dSubWorkflows.length; i++) { + const subworkflow = dSubWorkflows[i]; + const subId = subworkflow.template.id; + if (getSubWorkflowFromId(subId, workflow) === false) { + workflow.subWorkflows?.push(subworkflow); } } - /* Remove entry when done to prevent infinite loop */ - delete dynamicToMerge[node.id]; } + /* Remove entry when done to prevent infinite loop */ + delete dynamicToMerge[node.id]; } + } - if (node.branchNode) { + if (node.branchNode) { + dNode = createDNode({ + compiledNode: node, + parentDNode: root, + nodeExecutionsById, + staticExecutionIdsMap, + }); + buildDAG( + dNode, + node, + dTypes.branch, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + ); + } else if (node.workflowNode) { + if (node.workflowNode.launchplanRef) { dNode = createDNode({ compiledNode: node, parentDNode: root, - }); - buildDAG(dNode, node, dTypes.branch); - } else if (node.workflowNode) { - if (node.workflowNode.launchplanRef) { - dNode = createDNode({ - compiledNode: node, - parentDNode: root, - }); - } else { - const id = node.workflowNode.subWorkflowRef; - const subworkflow = getSubWorkflowFromId(id, workflow); - dNode = createDNode({ - compiledNode: node, - parentDNode: root, - }); - buildDAG(dNode, subworkflow, dTypes.subworkflow); - } - } else if (node.taskNode) { - const taskNode = node.taskNode as TaskNode; - const taskType: CompiledTask = getTaskTypeFromCompiledNode( - taskNode, - workflow.tasks, - ) as CompiledTask; - dNode = createDNode({ - compiledNode: node as CompiledNode, - parentDNode: root, - taskTemplate: taskType, + nodeExecutionsById, + staticExecutionIdsMap, }); } else { + const id = node.workflowNode.subWorkflowRef; + const subworkflow = getSubWorkflowFromId(id, workflow); dNode = createDNode({ compiledNode: node, parentDNode: root, + nodeExecutionsById, + staticExecutionIdsMap, }); + buildDAG( + dNode, + subworkflow, + dTypes.subworkflow, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + ); } - root?.nodes.push(dNode); - }; - - /** - * Handles parsing branch from CompiledNode - * - * @param root Root node for the branch that will be rendered - * @param context Current branch node being parsed - */ - interface ParseBranchProps { - root: dNode; - context: CompiledNode; + } else if (node.taskNode) { + const taskNode = node.taskNode as TaskNode; + const taskType: CompiledTask = getTaskTypeFromCompiledNode( + taskNode, + workflow.tasks, + ) as CompiledTask; + dNode = createDNode({ + compiledNode: node as CompiledNode, + parentDNode: root, + taskTemplate: taskType, + nodeExecutionsById, + staticExecutionIdsMap, + }); + } else { + dNode = createDNode({ + compiledNode: node, + parentDNode: root, + nodeExecutionsById, + staticExecutionIdsMap, + }); } - const parseBranch = ({ root, context }: ParseBranchProps) => { - const otherNode = context.branchNode?.ifElse?.other; - const thenNode = context.branchNode?.ifElse?.case?.thenNode as CompiledNode; - const elseNode = context.branchNode?.ifElse?.elseNode as CompiledNode; - - /* Check: then (if) case */ - if (thenNode) { - parseNode({ node: thenNode, root: root }); - } - - /* Check: else case */ - if (elseNode) { - parseNode({ node: elseNode, root: root }); - } + root?.nodes.push(dNode); +}; - /* Check: other (else-if) case */ - if (otherNode) { - otherNode.map(otherItem => { - const otherCompiledNode: CompiledNode = - otherItem.thenNode as CompiledNode; - parseNode({ - node: otherCompiledNode, - root: root, - }); +/** + * Recursively renders DAG of given context. + * + * @param root Root node of DAG (note: will mutate root) + * @param graphType DAG type (eg, branch, workflow) + * @param context Pointer to current context of response + */ +const buildDAG = ( + root: dNode, + context: any, + graphType: dTypes, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, +) => { + switch (graphType) { + case dTypes.branch: + parseBranch({ + root, + context, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, }); + break; + case dTypes.subworkflow: + parseWorkflow( + root, + context, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + ); + break; + case dTypes.primary: + return parseWorkflow( + root, + context, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + ); + default: { + throw new Error('unhandled case'); } + } +}; - /* Add edges and add start/end nodes */ - const { startNode, endNode } = buildBranchStartEndNodes(root); - for (let i = 0; i < root.nodes.length; i++) { - const startEdge: dEdge = createDEdge({ - sourceId: startNode.id, - targetId: root.nodes[i].scopedId, - }); - const endEdge: dEdge = createDEdge({ - sourceId: root.nodes[i].scopedId, - targetId: endNode.id, - }); - root.edges.push(startEdge); - root.edges.push(endEdge); - } - root.nodes.push(startNode); - root.nodes.push(endNode); - }; +/** + * Handles parsing branch from CompiledNode + * + * @param root Root node for the branch that will be rendered + * @param context Current branch node being parsed + */ +interface ParseBranchProps { + root: dNode; + context: CompiledNode; + nodeExecutionsById: NodeExecutionsById; + staticExecutionIdsMap: any; + dynamicToMerge: any | null; + workflow: any; +} +const parseBranch = ({ + root, + context, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, +}: ParseBranchProps) => { + const otherNode = context.branchNode?.ifElse?.other; + const thenNode = context.branchNode?.ifElse?.case?.thenNode as CompiledNode; + const elseNode = context.branchNode?.ifElse?.elseNode as CompiledNode; + + /* Check: then (if) case */ + if (thenNode) { + parseNode({ + node: thenNode, + root: root, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + }); + } - /** - * Handles parsing CompiledWorkflow - * - * @param root Root node for the graph that will be rendered - * @param context The current workflow being parsed - */ - const parseWorkflow = (root, context: CompiledWorkflow) => { - if (!context?.template?.nodes) { - return root; - } + /* Check: else case */ + if (elseNode) { + parseNode({ + node: elseNode, + root: root, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + }); + } - /* Build Nodes from template */ - for (let i = 0; i < context.template.nodes.length; i++) { - const compiledNode: CompiledNode = context.template.nodes[i]; + /* Check: other (else-if) case */ + if (otherNode) { + otherNode.map(otherItem => { + const otherCompiledNode: CompiledNode = + otherItem.thenNode as CompiledNode; parseNode({ - node: compiledNode, + node: otherCompiledNode, root: root, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, }); - } - - const nodesList = context.template.nodes; - const nodeMap = {}; + }); + } - /* Create mapping of CompiledNode.id => dNode.id to build edges */ - for (let i = 0; i < root.nodes.length; i++) { - const dNode = root.nodes[i]; - nodeMap[dNode.id] = { - dNode: dNode, - compiledNode: nodesList[i], - }; - } + /* Add edges and add start/end nodes */ + const { startNode, endNode } = buildBranchStartEndNodes( + root, + nodeExecutionsById, + staticExecutionIdsMap, + ); + for (let i = 0; i < root.nodes.length; i++) { + const startEdge: dEdge = createDEdge({ + sourceId: startNode.id, + targetId: root.nodes[i].scopedId, + }); + const endEdge: dEdge = createDEdge({ + sourceId: root.nodes[i].scopedId, + targetId: endNode.id, + }); + root.edges.push(startEdge); + root.edges.push(endEdge); + } + root.nodes.push(startNode); + root.nodes.push(endNode); +}; - /* Build Edges */ - buildWorkflowEdges(root, context.connections, startNodeId, nodeMap); +/** + * Handles parsing CompiledWorkflow + * + * @param root Root node for the graph that will be rendered + * @param context The current workflow being parsed + */ +const parseWorkflow = ( + root, + context: CompiledWorkflow, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, +) => { + if (!context?.template?.nodes) { return root; - }; + } + + const templateNodeList = context.template.nodes; + + /* Build Nodes from template */ + for (let i = 0; i < templateNodeList.length; i++) { + const compiledNode: CompiledNode = templateNodeList[i]; + parseNode({ + node: compiledNode, + root: root, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + }); + } + + const nodeMap = {}; + + /* Create mapping of CompiledNode.id => dNode.id to build edges */ + for (let i = 0; i < root.nodes.length; i++) { + const dNode = root.nodes[i]; + nodeMap[dNode.id] = { + dNode: dNode, + compiledNode: templateNodeList[i], + }; + } + + /* Build Edges */ + buildWorkflowEdges(root, context.connections, startNodeId, nodeMap); + return root; +}; + +/** + * Returns a DAG from Flyte workflow request data + * @param context input can be either CompiledWorkflow or CompiledNode + * @returns Display name + */ +export const transformerWorkflowToDag = ( + workflow: CompiledWorkflowClosure, + dynamicToMerge: any | null = null, + nodeExecutionsById = {}, +): any => { + const { primary } = workflow; + const staticExecutionIdsMap = {}; - /** - * Recursively renders DAG of given context. - * - * @param root Root node of DAG (note: will mutate root) - * @param graphType DAG type (eg, branch, workflow) - * @param context Pointer to current context of response - */ - const buildDAG = (root: dNode, context: any, graphType: dTypes) => { - switch (graphType) { - case dTypes.branch: - parseBranch({ root, context }); - break; - case dTypes.subworkflow: - parseWorkflow(root, context); - break; - case dTypes.primary: - return parseWorkflow(root, context); - } - }; const primaryWorkflowRoot = createDNode({ compiledNode: { id: startNodeId, } as CompiledNode, + nodeExecutionsById, + staticExecutionIdsMap, }); - const dag: dNode = buildDAG(primaryWorkflowRoot, primary, dTypes.primary); - debug('output:', dag); - return { dag, staticExecutionIdsMap }; + let dag: dNode; + + try { + dag = buildDAG( + primaryWorkflowRoot, + primary, + dTypes.primary, + dynamicToMerge, + nodeExecutionsById, + staticExecutionIdsMap, + workflow, + ); + debug('output:', dag); + return { dag, staticExecutionIdsMap }; + } catch (error) { + dag = {} as any; + debug('output:', dag); + return { dag, staticExecutionIdsMap, error }; + } }; diff --git a/packages/console/src/components/common/LoadingSpinner.tsx b/packages/console/src/components/common/LoadingSpinner.tsx index fc9f298df..7abc91df8 100644 --- a/packages/console/src/components/common/LoadingSpinner.tsx +++ b/packages/console/src/components/common/LoadingSpinner.tsx @@ -55,3 +55,11 @@ export const MediumLoadingSpinner: React.FC = () => ( export const LargeLoadingSpinner: React.FC = () => ( ); + +export const LargeLoadingComponent = () => { + return ( +
    + +
    + ); +}; diff --git a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx new file mode 100644 index 000000000..d5bf3a6a6 --- /dev/null +++ b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx @@ -0,0 +1,138 @@ +import React, { PropsWithChildren } from 'react'; +import { useNodeExecutionDynamicContext } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; +import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; +import { NodeExecutionPhase } from 'models'; +import { getNestedContainerStyle } from './utils'; + +const BREAD_FONT_SIZE = '9px'; +const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; +const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color; + +export const BreadElement = ({ + nestedView, + index, + currentNestedDepth, + scopedId, + onClick, +}) => { + const liStyles: React.CSSProperties = { + cursor: 'pointer', + fontSize: BREAD_FONT_SIZE, + color: BREAD_COLOR_ACTIVE, + }; + + const liStyleInactive: React.CSSProperties = { ...liStyles }; + liStyleInactive['color'] = BREAD_COLOR_INACTIVE; + + const beforeStyle: React.CSSProperties = { + cursor: 'pointer', + color: BREAD_COLOR_ACTIVE, + padding: '0 .2rem', + fontSize: BREAD_FONT_SIZE, + }; + // const onClick = + // currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; + return ( +
  • + {index === 0 ? {'>'} : null} + {nestedView} + {index < currentNestedDepth - 1 ? ( + {'>'} + ) : null} +
  • + ); +}; + +const BorderElement = ({ + nodeExecutionStatus, + children, +}: PropsWithChildren<{ + nodeExecutionStatus: NodeExecutionPhase; +}>) => { + const { componentProps } = useNodeExecutionDynamicContext(); + + const borderStyle = getNestedContainerStyle(nodeExecutionStatus); + + return ( +
    + {children} +
    + ); +}; + +export const BorderContainer = ({ + nodeExecutionStatus, + currentNestedDepth, + children, +}: PropsWithChildren<{ + currentNestedDepth: number; + nodeExecutionStatus: NodeExecutionPhase; +}>) => { + let borders = ( + + {children} + + ); + for (let i = 0; i < currentNestedDepth; i++) { + borders = ( + + {borders} + + ); + } + + return borders; +}; + +const breadContainerStyle: React.CSSProperties = { + position: 'absolute', + display: 'flex', + width: '100%', + marginTop: '-1rem', +}; +const olStyles: React.CSSProperties = { + margin: 0, + padding: 0, + display: 'flex', + listStyle: 'none', + listStyleImage: 'none', + minWidth: '1rem', +}; +const headerStyle: React.CSSProperties = { + color: BREAD_COLOR_ACTIVE, + fontSize: BREAD_FONT_SIZE, + margin: 0, + padding: 0, +}; + +export const BreadCrumbContainer = ({ + text, + currentNestedDepth, + handleRootClick, + children, +}: PropsWithChildren<{ + text: string; + currentNestedDepth: number; + handleRootClick: () => void; +}>) => { + const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; + return ( +
    +
    { + e.stopPropagation(); + rootClick?.(); + }} + > + {text} +
    +
      {children}
    +
    + ); +}; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx new file mode 100644 index 000000000..886202d88 --- /dev/null +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx @@ -0,0 +1,97 @@ +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { fetchChildrenExecutions } from 'components/Executions/utils'; +import React, { + createContext, + PropsWithChildren, + useContext, + Ref, + useState, +} from 'react'; +import { useQueryClient } from 'react-query'; +import { isUnFetchedDynamicNode } from './utils'; + +export type RefType = Ref; +export interface IReactFlowBreadCrumbContext { + currentNestedDepth: number; + currentNestedView: BreadCrumbViews; + setCurrentNestedView: (newLevels: BreadCrumbViews) => void; + onAddNestedView: (view: any, sourceNode?: any) => Promise; + onRemoveNestedView: (viewParent: any, viewIndex: any) => void; +} + +export const ReactFlowBreadCrumbContext = + createContext({ + currentNestedDepth: 0, + currentNestedView: {}, + setCurrentNestedView: () => {}, + onAddNestedView: () => { + throw new Error('please use NodeExecutionDynamicProvider'); + }, + onRemoveNestedView: () => { + throw new Error('please use NodeExecutionDynamicProvider'); + }, + }); + +export interface BreadCrumbViews { + [key: string]: string[]; +} +/** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ +export const ReactFlowBreadCrumbProvider = ({ + children, +}: PropsWithChildren<{}>) => { + const queryClient = useQueryClient(); + const [currentNestedView, setCurrentNestedView] = useState( + {}, + ); + const currentNestedDepth = (currentNestedView?.length || 0) as any as number; + + const { nodeExecutionsById, setCurrentNodeExecutionsById } = + useNodeExecutionsById(); + + const onAddNestedView = async (view, sourceNode: any = null) => { + if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { + await fetchChildrenExecutions( + queryClient, + sourceNode.scopedId, + nodeExecutionsById, + setCurrentNodeExecutionsById, + ); + } + + const currentView = currentNestedView[view.parent] || []; + const newView = { + [view.parent]: [...currentView, view.view], + }; + setCurrentNestedView(newView); + }; + + 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); + }; + + return ( + + {children} + + ); +}; + +export const useReactFlowBreadCrumbContext = + (): IReactFlowBreadCrumbContext => { + return useContext(ReactFlowBreadCrumbContext); + }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 24a5a5667..0478814c5 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -5,19 +5,15 @@ import { useNodeExecutionsById, } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { NodeExecutionPhase } from 'models/Execution/enums'; -import { - fetchChildrenExecutions, - isNodeGateNode, -} from 'components/Executions/utils'; +import { isNodeGateNode } from 'components/Executions/utils'; import { dNode } from 'models/Graph/types'; -import { useQueryClient } from 'react-query'; import { extractCompiledNodes } from 'components/hooks/utils'; -import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; -import { getRFBackground, isUnFetchedDynamicNode } from './utils'; +import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; import { PausedTasksComponent } from './PausedTasksComponent'; +import { useReactFlowBreadCrumbContext } from './ReactFlowBreadCrumbProvider'; const containerStyle: React.CSSProperties = { display: 'flex', @@ -33,44 +29,14 @@ export const ReactFlowGraphComponent = ({ onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, + isDetailsTabClosed, initialNodes, }) => { - const { nodeExecutionsById, shouldUpdate } = - useNodeExecutionsById(); - const { isDetailsTabClosed } = useDetailsPanel(); - + const { nodeExecutionsById, shouldUpdate } = useNodeExecutionsById(); const { compiledWorkflowClosure } = useNodeExecutionContext(); const [pausedNodes, setPausedNodes] = useState([]); - const [currentNestedView, setcurrentNestedView] = useState({}); - - const onAddNestedView = async (view, sourceNode: any = null) => { - // if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { - // await fetchChildrenExecutions( - // queryClient, - // sourceNode.scopedId, - // nodeExecutionsById, - // setCurrentNodeExecutionsById, - // ); - // } - - const currentView = currentNestedView[view.parent] || []; - const newView = { - [view.parent]: [...currentView, view.view], - }; - setcurrentNestedView(newView); - }; - - 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 { currentNestedView } = useReactFlowBreadCrumbContext(); const rfGraphJson = useMemo(() => { return ConvertFlyteDagToReactFlows({ @@ -79,22 +45,18 @@ export const ReactFlowGraphComponent = ({ onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, - onAddNestedView, - onRemoveNestedView, - currentNestedView, maxRenderDepth: 1, + currentNestedView, } as ConvertDagProps); }, [ data, isDetailsTabClosed, - shouldUpdate, nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, - onAddNestedView, - onRemoveNestedView, currentNestedView, + shouldUpdate, ]); const backgroundStyle = getRFBackground().nested; @@ -121,26 +83,25 @@ export const ReactFlowGraphComponent = ({ }; }); setPausedNodes(nodesWithExecutions); - }, [initialNodes, nodeExecutionsById]); + }, [initialNodes]); - const renderGraph = () => { - const reactFlowProps: RFWrapperProps = { - backgroundStyle, - rfGraphJson, - type: RFGraphTypes.main, - nodeExecutionsById, - currentNestedView: currentNestedView, - }; - return ( -
    - {pausedNodes && pausedNodes.length > 0 && ( - - )} - - -
    - ); + const ReactFlowProps: RFWrapperProps = { + backgroundStyle, + rfGraphJson, + type: RFGraphTypes.main, + nodeExecutionsById, + currentNestedView: currentNestedView, }; - return rfGraphJson ? renderGraph() : <>; + return rfGraphJson ? ( +
    + {pausedNodes && pausedNodes.length > 0 && ( + + )} + + +
    + ) : ( + <> + ); }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index 135f67c27..a5d95ad2c 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; import ReactFlow, { Background } from 'react-flow-renderer'; -import { withNodeExecutionDynamicProvider } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; import { getPositionedNodes, ReactFlowIdHash } from './utils'; import { ReactFlowCustomEndNode, @@ -19,29 +18,18 @@ import { RFWrapperProps } from './types'; * Mapping for using custom nodes inside ReactFlow */ const CustomNodeTypes = { - FlyteNode_task: withNodeExecutionDynamicProvider( - ReactFlowCustomTaskNode, - 'graph', - ), - FlyteNode_subworkflow: withNodeExecutionDynamicProvider( - ReactFlowSubWorkflowContainer, - 'graph', - ), + FlyteNode_task: ReactFlowCustomTaskNode, + FlyteNode_subworkflow: ReactFlowSubWorkflowContainer, FlyteNode_start: ReactFlowCustomStartNode, FlyteNode_end: ReactFlowCustomEndNode, FlyteNode_nestedStart: ReactFlowCustomNestedPoint, FlyteNode_nestedEnd: ReactFlowCustomNestedPoint, - FlyteNode_nestedMaxDepth: withNodeExecutionDynamicProvider( - ReactFlowCustomMaxNested, - 'graph', - ), + FlyteNode_nestedMaxDepth: ReactFlowCustomMaxNested, + FlyteNode_staticNode: ReactFlowStaticNode, FlyteNode_staticNestedNode: ReactFlowStaticNested, - FlyteNode_gateNode: withNodeExecutionDynamicProvider( - ReactFlowGateNode, - 'graph', - ), + FlyteNode_gateNode: ReactFlowGateNode, }; const reactFlowStyle: React.CSSProperties = { diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 0542e892c..0f6e6a620 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -6,7 +6,6 @@ import { RENDER_ORDER } from 'components/Executions/TaskExecutionsList/constants import { whiteColor } from 'components/Theme/constants'; 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'; import { CacheStatus } from 'components/Executions/CacheStatus'; import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog'; @@ -15,16 +14,24 @@ import { useNodeExecutionsById, } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { extractCompiledNodes } from 'components/hooks/utils'; -import { useNodeExecutionDynamicContext } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; +import { + NodeExecutionDynamicProvider, + useNodeExecutionDynamicContext, +} from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; import { COLOR_GRAPH_BACKGROUND, getGraphHandleStyle, getGraphNodeStyle, - getNestedContainerStyle, getStatusColor, } from './utils'; import { RFHandleProps, RFNode } from './types'; import t from './strings'; +import { + BorderContainer, + BreadCrumbContainer, + BreadElement, +} from './BreadCrumb'; +import { useReactFlowBreadCrumbContext } from './ReactFlowBreadCrumbProvider'; const taskContainerStyle: React.CSSProperties = { position: 'absolute', @@ -149,33 +156,58 @@ export const ReactFlowCustomNestedPoint = ({ data }: RFNode) => { * denoted by solid color. * @param props.data data property of ReactFlowGraphNodeData */ - -export const ReactFlowCustomMaxNested = ({ data }: RFNode) => { - const { text, taskType, scopedId, onAddNestedView } = data; +export const ReactFlowCustomMaxNested = (props: ReactFlowNodeProps) => { + return ( + + + + ); +}; +const ReactFlowCustomMaxNestedInner = ({ data }: RFNode) => { + const { text, taskType, scopedId, isParentNode, parentScopedId, node } = data; const styles = getGraphNodeStyle(dTypes.nestedMaxDepth); + const { onAddNestedView } = useReactFlowBreadCrumbContext(); const { componentProps } = useNodeExecutionDynamicContext(); - const onClick = () => { - onAddNestedView(); - }; - return renderBasicNode( taskType, text, scopedId, styles, - onClick, + () => { + onAddNestedView( + { + parent: isParentNode ? parentScopedId : scopedId, + view: scopedId, + }, + node, + ); + }, componentProps, ); }; -export const ReactFlowStaticNested = ({ data }: RFNode) => { +export const ReactFlowStaticNested = (props: ReactFlowNodeProps) => { + return ( + + + + ); +}; +const ReactFlowStaticNestedInner = ({ data }: RFNode) => { const { text, taskType, scopedId } = data; const styles = getGraphNodeStyle(dTypes.staticNestedNode); return renderBasicNode(taskType, text, scopedId, styles); }; -export const ReactFlowStaticNode = ({ data }: RFNode) => { +export const ReactFlowStaticNode = (props: ReactFlowNodeProps) => { + return ( + + + + ); +}; +const ReactFlowStaticNodeInner = ({ data }: RFNode) => { const { text, taskType, scopedId } = data; const styles = getGraphNodeStyle(dTypes.staticNode); return renderBasicNode(taskType, text, scopedId, styles); @@ -235,8 +267,14 @@ const TaskPhaseItem = ({ * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ - -export const ReactFlowGateNode = ({ data }: RFNode) => { +export const ReactFlowGateNode = (props: ReactFlowNodeProps) => { + return ( + + + + ); +}; +const ReactFlowGateNodeInner = ({ data }: RFNode) => { const { compiledWorkflowClosure } = useNodeExecutionContext(); const { nodeExecutionsById } = useNodeExecutionsById(); const { @@ -269,7 +307,8 @@ export const ReactFlowGateNode = ({ data }: RFNode) => { cursor: 'pointer', }; - const handleNodeClick = () => { + const handleNodeClick = e => { + e.stopPropagation(); onNodeSelectionChanged(true); }; @@ -311,14 +350,19 @@ export const ReactFlowGateNode = ({ data }: RFNode) => { * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ -export type ReactFlowCustomTaskNodeProps = ReactFlowProps & RFNode; -export const ReactFlowCustomTaskNode = ( - props: ReactFlowCustomTaskNodeProps, -) => { +export type ReactFlowNodeProps = ReactFlowProps & RFNode; +export const ReactFlowCustomTaskNode = (props: ReactFlowNodeProps) => { + return ( + + + + ); +}; +const ReactFlowCustomTaskNodeInner = (props: ReactFlowNodeProps) => { const { data } = props; const { nodeType, - nodeExecutionStatus, + nodeExecutionStatus: initialNodeExecutionStatus, selectedPhase: initialPhase, taskType, text, @@ -328,12 +372,15 @@ export const ReactFlowCustomTaskNode = ( onNodeSelectionChanged, onPhaseSelectionChanged, } = data; - const styles = getGraphNodeStyle(nodeType, nodeExecutionStatus); + const [selectedNode, setSelectedNode] = useState(false); const [selectedPhase, setSelectedPhase] = useState< TaskExecutionPhase | undefined >(initialPhase); - const { componentProps } = useNodeExecutionDynamicContext(); + const { nodeExecution, componentProps } = useNodeExecutionDynamicContext(); + const nodeExecutionStatus = + nodeExecution?.closure?.phase || initialNodeExecutionStatus; + const styles = getGraphNodeStyle(nodeType, nodeExecutionStatus); useEffect(() => { if (selectedNode === true) { @@ -374,7 +421,9 @@ export const ReactFlowCustomTaskNode = ( display: 'flex', }; - const handleNodeClick = _e => { + const handleNodeClick = e => { + e.stopPropagation(); + if (nodeExecutionStatus === NodeExecutionPhase.SKIPPED) { return; } @@ -444,130 +493,58 @@ export const ReactFlowCustomTaskNode = ( * and any edge handles. * @param props.data data property of ReactFlowGraphNodeData */ -export const ReactFlowSubWorkflowContainer = ({ data }: RFNode) => { +export const ReactFlowSubWorkflowContainer = (props: ReactFlowNodeProps) => { + return ( + + + + ); +}; +export const ReactFlowSubWorkflowContainerInner = ({ data }: RFNode) => { const { - nodeExecutionStatus, + nodeExecutionStatus: initialNodeExecutionStatus, text, scopedId, currentNestedView, - onRemoveNestedView, } = data; - const BREAD_FONT_SIZE = '9px'; - const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; - const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color; - const borderStyle = getNestedContainerStyle(nodeExecutionStatus); - const { componentProps } = useNodeExecutionDynamicContext(); - const handleNestedViewClick = e => { - const index = e.target.id.substr( - e.target.id.indexOf('_') + 1, - e.target.id.length, - ); - onRemoveNestedView(scopedId, index); - }; + const { onRemoveNestedView } = useReactFlowBreadCrumbContext(); + const { nodeExecution } = useNodeExecutionDynamicContext(); const handleRootClick = () => { onRemoveNestedView(scopedId, -1); }; const currentNestedDepth = currentNestedView?.length || 0; - - const BreadElement = ({ nestedView, index }) => { - const liStyles: React.CSSProperties = { - cursor: 'pointer', - fontSize: BREAD_FONT_SIZE, - color: BREAD_COLOR_ACTIVE, - }; - - const liStyleInactive: React.CSSProperties = { ...liStyles }; - liStyleInactive['color'] = BREAD_COLOR_INACTIVE; - - const beforeStyle: React.CSSProperties = { - cursor: 'pointer', - color: BREAD_COLOR_ACTIVE, - padding: '0 .2rem', - fontSize: BREAD_FONT_SIZE, - }; - // const onClick = - // currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; - return ( -
  • - {index === 0 ? {'>'} : null} - {nestedView} - {index < currentNestedDepth - 1 ? ( - {'>'} - ) : null} -
  • - ); - }; - - const BorderElement = props => { - return ( -
    - {props.children} -
    - ); - }; - - const BorderContainer = props => { - let output = BorderElement(props); - for (let i = 0; i < currentNestedDepth; i++) { - output = {output}; - } - return output; - }; - - const renderBreadCrumb = () => { - const breadContainerStyle: React.CSSProperties = { - position: 'absolute', - display: 'flex', - width: '100%', - marginTop: '-1rem', - }; - const olStyles: React.CSSProperties = { - margin: 0, - padding: 0, - display: 'flex', - listStyle: 'none', - listStyleImage: 'none', - minWidth: '1rem', - }; - const headerStyle: React.CSSProperties = { - color: BREAD_COLOR_ACTIVE, - fontSize: BREAD_FONT_SIZE, - margin: 0, - padding: 0, - }; - - const rootClick = currentNestedDepth > 0 ? handleRootClick : undefined; - return ( -
    -
    - {text} -
    -
      - {currentNestedView?.map((nestedView, i) => { - return ( - - ); - })} -
    -
    - ); - }; - + const nodeExecutionStatus = + nodeExecution?.closure?.phase || initialNodeExecutionStatus; return ( <> - {renderBreadCrumb()} - + + {currentNestedView?.map((nestedView, viewIndex) => { + return ( + { + e.stopPropagation(); + onRemoveNestedView(scopedId, viewIndex); + }} + /> + ); + })} + + {renderDefaultHandles( scopedId, getGraphHandleStyle('source'), diff --git a/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx b/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx index 7b1e6d0d1..c7a218b33 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, render } from '@testing-library/react'; import { NodeExecutionDetailsContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { mockWorkflowId } from 'mocks/data/fixtures/types'; import { dTypes } from 'models/Graph/types'; -import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { dateToTimestamp } from 'common/utils'; import { PausedTasksComponent } from '../PausedTasksComponent'; @@ -96,11 +96,11 @@ describe('flytegraph > ReactFlow > PausedTasksComponent', () => { compiledWorkflowClosure, }} > - {} }} > - + , ); diff --git a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index fc834e33b..5e440e434 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -53,8 +53,6 @@ interface BuildDataProps { onNodeSelectionChanged: any; onPhaseSelectionChanged: (phase: TaskExecutionPhase) => void; selectedPhase: TaskExecutionPhase; - onAddNestedView: any; - onRemoveNestedView: any; rootParentNode: dNode; currentNestedView: string[]; } @@ -64,8 +62,6 @@ const buildReactFlowDataProps = ({ onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, - onAddNestedView, - onRemoveNestedView, rootParentNode, currentNestedView, }: BuildDataProps) => { @@ -112,6 +108,7 @@ const buildReactFlowDataProps = ({ taskType, nodeLogsByPhase, isParentNode, + parentScopedId: rootParentNode ? rootParentNode.scopedId : scopedId, cacheStatus, selectedPhase, onNodeSelectionChanged: () => { @@ -124,16 +121,6 @@ const buildReactFlowDataProps = ({ onPhaseSelectionChanged(phase); } }, - onAddNestedView: () => { - onAddNestedView( - { - parent: rootParentNode ? rootParentNode.scopedId : scopedId, - view: scopedId, - }, - node, - ); - }, - onRemoveNestedView, }; for (const rootParentId in currentNestedView) { @@ -225,8 +212,6 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, - onAddNestedView, - onRemoveNestedView, currentNestedView, isStaticGraph, } = props; @@ -235,8 +220,6 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, - onAddNestedView, - onRemoveNestedView, currentNestedView, }; const root: ReactFlowGraph = { diff --git a/packages/console/src/components/flytegraph/ReactFlow/types.ts b/packages/console/src/components/flytegraph/ReactFlow/types.ts index 8c39c8f3a..1bcbc1c84 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/types.ts +++ b/packages/console/src/components/flytegraph/ReactFlow/types.ts @@ -48,8 +48,6 @@ export interface BuildRFNodeProps { nodeExecutionsById: any; typeOverride: dTypes | null; onNodeSelectionChanged: any; - onAddNestedView?: any; - onRemoveNestedView?: any; currentNestedView?: any; isStaticGraph: boolean; } @@ -58,8 +56,6 @@ export interface ConvertDagProps { root: dNode; nodeExecutionsById: any; onNodeSelectionChanged: any; - onRemoveNestedView?: any; - onAddNestedView?: any; currentNestedView?: any; maxRenderDepth: number; isStaticGraph?: boolean; @@ -80,14 +76,13 @@ interface RFCustomData { dag: any; taskType: dTypes; cacheStatus: CatalogCacheStatus; + parentScopedId: string; isParentNode: boolean; nodeLogsByPhase: LogsByPhase; selectedPhase: TaskExecutionPhase; currentNestedView: string[]; onNodeSelectionChanged: (n: boolean) => void; onPhaseSelectionChanged: (p?: TaskExecutionPhase) => void; - onAddNestedView: () => void; - onRemoveNestedView: (scopedId: string, index: number) => void; } export interface RFNode { diff --git a/packages/console/src/components/hooks/utils.ts b/packages/console/src/components/hooks/utils.ts index 8769842cb..1e8f7aa5c 100644 --- a/packages/console/src/components/hooks/utils.ts +++ b/packages/console/src/components/hooks/utils.ts @@ -1,17 +1,22 @@ import { CompiledNode, GloballyUniqueNode } from 'models/Node/types'; import { TaskTemplate } from 'models/Task/types'; -import { CompiledWorkflowClosure, Workflow } from 'models/Workflow/types'; +import { + CompiledWorkflow, + CompiledWorkflowClosure, + Workflow, +} from 'models/Workflow/types'; export function extractCompiledNodes( compiledWorkflowClosure: CompiledWorkflowClosure | null, ): CompiledNode[] { if (!compiledWorkflowClosure) return []; - const { primary, subWorkflows = [] } = compiledWorkflowClosure; + const { primary = {} as CompiledWorkflow, subWorkflows = [] } = + compiledWorkflowClosure; return subWorkflows.reduce( (out, subWorkflow) => [...out, ...subWorkflow.template.nodes], - primary.template.nodes, + primary?.template?.nodes, ); } From bb83dd417544e2968dc969cd63a10e3cace2b06b Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 10 Apr 2023 16:52:58 -0700 Subject: [PATCH 14/25] chore: fix map tasks Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionTab.tsx | 24 ++----- .../ExecutionDetails/ExecutionTabView.tsx | 8 +-- .../Executions/Tables/NodeExecutionRow.tsx | 35 ++++++--- .../NodeExecutionDynamicProvider.tsx | 67 ++++++----------- .../WorkflowNodeExecutionsProvider.tsx | 10 ++- .../NodeExecutionDetails/utils.ts | 18 ++++- .../Executions/nodeExecutionQueries.ts | 72 ++++++++++--------- .../src/components/Executions/utils.ts | 6 +- .../WorkflowGraph/WorkflowGraph.tsx | 25 ++----- .../flytegraph/ReactFlow/BreadCrumb.tsx | 42 ++++++----- .../ReactFlow/ReactFlowBreadCrumbProvider.tsx | 19 +---- .../ReactFlow/ReactFlowGraphComponent.tsx | 67 +++++++++-------- .../ReactFlow/customNodeComponents.tsx | 52 ++++++-------- .../ReactFlow/transformDAGToReactFlowV2.tsx | 21 +++--- .../components/flytegraph/ReactFlow/types.ts | 3 +- 15 files changed, 209 insertions(+), 260 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index ae4174519..3958671ff 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -19,38 +19,22 @@ const useStyles = makeStyles((theme: Theme) => ({ })); interface ExecutionTabProps { - executionsContext: IWorkflowNodeExecutionsContext; tabType: string; } /** Contains the available ways to visualize the nodes of a WorkflowExecution */ -export const ExecutionTab: React.FC = ({ - tabType, - executionsContext, -}) => { +export const ExecutionTab: React.FC = ({ tabType }) => { const styles = useStyles(); - const renderContent = (executionsContext: IWorkflowNodeExecutionsContext) => { - switch (tabType) { - case tabs.nodes.id: - return ; - case tabs.graph.id: - return ; - case tabs.timeline.id: - return ; - default: - return null; - } - }; - return (
    - {renderContent(executionsContext)} + {tabType === tabs.nodes.id && } + {tabType === tabs.graph.id && } + {tabType === tabs.timeline.id && }
    - {/* Side panel, shows information for specific node */}
    ); }; diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx index 32d00cc50..408802537 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTabView.tsx @@ -5,7 +5,6 @@ import { useTabState } from 'components/hooks/useTabState'; import { secondaryBackgroundColor } from 'components/Theme/constants'; import { tabs } from './constants'; import { ExecutionTab } from './ExecutionTab'; -import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; const useStyles = makeStyles((theme: Theme) => ({ tabs: { @@ -21,8 +20,6 @@ export const ExecutionTabView: React.FC<{}> = () => { const styles = useStyles(); const tabState = useTabState(tabs, DEFAULT_TAB); - const executionsContext = useNodeExecutionsById(); - return ( <> @@ -31,10 +28,7 @@ export const ExecutionTabView: React.FC<{}> = () => { - + ); }; diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index b899e9632..1bd341a80 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -1,13 +1,12 @@ import React from 'react'; import classnames from 'classnames'; -import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { isEqual } from 'lodash'; import { useTheme } from 'components/Theme/useTheme'; import { makeStyles } from '@material-ui/core'; -import { ignoredNodeIds } from 'models/Node/constants'; import { isExpanded } from 'models/Node/utils'; +import { dateToTimestamp } from 'common/utils'; import { grayedClassName, selectedClassName, @@ -18,7 +17,6 @@ import { useDetailsPanel } from '../ExecutionDetails/DetailsPanelContext'; import { RowExpander } from './RowExpander'; import { calculateNodeExecutionRowLeftSpacing } from './utils'; import { isParentNode } from '../utils'; -import { useNodeExecutionsById } from '../contextProvider/NodeExecutionDetails'; import { useNodeExecutionDynamicContext } from '../contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; const useStyles = makeStyles(theme => ({ @@ -53,8 +51,7 @@ export const NodeExecutionRow: React.FC = ({ const styles = useStyles(); const theme = useTheme(); const tableStyles = useExecutionTableStyles(); - const { childCount, nodeExecution, componentProps } = - useNodeExecutionDynamicContext(); + const { childCount, componentProps } = useNodeExecutionDynamicContext(); // const key = getCacheKey(nodeExecution.id); const nodeLevel = node?.level ?? 0; @@ -74,11 +71,11 @@ export const NodeExecutionRow: React.FC = ({ const { selectedExecution, setSelectedExecution } = useDetailsPanel(); const selected = selectedExecution - ? isEqual(selectedExecution, nodeExecution) + ? isEqual(selectedExecution, node.execution?.id) : false; const expanderContent = React.useMemo(() => { - const isParent = isParentNode(nodeExecution); + const isParent = node?.execution ? isParentNode(node.execution) : false; const isExpandedVal = isExpanded(node); return isParent ? ( @@ -93,13 +90,13 @@ export const NodeExecutionRow: React.FC = ({ ) : (
    ); - }, [node, nodeLevel, nodeExecution, childCount]); + }, [node, nodeLevel, node.execution, childCount]); // 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 const onClickRow = () => - nodeExecution.closure.phase !== NodeExecutionPhase.UNDEFINED && - setSelectedExecution(nodeExecution?.id ?? null); + node?.execution?.closure.phase !== NodeExecutionPhase.UNDEFINED && + setSelectedExecution(node.execution?.id ?? null); return (
    = ({ > {cellRenderer({ node, - execution: nodeExecution, + execution: node.execution || { + closure: { + createdAt: dateToTimestamp(new Date()), + outputUri: '', + phase: NodeExecutionPhase.UNDEFINED, + }, + id: { + executionId: { + domain: node.value?.taskNode?.referenceId?.domain, + name: node.value?.taskNode?.referenceId?.name, + project: node.value?.taskNode?.referenceId?.project, + }, + nodeId: node.id, + }, + inputUri: '', + scopedId: node.scopedId, + }, className: node.grayedOut ? tableStyles.grayed : '', })}
    diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index 55cf2869c..3ee843665 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -7,18 +7,14 @@ import React, { Ref, useState, } from 'react'; -import { dateToTimestamp } from 'common/utils'; -import { - ExecutionContext, - WorkflowNodeExecution, -} from 'components/Executions/contexts'; +import { WorkflowNodeExecution } from 'components/Executions/contexts'; import { useNodeExecutionRow } from 'components/Executions/ExecutionDetails/useNodeExecutionRow'; import { isParentNode, nodeExecutionIsTerminal, } from 'components/Executions/utils'; import { keyBy, values } from 'lodash'; -import { NodeExecution, NodeExecutionPhase } from 'models'; +import { NodeExecution } from 'models'; import { dNode } from 'models/Graph/types'; import { useInView } from 'react-intersection-observer'; import { useQueryClient } from 'react-query'; @@ -28,7 +24,6 @@ export type RefType = Ref; export interface INodeExecutionDynamicContext { context: string; node: dNode; - nodeExecution: WorkflowNodeExecution; childExecutions: WorkflowNodeExecution[]; childCount: number; inView: boolean; @@ -43,7 +38,6 @@ export const NodeExecutionDynamicContext = createContext({ context: 'none', node: {} as dNode, - nodeExecution: undefined as any, childExecutions: [], childCount: 0, inView: false, @@ -79,54 +73,38 @@ const checkEnableChildQuery = ( export type NodeExecutionDynamicProviderProps = PropsWithChildren<{ node: dNode; + overrideInViewValue?: boolean; context?: string; }>; /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ export const NodeExecutionDynamicProvider = ({ node, context, + overrideInViewValue, children, }: NodeExecutionDynamicProviderProps) => { const queryClient = useQueryClient(); const { ref, inView } = useInView(); - const { execution } = useContext(ExecutionContext); + const [overloadedInView, setOverloadedInView] = useState(false); const [fetchedChildCount, setFetchedChildCount] = useState(0); + useEffect(() => { + setOverloadedInView(prev => { + const newVal = + overrideInViewValue === undefined ? inView : overrideInViewValue; + if (newVal === prev) { + return prev; + } + + return newVal; + }); + }, [inView, overrideInViewValue]); // get running data const { setCurrentNodeExecutionsById, nodeExecutionsById } = useNodeExecutionsById(); // get the node execution - const nodeExecution: WorkflowNodeExecution | undefined = useMemo(() => { - if (nodeExecutionsById?.[node.scopedId]) { - return nodeExecutionsById[node.scopedId]; - } - - const splitScope = node.scopedId.split('-'); - const fromUniqueParentId = - splitScope.length > 2 - ? { - fromUniqueParentId: splitScope - .slice(0, splitScope.length - 2) - .join('-'), - } - : {}; - - return { - closure: { - createdAt: dateToTimestamp(new Date()), - outputUri: '', - phase: NodeExecutionPhase.UNDEFINED, - }, - id: { - executionId: execution.id, - nodeId: node.scopedId, - }, - inputUri: '', - scopedId: node.scopedId, - ...fromUniqueParentId, - }; - }, [nodeExecutionsById, node]); + const nodeExecution = node.execution; // useMemo(() => node.execution, [node.execution]); const childExecutions = useMemo(() => { const children = values(nodeExecutionsById).filter(execution => { @@ -139,15 +117,11 @@ export const NodeExecutionDynamicProvider = ({ const { nodeExecutionRowQuery } = useNodeExecutionRow( queryClient, nodeExecution!, - nodeExecutionList => { - if (!nodeExecutionList?.length) { - return true; - } - + () => { const shouldRun = checkEnableChildQuery( childExecutions, nodeExecution!, - inView, + !!overloadedInView, ); return shouldRun; @@ -182,9 +156,8 @@ export const NodeExecutionDynamicProvider = ({ key={node.scopedId} value={{ context: context!, - inView, + inView: overloadedInView, node, - nodeExecution: nodeExecution!, childExecutions, childCount: fetchedChildCount, componentProps: { diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx index 5bc6073bf..c13efdae4 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/WorkflowNodeExecutionsProvider.tsx @@ -21,7 +21,11 @@ import { checkForDynamicExecutions } from 'components/common/utils'; import { useQuery } from 'react-query'; import { convertToPlainNodes } from 'components/Executions/ExecutionDetails/Timeline/helpers'; import { useNodeExecutionContext } from './NodeExecutionDetailsContextProvider'; -import { mapStringifyReplacer, mergeNodeExecutions } from './utils'; +import { + mapStringifyReplacer, + mergeNodeExecutions, + stringifyIsEqual, +} from './utils'; export type WorkflowNodeExecutionsProviderProps = PropsWithChildren<{ initialNodeExecutions?: NodeExecution[]; @@ -114,7 +118,7 @@ export const WorkflowNodeExecutionsProvider = ({ } setDagError(error); setMergedDag(prev => { - if (isEqual(prev, newMergedDag)) { + if (stringifyIsEqual(prev, newMergedDag)) { return prev; } return newMergedDag; @@ -129,7 +133,7 @@ export const WorkflowNodeExecutionsProvider = ({ } }); setInitialDNodes(prev => { - if (isEqual(prev, plainNodes)) { + if (stringifyIsEqual(prev, plainNodes)) { return prev; } return plainNodes; diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts index cfe4c7cef..83370a0cf 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts @@ -1,4 +1,4 @@ -import { merge } from 'lodash'; +import { merge, mergeWith } from 'lodash'; export const mapStringifyReplacer = (key: string, value: any) => { if (value instanceof Map) { @@ -11,7 +11,21 @@ export const mapStringifyReplacer = (key: string, value: any) => { } }; +export const stringifyIsEqual = (a: any, b: any) => { + return ( + JSON.stringify(a, mapStringifyReplacer) === + JSON.stringify(b, mapStringifyReplacer) + ); +}; + export const mergeNodeExecutions = (val, srcVal, _key) => { - const retVal = merge(val, srcVal); + const retVal = mergeWith(val, srcVal, (val, srcVal, _key, ...rest) => { + if (srcVal instanceof Map) { + return new Map([...(val || []), ...srcVal]); + } + const finaVal = + typeof srcVal === 'object' ? merge({ ...val }, { ...srcVal }) : srcVal; + return finaVal; + }); return retVal; }; diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 5dc9ecb16..e23ba8043 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -57,8 +57,8 @@ export const getTaskExecutions = async ( nodeExecution: WorkflowNodeExecution, queryClient, ) => { - if (executionIsTerminal(nodeExecution as any) && nodeExecution.tasksFetched) { - return Promise.resolve(nodeExecution); + if (executionIsTerminal(nodeExecution as any)) { + return nodeExecution; } return await fetchTaskExecutionList( // request listTaskExecutions @@ -90,6 +90,7 @@ export const getTaskExecutions = async ( }; }); }; + /** A query for fetching a single `NodeExecution` by id. */ export function makeNodeExecutionQueryEnhanced( nodeExecution: WorkflowNodeExecution, @@ -111,40 +112,40 @@ export function makeNodeExecutionQueryEnhanced( nodeExecution.scopedId = parentScopeId; // if the node is a parent, force refetch its children + // called by NodeExecutionDynamicProvider const parentExecutionsPromise = isParent - ? fetchNodeExecutionList( - // requests listNodeExecutions - queryClient, - id.executionId, - { - params: { - [nodeExecutionQueryParams.parentNodeId]: parentNodeID, + ? () => + fetchNodeExecutionList( + // requests listNodeExecutions + queryClient, + id.executionId, + { + params: { + [nodeExecutionQueryParams.parentNodeId]: parentNodeID, + }, }, - }, - ) - : Promise.resolve([]); - - const result = await parentExecutionsPromise.then(childExecutions => { - const children = childExecutions.map(e => { - const scopedId = e.metadata?.specNodeId - ? retriesToZero(e?.metadata?.specNodeId) - : retriesToZero(e?.id?.nodeId); - e['scopedId'] = `${parentScopeId}-0-${scopedId}`; - e['fromUniqueParentId'] = parentNodeID; - - return e; - }); - + ).then(childExecutions => { + const children = childExecutions.map(e => { + const scopedId = e.metadata?.specNodeId + ? retriesToZero(e?.metadata?.specNodeId) + : retriesToZero(e?.id?.nodeId); + e['scopedId'] = `${parentScopeId}-0-${scopedId}`; + e['fromUniqueParentId'] = parentNodeID; + + return e; + }); + return [nodeExecution, ...children]; + }) + : () => Promise.resolve([nodeExecution]); + + const result = await parentExecutionsPromise().then(parentAndChildren => { // we don't need to fetch the childrens task executions, this is handled separately // by each row component - const childPromises = children.map(c => Promise.resolve(c)); - - const parentAndChildren = [ - getTaskExecutions(nodeExecution, queryClient), - ...childPromises, - ]; - // const childrenToSkip = nodeExecution.tasksFetched ? children.map(c => c.scopedId as string) : [] - return Promise.all(parentAndChildren); + const childPromises = parentAndChildren.map(c => + getTaskExecutions(c, queryClient), + ); + + return Promise.all(childPromises); }); return result; @@ -199,9 +200,9 @@ export function makeNodeExecutionListQuery( return { queryKey: [QueryType.NodeExecutionList, id, config], queryFn: async () => { - const nodeExecutions = removeSystemNodes( - (await listNodeExecutions(id, config)).entities, - ); + // called by useExecutionNodeViewsStatePoll + const promise = (await listNodeExecutions(id, config)).entities; + const nodeExecutions = removeSystemNodes(promise); nodeExecutions.map(exe => { if (exe.metadata?.specNodeId) { return (exe.scopedId = retriesToZero(exe.metadata.specNodeId)); @@ -210,6 +211,7 @@ export function makeNodeExecutionListQuery( } }); cacheNodeExecutions(queryClient, nodeExecutions); + return nodeExecutions; }, }; diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index 1ec2fbb24..b04df9c02 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -1,5 +1,5 @@ import { durationToMilliseconds, timestampToDate } from 'common/utils'; -import { clone, isEqual, keyBy, merge } from 'lodash'; +import { cloneDeep, keyBy, merge } from 'lodash'; import { runningExecutionStates, terminalExecutionStates, @@ -264,8 +264,8 @@ export async function fetchChildrenExecutions( }); if (childGroupsExecutionsById) { const currentNodeExecutionsById = merge( - nodeExecutionsByIdAdapted, - childGroupsExecutionsById, + cloneDeep(nodeExecutionsByIdAdapted), + cloneDeep(childGroupsExecutionsById), ); setCurrentNodeExecutionsById(currentNodeExecutionsById, true); diff --git a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx index 18915bd66..2c46e75ab 100644 --- a/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { ReactFlowGraphComponent } from 'components/flytegraph/ReactFlow/ReactFlowGraphComponent'; import { NonIdealState } from 'components/common/NonIdealState'; import { CompiledNode } from 'models/Node/types'; -import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; import { ReactFlowBreadCrumbProvider } from 'components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider'; -import { IWorkflowNodeExecutionsContext } from 'components/Executions/contexts'; +import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import t from './strings'; export interface DynamicWorkflowMapping { @@ -13,19 +12,10 @@ export interface DynamicWorkflowMapping { dynamicExecutions: any[]; } -export const WorkflowGraph: React.FC<{ - executionsContext: IWorkflowNodeExecutionsContext; -}> = ({ executionsContext }) => { +export const WorkflowGraph: React.FC<{}> = () => { const { - selectedPhase, - isDetailsTabClosed, - onNodeSelectionChanged, - setSelectedPhase: onPhaseSelectionChanged, - } = useDetailsPanel(); - const { - initialDNodes: initialNodes, dagData: { mergedDag, dagError }, - } = executionsContext; + } = useNodeExecutionsById(); if (dagError) { return ( @@ -48,14 +38,7 @@ export const WorkflowGraph: React.FC<{ return ( - + ); }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx index d5bf3a6a6..ac2e0241c 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx @@ -1,8 +1,12 @@ import React, { PropsWithChildren } from 'react'; -import { useNodeExecutionDynamicContext } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; +import { + NodeExecutionDynamicProvider, + useNodeExecutionDynamicContext, +} from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; -import { NodeExecutionPhase } from 'models'; +import { dNode } from 'models/Graph/types'; import { getNestedContainerStyle } from './utils'; +import { RFCustomData } from './types'; const BREAD_FONT_SIZE = '9px'; const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color; @@ -49,13 +53,13 @@ export const BreadElement = ({ }; const BorderElement = ({ - nodeExecutionStatus, + node, children, }: PropsWithChildren<{ - nodeExecutionStatus: NodeExecutionPhase; + node: dNode; }>) => { const { componentProps } = useNodeExecutionDynamicContext(); - + const nodeExecutionStatus = node.execution?.closure.phase; const borderStyle = getNestedContainerStyle(nodeExecutionStatus); return ( @@ -66,26 +70,26 @@ const BorderElement = ({ }; export const BorderContainer = ({ - nodeExecutionStatus, - currentNestedDepth, + data, children, }: PropsWithChildren<{ - currentNestedDepth: number; - nodeExecutionStatus: NodeExecutionPhase; + data: RFCustomData; }>) => { - let borders = ( - - {children} - - ); - for (let i = 0; i < currentNestedDepth; i++) { + const { node, currentNestedView } = data; + + let contextNode = node; + let borders = {children}; + for (const view of currentNestedView || []) { + contextNode = contextNode?.nodes.find(n => n.scopedId === view)!; borders = ( - - {borders} - + + {borders} + ); } - return borders; }; diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx index 886202d88..26e7a6633 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowBreadCrumbProvider.tsx @@ -1,5 +1,3 @@ -import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { fetchChildrenExecutions } from 'components/Executions/utils'; import React, { createContext, PropsWithChildren, @@ -7,8 +5,6 @@ import React, { Ref, useState, } from 'react'; -import { useQueryClient } from 'react-query'; -import { isUnFetchedDynamicNode } from './utils'; export type RefType = Ref; export interface IReactFlowBreadCrumbContext { @@ -39,25 +35,12 @@ export interface BreadCrumbViews { export const ReactFlowBreadCrumbProvider = ({ children, }: PropsWithChildren<{}>) => { - const queryClient = useQueryClient(); const [currentNestedView, setCurrentNestedView] = useState( {}, ); const currentNestedDepth = (currentNestedView?.length || 0) as any as number; - const { nodeExecutionsById, setCurrentNodeExecutionsById } = - useNodeExecutionsById(); - - const onAddNestedView = async (view, sourceNode: any = null) => { - if (sourceNode && isUnFetchedDynamicNode(sourceNode)) { - await fetchChildrenExecutions( - queryClient, - sourceNode.scopedId, - nodeExecutionsById, - setCurrentNodeExecutionsById, - ); - } - + const onAddNestedView = async view => { const currentView = currentNestedView[view.parent] || []; const newView = { [view.parent]: [...currentView, view.view], diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 0478814c5..57f036adc 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, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; import { useNodeExecutionContext, @@ -8,7 +8,9 @@ import { NodeExecutionPhase } from 'models/Execution/enums'; import { isNodeGateNode } from 'components/Executions/utils'; import { dNode } from 'models/Graph/types'; import { extractCompiledNodes } from 'components/hooks/utils'; -import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; +import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; +import { stringifyIsEqual } from 'components/Executions/contextProvider/NodeExecutionDetails/utils'; +import { RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; import { Legend } from './NodeStatusLegend'; @@ -24,45 +26,55 @@ const containerStyle: React.CSSProperties = { height: '100%', }; -export const ReactFlowGraphComponent = ({ - data, - onNodeSelectionChanged, - onPhaseSelectionChanged, - selectedPhase, - isDetailsTabClosed, - initialNodes, -}) => { - const { nodeExecutionsById, shouldUpdate } = useNodeExecutionsById(); +export const ReactFlowGraphComponent = () => { + const { + selectedPhase, + isDetailsTabClosed, + onNodeSelectionChanged, + setSelectedPhase: onPhaseSelectionChanged, + } = useDetailsPanel(); + const { + nodeExecutionsById, + initialDNodes, + dagData: { mergedDag }, + } = useNodeExecutionsById(); const { compiledWorkflowClosure } = useNodeExecutionContext(); const [pausedNodes, setPausedNodes] = useState([]); const { currentNestedView } = useReactFlowBreadCrumbContext(); - const rfGraphJson = useMemo(() => { - return ConvertFlyteDagToReactFlows({ - root: data, - nodeExecutionsById, + const [rfGraphJson, setrfGraphJson] = useState(); + + useEffect(() => { + const newrfGraphJson = ConvertFlyteDagToReactFlows({ + root: mergedDag, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, maxRenderDepth: 1, currentNestedView, } as ConvertDagProps); + setrfGraphJson(prev => { + if (stringifyIsEqual(prev, newrfGraphJson)) { + return prev; + } + + return newrfGraphJson; + }); }, [ - data, + initialDNodes, + mergedDag, isDetailsTabClosed, - nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, currentNestedView, - shouldUpdate, ]); const backgroundStyle = getRFBackground().nested; useEffect(() => { - const updatedPausedNodes: dNode[] = initialNodes.filter(node => { + const updatedPausedNodes: dNode[] = initialDNodes.filter(node => { const nodeExecution = nodeExecutionsById[node.id]; if (nodeExecution) { const phase = nodeExecution?.closure.phase; @@ -83,15 +95,7 @@ export const ReactFlowGraphComponent = ({ }; }); setPausedNodes(nodesWithExecutions); - }, [initialNodes]); - - const ReactFlowProps: RFWrapperProps = { - backgroundStyle, - rfGraphJson, - type: RFGraphTypes.main, - nodeExecutionsById, - currentNestedView: currentNestedView, - }; + }, [initialDNodes]); return rfGraphJson ? (
    @@ -99,7 +103,12 @@ export const ReactFlowGraphComponent = ({ )} - +
    ) : ( <> diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index 0f6e6a620..b12dd2323 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Handle, Position, ReactFlowProps } from 'react-flow-renderer'; import { dTypes } from 'models/Graph/types'; import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums'; @@ -9,10 +9,7 @@ import { Tooltip } from '@material-ui/core'; import { getNodeFrontendPhase } from 'components/Executions/utils'; import { CacheStatus } from 'components/Executions/CacheStatus'; import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog'; -import { - useNodeExecutionContext, - useNodeExecutionsById, -} from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { extractCompiledNodes } from 'components/hooks/utils'; import { NodeExecutionDynamicProvider, @@ -158,7 +155,7 @@ export const ReactFlowCustomNestedPoint = ({ data }: RFNode) => { */ export const ReactFlowCustomMaxNested = (props: ReactFlowNodeProps) => { return ( - + ); @@ -189,7 +186,7 @@ const ReactFlowCustomMaxNestedInner = ({ data }: RFNode) => { export const ReactFlowStaticNested = (props: ReactFlowNodeProps) => { return ( - + ); @@ -202,7 +199,7 @@ const ReactFlowStaticNestedInner = ({ data }: RFNode) => { export const ReactFlowStaticNode = (props: ReactFlowNodeProps) => { return ( - + ); @@ -269,22 +266,23 @@ const TaskPhaseItem = ({ */ export const ReactFlowGateNode = (props: ReactFlowNodeProps) => { return ( - + ); }; const ReactFlowGateNodeInner = ({ data }: RFNode) => { const { compiledWorkflowClosure } = useNodeExecutionContext(); - const { nodeExecutionsById } = useNodeExecutionsById(); const { + node, nodeType, nodeExecutionStatus, text, scopedId, onNodeSelectionChanged, } = data; - const { componentProps, nodeExecution } = useNodeExecutionDynamicContext(); + const { componentProps } = useNodeExecutionDynamicContext(); + const nodeExecution = node.execution; const phase = getNodeFrontendPhase( nodeExecution?.closure?.phase || nodeExecutionStatus, true, @@ -294,8 +292,8 @@ const ReactFlowGateNodeInner = ({ data }: RFNode) => { const compiledNode = extractCompiledNodes(compiledWorkflowClosure).find( node => - node.id === nodeExecutionsById[scopedId]?.metadata?.specNodeId || - node.id === nodeExecutionsById[scopedId]?.id?.nodeId, + node.id === nodeExecution?.metadata?.specNodeId || + node.id === nodeExecution?.id?.nodeId, ); const iconStyles: React.CSSProperties = { @@ -336,7 +334,7 @@ const ReactFlowGateNodeInner = ({ data }: RFNode) => { @@ -353,7 +351,7 @@ const ReactFlowGateNodeInner = ({ data }: RFNode) => { export type ReactFlowNodeProps = ReactFlowProps & RFNode; export const ReactFlowCustomTaskNode = (props: ReactFlowNodeProps) => { return ( - + ); @@ -361,6 +359,7 @@ export const ReactFlowCustomTaskNode = (props: ReactFlowNodeProps) => { const ReactFlowCustomTaskNodeInner = (props: ReactFlowNodeProps) => { const { data } = props; const { + node, nodeType, nodeExecutionStatus: initialNodeExecutionStatus, selectedPhase: initialPhase, @@ -377,7 +376,8 @@ const ReactFlowCustomTaskNodeInner = (props: ReactFlowNodeProps) => { const [selectedPhase, setSelectedPhase] = useState< TaskExecutionPhase | undefined >(initialPhase); - const { nodeExecution, componentProps } = useNodeExecutionDynamicContext(); + const { componentProps } = useNodeExecutionDynamicContext(); + const nodeExecution = node.execution; const nodeExecutionStatus = nodeExecution?.closure?.phase || initialNodeExecutionStatus; const styles = getGraphNodeStyle(nodeType, nodeExecutionStatus); @@ -495,29 +495,24 @@ const ReactFlowCustomTaskNodeInner = (props: ReactFlowNodeProps) => { */ export const ReactFlowSubWorkflowContainer = (props: ReactFlowNodeProps) => { return ( - + ); }; export const ReactFlowSubWorkflowContainerInner = ({ data }: RFNode) => { - const { - nodeExecutionStatus: initialNodeExecutionStatus, - text, - scopedId, - currentNestedView, - } = data; + const { text, scopedId, currentNestedView } = data; const { onRemoveNestedView } = useReactFlowBreadCrumbContext(); - const { nodeExecution } = useNodeExecutionDynamicContext(); const handleRootClick = () => { onRemoveNestedView(scopedId, -1); }; const currentNestedDepth = currentNestedView?.length || 0; - const nodeExecutionStatus = - nodeExecution?.closure?.phase || initialNodeExecutionStatus; return ( <> { ); })} - + {renderDefaultHandles( scopedId, getGraphHandleStyle('source'), diff --git a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx index 5e440e434..cb5fe808c 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx @@ -49,7 +49,6 @@ export const isStartOrEndEdge = edge => { interface BuildDataProps { node: dNode; - nodeExecutionsById: any; onNodeSelectionChanged: any; onPhaseSelectionChanged: (phase: TaskExecutionPhase) => void; selectedPhase: TaskExecutionPhase; @@ -58,7 +57,6 @@ interface BuildDataProps { } const buildReactFlowDataProps = ({ node, - nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, @@ -71,24 +69,23 @@ const buildReactFlowDataProps = ({ scopedId, type: nodeType, isParentNode, + execution, } = node; const taskType = nodeValue?.template?.type ?? null; const mapNodeExecutionStatus = () => { - if (nodeExecutionsById) { - if (nodeExecutionsById[scopedId]) { - return nodeExecutionsById[scopedId].closure.phase as NodeExecutionPhase; - } else { - return NodeExecutionPhase.SKIPPED; - } + if (execution) { + return ( + (execution.closure.phase as NodeExecutionPhase) || + NodeExecutionPhase.SKIPPED + ); } else { return NodeExecutionPhase.UNDEFINED; } }; const nodeExecutionStatus = mapNodeExecutionStatus(); - const nodeLogsByPhase: LogsByPhase = - nodeExecutionsById?.[node.scopedId]?.logsByPhase; + const nodeLogsByPhase: LogsByPhase = (execution as any)?.logsByPhase; // get the cache status for mapped task const isMapCache = @@ -96,7 +93,7 @@ const buildReactFlowDataProps = ({ const cacheStatus: CatalogCacheStatus = isMapCache ? CatalogCacheStatus.MAP_CACHE - : nodeExecutionsById?.[scopedId]?.closure.taskNodeMetadata?.cacheStatus; + : (execution?.closure.taskNodeMetadata?.cacheStatus as CatalogCacheStatus); const dataProps = { node, @@ -208,7 +205,6 @@ export const nodesToArray = nodes => { export const buildGraphMapping = (props): ReactFlowGraphMapping => { const dag: dNode = props.root; const { - nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, @@ -216,7 +212,6 @@ export const buildGraphMapping = (props): ReactFlowGraphMapping => { isStaticGraph, } = props; const nodeDataProps = { - nodeExecutionsById, onNodeSelectionChanged, onPhaseSelectionChanged, selectedPhase, diff --git a/packages/console/src/components/flytegraph/ReactFlow/types.ts b/packages/console/src/components/flytegraph/ReactFlow/types.ts index 1bcbc1c84..0ebf50ab5 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/types.ts +++ b/packages/console/src/components/flytegraph/ReactFlow/types.ts @@ -54,7 +54,6 @@ export interface BuildRFNodeProps { export interface ConvertDagProps { root: dNode; - nodeExecutionsById: any; onNodeSelectionChanged: any; currentNestedView?: any; maxRenderDepth: number; @@ -66,7 +65,7 @@ export interface DagToReactFlowProps extends ConvertDagProps { parents: any; } -interface RFCustomData { +export interface RFCustomData { node: dNode; nodeExecutionStatus: NodeExecutionPhase; text: string; From 6cb894bfff78099dacae19ab7b72988be5dc9de3 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Tue, 11 Apr 2023 08:45:07 -0700 Subject: [PATCH 15/25] chore: dynamic execution fixes Signed-off-by: Carina Ursu --- .../ExecutionDetails/useNodeExecutionRow.ts | 1 - .../Executions/Tables/NodeExecutionsTable.tsx | 3 +- .../NodeExecutionDynamicProvider.tsx | 10 ++++- .../NodeExecutionDetails/utils.ts | 4 +- .../Executions/nodeExecutionQueries.ts | 40 ++++++++++--------- .../src/components/Executions/types.ts | 6 +++ .../src/components/Executions/utils.ts | 8 ++++ .../components/Workflow/workflowQueries.ts | 5 ++- .../transformerWorkflowToDag.tsx | 2 +- .../console/src/components/common/utils.ts | 2 +- .../ReactFlow/ReactFlowGraphComponent.tsx | 8 +--- .../ReactFlow/customNodeComponents.tsx | 13 +++--- 12 files changed, 61 insertions(+), 41 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts index b1bdfceb9..b714131ff 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts +++ b/packages/console/src/components/Executions/ExecutionDetails/useNodeExecutionRow.ts @@ -16,7 +16,6 @@ export const useNodeExecutionRow = ( { ...makeNodeExecutionQueryEnhanced(execution, queryClient), refetchInterval: nodeExecutionRefreshIntervalMs, - enabled: !!execution, }, shouldEnableQuery, ); diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 4041cb40f..2071cb4ca 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -28,6 +28,7 @@ import { NodeExecutionDynamicProvider } from '../contextProvider/NodeExecutionDe import { ExecutionFilters } from '../ExecutionFilters'; import { ExecutionContext, FilteredNodeExecutions } from '../contexts'; import { useExecutionNodeViewsStatePoll } from '../ExecutionDetails/useExecutionNodeViewsState'; +import { stringifyIsEqual } from '../contextProvider/NodeExecutionDetails/utils'; const scrollbarPadding = scrollbarSize(); @@ -182,7 +183,7 @@ export const NodeExecutionsTable: React.FC<{}> = () => { ? mergeOriginIntoNodes(initialFilteredNodes, plainNodes) : merge(initialNodes, ogn); - if (!isEqual(newNodes, ogn)) { + if (!stringifyIsEqual(newNodes, ogn)) { return newNodes; } diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index 3ee843665..30a8d71e0 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -48,7 +48,7 @@ export const NodeExecutionDynamicContext = const checkEnableChildQuery = ( childExecutions: NodeExecution[], - nodeExecution: NodeExecution, + nodeExecution: WorkflowNodeExecution, inView: boolean, ) => { // check that we fetched all children otherwise force fetch @@ -61,8 +61,14 @@ const checkEnableChildQuery = ( const executionRunning = !nodeExecutionIsTerminal(nodeExecution); + const tasksFetched = nodeExecution.tasksFetched; + const forceRefetch = - inView && (missingChildren || childrenStillRunning || executionRunning); + inView && + (!tasksFetched || + missingChildren || + childrenStillRunning || + executionRunning); // force fetch: // if parent's children haven't been fetched diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts index 83370a0cf..7f71deba4 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/utils.ts @@ -19,9 +19,9 @@ export const stringifyIsEqual = (a: any, b: any) => { }; export const mergeNodeExecutions = (val, srcVal, _key) => { - const retVal = mergeWith(val, srcVal, (val, srcVal, _key, ...rest) => { + const retVal = mergeWith(val, srcVal, (val, srcVal, _key) => { if (srcVal instanceof Map) { - return new Map([...(val || []), ...srcVal]); + return srcVal; } const finaVal = typeof srcVal === 'object' ? merge({ ...val }, { ...srcVal }) : srcVal; diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index e23ba8043..25883754e 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -28,7 +28,7 @@ import { WorkflowNodeExecution } from './contexts'; import { fetchTaskExecutionList } from './taskExecutionQueries'; import { formatRetryAttempt, getGroupedLogs } from './TaskExecutionsList/utils'; import { NodeExecutionGroup } from './types'; -import { executionIsTerminal, isParentNode } from './utils'; +import { isDynamicNode, isParentNode, nodeExecutionIsTerminal } from './utils'; function removeSystemNodes(nodeExecutions: NodeExecution[]): NodeExecution[] { return nodeExecutions.filter(ne => { @@ -57,11 +57,14 @@ export const getTaskExecutions = async ( nodeExecution: WorkflowNodeExecution, queryClient, ) => { - if (executionIsTerminal(nodeExecution as any)) { - return nodeExecution; + const isTerminal = nodeExecutionIsTerminal(nodeExecution); + const tasksFetched = !!nodeExecution.tasksFetched; + const isDynamic = isDynamicNode(nodeExecution); + if (!isDynamic && tasksFetched) { + // return null to signal no change + return; } return await fetchTaskExecutionList( - // request listTaskExecutions queryClient, nodeExecution.id as any, ).then(taskExecutions => { @@ -83,10 +86,13 @@ export const getTaskExecutions = async ( const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); + const appendTasksFetched = !isDynamic || (isDynamic && isTerminal); + + const { closure: _, ...restofNodeExecution } = nodeExecution; return { - ...nodeExecution, + ...restofNodeExecution, ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), - tasksFetched: true, + ...((appendTasksFetched && { tasksFetched: true }) || {}), }; }); }; @@ -99,6 +105,7 @@ export function makeNodeExecutionQueryEnhanced( const { id } = nodeExecution || {}; return { + enabled: !!nodeExecution, queryKey: [QueryType.NodeExecutionAndChilList, id], queryFn: async () => { // complexity: @@ -113,7 +120,7 @@ export function makeNodeExecutionQueryEnhanced( // if the node is a parent, force refetch its children // called by NodeExecutionDynamicProvider - const parentExecutionsPromise = isParent + const parentNodeExecutions = isParent ? () => fetchNodeExecutionList( // requests listNodeExecutions @@ -134,21 +141,18 @@ export function makeNodeExecutionQueryEnhanced( return e; }); - return [nodeExecution, ...children]; + return children; }) - : () => Promise.resolve([nodeExecution]); - - const result = await parentExecutionsPromise().then(parentAndChildren => { - // we don't need to fetch the childrens task executions, this is handled separately - // by each row component - const childPromises = parentAndChildren.map(c => - getTaskExecutions(c, queryClient), - ); + : () => Promise.resolve([]); - return Promise.all(childPromises); + const parentNodeAndTaskExecutions = await Promise.all([ + getTaskExecutions(nodeExecution, queryClient), + parentNodeExecutions(), + ]).then(([parent, children]) => { + return [parent, ...children].filter(n => !!n); }); - return result; + return parentNodeAndTaskExecutions as NodeExecution[]; }, }; } diff --git a/packages/console/src/components/Executions/types.ts b/packages/console/src/components/Executions/types.ts index 3bed86f03..fa61b44f9 100644 --- a/packages/console/src/components/Executions/types.ts +++ b/packages/console/src/components/Executions/types.ts @@ -40,6 +40,12 @@ export interface ParentNodeExecution extends NodeExecution { }; } +export interface DynamicNodeExecution extends NodeExecution { + metadata: NodeExecutionMetadata & { + isDynamic: true; + }; +} + export interface WorkflowNodeExecutionClosure extends NodeExecutionClosure { workflowNodeMetadata: WorkflowNodeMetadata; } diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index b04df9c02..d7329331c 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -35,6 +35,7 @@ import { import { isChildGroupsFetched } from './ExecutionDetails/utils'; import { fetchChildNodeExecutionGroups } from './nodeExecutionQueries'; import { + DynamicNodeExecution, ExecutionPhaseConstants, NodeExecutionDisplayType, ParentNodeExecution, @@ -155,6 +156,13 @@ export function isParentNode( return !!nodeExecution?.metadata?.isParentNode; } +/** Indicates if a NodeExecution is explicitly marked as a parent node. */ +export function isDynamicNode( + nodeExecution: NodeExecution, +): nodeExecution is DynamicNodeExecution { + return !!nodeExecution?.metadata?.isDynamic; +} + export function flattenBranchNodes(node: CompiledNode): CompiledNode[] { const ifElse = node.branchNode?.ifElse; if (!ifElse) { diff --git a/packages/console/src/components/Workflow/workflowQueries.ts b/packages/console/src/components/Workflow/workflowQueries.ts index af3e5290e..8545b5f75 100644 --- a/packages/console/src/components/Workflow/workflowQueries.ts +++ b/packages/console/src/components/Workflow/workflowQueries.ts @@ -36,11 +36,14 @@ export interface NodeExecutionDynamicWorkflowQueryResult { export function makeNodeExecutionDynamicWorkflowQuery( parentsToFetch, ): QueryInput { + const parentsIds = Object.keys(parentsToFetch); return { queryKey: [QueryType.DynamicWorkflowFromNodeExecution, parentsToFetch], + // don't make any requests as long as there are no dynamic node executions to fetch + enabled: !!parentsIds?.length, queryFn: async () => { return await Promise.all( - Object.keys(parentsToFetch) + parentsIds .filter(id => parentsToFetch[id]) .map(id => { const executionId = parentsToFetch[id]; diff --git a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx index aec7079c6..de3dcd148 100644 --- a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx +++ b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -499,7 +499,7 @@ export const transformerWorkflowToDag = ( workflow: CompiledWorkflowClosure, dynamicToMerge: any | null = null, nodeExecutionsById = {}, -): any => { +) => { const { primary } = workflow; const staticExecutionIdsMap = {}; diff --git a/packages/console/src/components/common/utils.ts b/packages/console/src/components/common/utils.ts index e4ab7f3e7..46bd4828b 100644 --- a/packages/console/src/components/common/utils.ts +++ b/packages/console/src/components/common/utils.ts @@ -30,7 +30,7 @@ export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { 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 57f036adc..60af621f7 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -54,13 +54,7 @@ export const ReactFlowGraphComponent = () => { maxRenderDepth: 1, currentNestedView, } as ConvertDagProps); - setrfGraphJson(prev => { - if (stringifyIsEqual(prev, newrfGraphJson)) { - return prev; - } - - return newrfGraphJson; - }); + setrfGraphJson(newrfGraphJson); }, [ initialDNodes, mergedDag, diff --git a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx index b12dd2323..7e0b38be3 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx @@ -383,18 +383,17 @@ const ReactFlowCustomTaskNodeInner = (props: ReactFlowNodeProps) => { const styles = getGraphNodeStyle(nodeType, nodeExecutionStatus); useEffect(() => { - if (selectedNode === true) { + // if the node execution isn't there + // (like in the case of a dynamic flow when the node execution isn't available yet) + // bringing up the context pane will result in an infinite loop + // checking if the node execution is present prevents that from happening + if (selectedNode === true && nodeExecution) { onNodeSelectionChanged(selectedNode); setSelectedNode(false); onPhaseSelectionChanged(selectedPhase); setSelectedPhase(selectedPhase); } - }, [ - selectedNode, - onNodeSelectionChanged, - selectedPhase, - onPhaseSelectionChanged, - ]); + }, [selectedNode, selectedPhase]); const mapTaskContainerStyle: React.CSSProperties = { position: 'absolute', From 9a497cb1d5a857fb9ed96ca759fc71d67cfa811a Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Tue, 11 Apr 2023 10:08:28 -0700 Subject: [PATCH 16/25] chore: fixes Signed-off-by: Carina Ursu --- .../NodeExecutionDynamicProvider.tsx | 6 ++-- .../WorkflowNodeExecutionsProvider.tsx | 6 ++-- .../transformerWorkflowToDag.tsx | 7 +++- .../flytegraph/ReactFlow/BreadCrumb.tsx | 36 +++++++++++++++---- .../components/flytegraph/ReactFlow/utils.tsx | 15 +++++++- packages/console/src/models/Graph/types.ts | 3 +- 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index 30a8d71e0..0bb3d0278 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -110,11 +110,11 @@ export const NodeExecutionDynamicProvider = ({ useNodeExecutionsById(); // get the node execution - const nodeExecution = node.execution; // useMemo(() => node.execution, [node.execution]); + const nodeExecution = node?.execution; // useMemo(() => node.execution, [node.execution]); const childExecutions = useMemo(() => { const children = values(nodeExecutionsById).filter(execution => { - return execution.fromUniqueParentId === node.scopedId; + return execution.fromUniqueParentId === node?.scopedId; }); return children; @@ -159,7 +159,7 @@ export const NodeExecutionDynamicProvider = ({ return ( ({}); - const [dagError, setDagError] = useState(null); + const [dagError, setDagError] = useState(); const [mergedDag, setMergedDag] = useState({}); const [initialDNodes, setInitialDNodes] = useState([]); @@ -98,9 +98,9 @@ export const WorkflowNodeExecutionsProvider = ({ dynamicWorkflows, nodeExecutionsById, ) - : { dag: {}, staticExecutionIdsMap: {}, error: null }; + : { dag: {} as dNode, staticExecutionIdsMap: {}, error: undefined }; const { dag, staticExecutionIdsMap, error } = dagData; - const nodes = dag.nodes ?? []; + const nodes = dag?.nodes ?? []; let newMergedDag = dag; diff --git a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx index de3dcd148..d5a202ae7 100644 --- a/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx +++ b/packages/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx @@ -495,11 +495,16 @@ const parseWorkflow = ( * @param context input can be either CompiledWorkflow or CompiledNode * @returns Display name */ +export interface TransformerWorkflowToDag { + dag: dNode; + staticExecutionIdsMap: {}; + error?: Error; +} export const transformerWorkflowToDag = ( workflow: CompiledWorkflowClosure, dynamicToMerge: any | null = null, nodeExecutionsById = {}, -) => { +): TransformerWorkflowToDag => { const { primary } = workflow; const staticExecutionIdsMap = {}; diff --git a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx index ac2e0241c..b3301c015 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx @@ -5,7 +5,8 @@ import { } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; import { dNode } from 'models/Graph/types'; -import { getNestedContainerStyle } from './utils'; +import { NodeExecutionPhase } from 'models'; +import { findNodeInDag, getNestedContainerStyle } from './utils'; import { RFCustomData } from './types'; const BREAD_FONT_SIZE = '9px'; @@ -54,12 +55,16 @@ export const BreadElement = ({ const BorderElement = ({ node, + initialNodeExecutionStatus, children, }: PropsWithChildren<{ node: dNode; + initialNodeExecutionStatus: NodeExecutionPhase; }>) => { const { componentProps } = useNodeExecutionDynamicContext(); - const nodeExecutionStatus = node.execution?.closure.phase; + + const nodeExecutionStatus = + node?.execution?.closure.phase || initialNodeExecutionStatus; const borderStyle = getNestedContainerStyle(nodeExecutionStatus); return ( @@ -75,19 +80,36 @@ export const BorderContainer = ({ }: PropsWithChildren<{ data: RFCustomData; }>) => { - const { node, currentNestedView } = data; + const { node, currentNestedView, nodeExecutionStatus } = data; let contextNode = node; - let borders = {children}; + let borders = ( + + {children} + + ); for (const view of currentNestedView || []) { - contextNode = contextNode?.nodes.find(n => n.scopedId === view)!; - borders = ( + contextNode = findNodeInDag(view, contextNode); + + borders = contextNode ? ( - {borders} + + {borders} + + ) : ( + + {borders} + ); } return borders; diff --git a/packages/console/src/components/flytegraph/ReactFlow/utils.tsx b/packages/console/src/components/flytegraph/ReactFlow/utils.tsx index 9fa902b3f..b5e34de70 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/utils.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/utils.tsx @@ -1,6 +1,6 @@ import React, { CSSProperties } from 'react'; import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums'; -import { dTypes } from 'models/Graph/types'; +import { dNode, dTypes } from 'models/Graph/types'; import { graphStatusColors } from 'components/Theme/constants'; import { nodeExecutionPhaseConstants } from 'components/Executions/constants'; import dagre from 'dagre'; @@ -151,6 +151,19 @@ export const getNestedGraphContainerStyle = overwrite => { return output; }; +export const findNodeInDag = (scopedId: string, root: dNode) => { + if (root.scopedId === scopedId) { + return root; + } + + for (const node of root.nodes) { + const tmp = findNodeInDag(scopedId, node); + if (tmp) { + return tmp; + } + } +}; + export const getNestedContainerStyle = nodeExecutionStatus => { const style = { border: `1px dashed ${getStatusColor(nodeExecutionStatus)}`, diff --git a/packages/console/src/models/Graph/types.ts b/packages/console/src/models/Graph/types.ts index fcc92cf07..464eb72df 100644 --- a/packages/console/src/models/Graph/types.ts +++ b/packages/console/src/models/Graph/types.ts @@ -1,3 +1,4 @@ +import { WorkflowNodeExecution } from 'components/Executions/contexts'; import { NodeExecution } from 'models/Execution/types'; import { TaskTemplate } from 'models/Task/types'; @@ -60,6 +61,6 @@ export interface dNode { expanded?: boolean; grayedOut?: boolean; level?: number; - execution?: NodeExecution; + execution?: WorkflowNodeExecution; isParentNode?: boolean; } From 40f690a16bf041423f13d1d8d33b5d451eadcb1e Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Tue, 11 Apr 2023 23:06:59 -0700 Subject: [PATCH 17/25] chore: cleanup Signed-off-by: Carina Ursu --- .../TaskExecutionNodeRenderer/TaskExecutionNode.tsx | 2 +- .../src/components/Executions/Tables/NodeExecutionRow.tsx | 1 - packages/console/src/components/Executions/constants.ts | 2 +- .../src/components/Executions/nodeExecutionQueries.ts | 8 ++++---- packages/console/src/components/data/types.ts | 2 +- .../src/components/flytegraph/ReactFlow/BreadCrumb.tsx | 3 +-- .../components/flytegraph/ReactFlow/ReactFlowWrapper.tsx | 3 ++- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx index b6fadc3f6..d796b38bf 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/TaskExecutionNodeRenderer/TaskExecutionNode.tsx @@ -1,10 +1,10 @@ +import React from 'react'; import { useNodeExecutionsById } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { getNodeExecutionPhaseConstants } from 'components/Executions/utils'; 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 React from 'react'; import { StatusIndicator } from './StatusIndicator'; /** Renders DAGNodes with colors based on their node type, as well as dots to diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx index 1bd341a80..03be656a6 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionRow.tsx @@ -52,7 +52,6 @@ export const NodeExecutionRow: React.FC = ({ const theme = useTheme(); const tableStyles = useExecutionTableStyles(); const { childCount, componentProps } = useNodeExecutionDynamicContext(); - // const key = getCacheKey(nodeExecution.id); const nodeLevel = node?.level ?? 0; // For the first level, we want the borders to span the entire table, diff --git a/packages/console/src/components/Executions/constants.ts b/packages/console/src/components/Executions/constants.ts index c62b31373..eb8ad684e 100644 --- a/packages/console/src/components/Executions/constants.ts +++ b/packages/console/src/components/Executions/constants.ts @@ -16,7 +16,7 @@ import t from './strings'; import { ExecutionPhaseConstants, NodeExecutionDisplayType } from './types'; export const executionRefreshIntervalMs = 10000; -export const nodeExecutionRefreshIntervalMs = 2000; +export const nodeExecutionRefreshIntervalMs = 3000; export const noLogsFoundString = t('noLogsFoundString'); /** Shared values for color/text/etc for each execution phase */ diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 25883754e..294be6fdf 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -88,9 +88,10 @@ export const getTaskExecutions = async ( const appendTasksFetched = !isDynamic || (isDynamic && isTerminal); - const { closure: _, ...restofNodeExecution } = nodeExecution; + const { closure: _, metadata: __, ...nodeExecutionLight } = nodeExecution; return { - ...restofNodeExecution, + // to avoid overwiring data from queries that handle status updates + ...nodeExecutionLight, ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), ...((appendTasksFetched && { tasksFetched: true }) || {}), }; @@ -106,7 +107,7 @@ export function makeNodeExecutionQueryEnhanced( return { enabled: !!nodeExecution, - queryKey: [QueryType.NodeExecutionAndChilList, id], + queryKey: [QueryType.NodeExecutionAndChildList, id], queryFn: async () => { // complexity: // +1 for parent node tasks @@ -204,7 +205,6 @@ export function makeNodeExecutionListQuery( return { queryKey: [QueryType.NodeExecutionList, id, config], queryFn: async () => { - // called by useExecutionNodeViewsStatePoll const promise = (await listNodeExecutions(id, config)).entities; const nodeExecutions = removeSystemNodes(promise); nodeExecutions.map(exe => { diff --git a/packages/console/src/components/data/types.ts b/packages/console/src/components/data/types.ts index 9dfeb3bcf..1cb3d3aec 100644 --- a/packages/console/src/components/data/types.ts +++ b/packages/console/src/components/data/types.ts @@ -8,7 +8,7 @@ export enum QueryType { DynamicWorkflowFromNodeExecution = 'DynamicWorkflowFromNodeExecution', NodeExecution = 'nodeExecution', NodeExecutionList = 'nodeExecutionList', - NodeExecutionAndChilList = 'nodeExecutionAndChilcList', + NodeExecutionAndChildList = 'nodeExecutionAndChildList', NodeExecutionChildList = 'nodeExecutionChildList', NodeExecutionTreeList = 'nodeExecutionTreeList', TaskExecution = 'taskExecution', diff --git a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx index b3301c015..3b6a64e31 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/BreadCrumb.tsx @@ -35,8 +35,7 @@ export const BreadElement = ({ padding: '0 .2rem', fontSize: BREAD_FONT_SIZE, }; - // const onClick = - // currentNestedDepth > index + 1 ? handleNestedViewClick : undefined; + return (
  • = ({ const setStateDeduped = (newState: typeof state) => { setState(prevState => { - if (JSON.stringify(prevState) === JSON.stringify(newState)) { + if (stringifyIsEqual(prevState, newState)) { return prevState; } return newState; From 0addfe1223d5362ce87c3423af5d98685009b018 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Wed, 12 Apr 2023 09:40:04 -0700 Subject: [PATCH 18/25] chore: append task executions Signed-off-by: Carina Ursu --- .../console/src/components/Executions/nodeExecutionQueries.ts | 1 + packages/console/src/models/Task/constants.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/console/src/components/Executions/nodeExecutionQueries.ts b/packages/console/src/components/Executions/nodeExecutionQueries.ts index 294be6fdf..1fd2ce457 100644 --- a/packages/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/console/src/components/Executions/nodeExecutionQueries.ts @@ -92,6 +92,7 @@ export const getTaskExecutions = async ( return { // to avoid overwiring data from queries that handle status updates ...nodeExecutionLight, + taskExecutions, ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), ...((appendTasksFetched && { tasksFetched: true }) || {}), }; diff --git a/packages/console/src/models/Task/constants.ts b/packages/console/src/models/Task/constants.ts index 110dfe54c..1f362f014 100644 --- a/packages/console/src/models/Task/constants.ts +++ b/packages/console/src/models/Task/constants.ts @@ -5,6 +5,7 @@ export enum TaskType { ARRAY = 'container_array', BATCH_HIVE = 'batch_hive', DYNAMIC = 'dynamic-task', + BRANCH = 'branch-node', HIVE = 'hive', PYTHON = 'python-task', SIDECAR = 'sidecar', From ae062ad5af07b0ed8937df3cc8c1fc73aebf2b97 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Wed, 12 Apr 2023 16:35:15 -0700 Subject: [PATCH 19/25] chore: fix ExecutionTabContent.test.tsx Signed-off-by: Carina Ursu --- .../Executions/ExecutionDetails/ExecutionDetails.tsx | 2 +- .../Executions/ExecutionDetails/ExecutionTab.tsx | 1 - .../ExecutionDetails/test/ExecutionTabContent.test.tsx | 7 ++----- .../NodeExecutionDetailsContextProvider.tsx | 6 +++--- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx index d7b31dffb..678306280 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionDetails.tsx @@ -64,7 +64,7 @@ const RenderExecutionContainer: React.FC<{}> = () => { {workflow => ( <> {/* Provides a node execution tree for the current workflow */} - +
    diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 3958671ff..13b66d08f 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -6,7 +6,6 @@ import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { DetailsPanelContextProvider } from './DetailsPanelContext'; import { ScaleProvider } from './Timeline/scaleContext'; import { ExecutionTimelineContainer } from './Timeline/ExecutionTimelineContainer'; -import { IWorkflowNodeExecutionsContext } from '../contexts'; const useStyles = makeStyles((theme: Theme) => ({ nodesContainer: { diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx index 6877e51d3..63ddae854 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionTabContent.test.tsx @@ -8,7 +8,7 @@ import { mockServer } from 'mocks/server'; import * as React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { createTestQueryClient } from 'test/utils'; -import { ExecutionTabContent } from '../ExecutionTabContent'; +import { ExecutionTab } from '../ExecutionTab'; import { tabs } from '../constants'; jest.mock('components/Workflow/workflowQueries'); @@ -65,10 +65,7 @@ describe('Executions > ExecutionDetails > ExecutionTabContent', () => { - + , diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx index 46b9d8545..fffad72b9 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx @@ -59,17 +59,17 @@ export const useNodeExecutionContext = (): NodeExecutionDetailsState => useContext(NodeExecutionDetailsContext); export type ProviderProps = PropsWithChildren<{ - workflow: Workflow; + workflowId: Identifier; }>; /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ export const NodeExecutionDetailsContextProvider = ({ - workflow, + workflowId, children, }: ProviderProps) => { // workflow Identifier - separated to parameters, to minimize re-render count // as useEffect doesn't know how to do deep comparison - const { resourceType, project, domain, name, version } = workflow.id; + const { resourceType, project, domain, name, version } = workflowId; const [executionTree, setExecutionTree] = useState( {} as CurrentExecutionDetails, From 602680d0e70fac7cd39326629308cc4b2e844a14 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Wed, 12 Apr 2023 17:43:12 -0700 Subject: [PATCH 20/25] chore: fix WorkflowGraph.test.tsx Signed-off-by: Carina Ursu --- .../src/components/Executions/constants.ts | 1 + .../console/src/components/Theme/constants.ts | 1 + .../WorkflowGraph/test/WorkflowGraph.test.tsx | 46 ++++++++++++------- .../ReactFlow/ReactFlowGraphComponent.tsx | 1 - 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/console/src/components/Executions/constants.ts b/packages/console/src/components/Executions/constants.ts index eb8ad684e..e586872e7 100644 --- a/packages/console/src/components/Executions/constants.ts +++ b/packages/console/src/components/Executions/constants.ts @@ -262,6 +262,7 @@ export const taskTypeToNodeExecutionDisplayType: { [TaskType.MPI]: NodeExecutionDisplayType.MpiTask, [TaskType.ARRAY_AWS]: NodeExecutionDisplayType.ARRAY_AWS, [TaskType.ARRAY_K8S]: NodeExecutionDisplayType.ARRAY_K8S, + [TaskType.BRANCH]: NodeExecutionDisplayType.BranchNode, }; export const cacheStatusMessages: { [k in CatalogCacheStatus]: string } = { diff --git a/packages/console/src/components/Theme/constants.ts b/packages/console/src/components/Theme/constants.ts index 7d464dee2..8837031d5 100644 --- a/packages/console/src/components/Theme/constants.ts +++ b/packages/console/src/components/Theme/constants.ts @@ -97,6 +97,7 @@ export const taskColors: TaskColorMap = { // plugins [TaskType.ARRAY_AWS]: '#E1E8ED', [TaskType.ARRAY_K8S]: '#E1E8ED', + [TaskType.BRANCH]: '#E1E8ED', }; export const bodyFontSize = '0.875rem'; diff --git a/packages/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx b/packages/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx index 45437261b..0e6da9161 100644 --- a/packages/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx +++ b/packages/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx @@ -2,8 +2,16 @@ import { act, render, screen, waitFor } from '@testing-library/react'; import * as React from 'react'; import { createTestQueryClient } from 'test/utils'; import { QueryClient, QueryClientProvider } from 'react-query'; +import { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; import { WorkflowGraph } from '../WorkflowGraph'; +jest.mock('../../flytegraph/ReactFlow/transformDAGToReactFlowV2.tsx', () => ({ + ConvertFlyteDagToReactFlows: jest.fn(() => ({ + nodes: [], + edges: [], + })), +})); + jest.mock('../../flytegraph/ReactFlow/ReactFlowWrapper.tsx', () => ({ ReactFlowWrapper: jest.fn(({ children }) => (
    {children}
    @@ -21,24 +29,30 @@ describe('WorkflowGraph', () => { await act(() => { render( - {}, + setShouldUpdate: () => {}, + shouldUpdate: false, + dagData: { + mergedDag: { + edges: [], + id: 'node', + name: 'node', + nodes: [], + type: 4, + value: { + id: 'name', + }, + }, + dagError: undefined, }, }} - error={null} - dynamicWorkflows={[]} - initialNodes={[]} - /> + > + + , ); }); diff --git a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 60af621f7..ee2f64a7b 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -9,7 +9,6 @@ import { isNodeGateNode } from 'components/Executions/utils'; import { dNode } from 'models/Graph/types'; import { extractCompiledNodes } from 'components/hooks/utils'; import { useDetailsPanel } from 'components/Executions/ExecutionDetails/DetailsPanelContext'; -import { stringifyIsEqual } from 'components/Executions/contextProvider/NodeExecutionDetails/utils'; import { RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; From 48b383143a9dfe5690f4552cf1647df6c62951b9 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 13 Apr 2023 09:20:07 -0700 Subject: [PATCH 21/25] chore: test fixes Signed-off-by: Carina Ursu --- .../test/ExecutionMetadata.test.tsx | 9 ++++++- .../test/ExecutionNodeViews.test.tsx | 5 +++- .../ExecutionDetails/test/TaskNames.test.tsx | 1 - .../Tables/NodeExecutionActions.tsx | 2 +- .../Executions/Tables/NodeExecutionsTable.tsx | 6 +---- .../Tables/test/NodeExecutionActions.test.tsx | 10 +++++-- .../Tables/test/NodeExecutionRow.test.tsx | 26 ++++++++++++++----- .../NodeExecutionDetailsContextProvider.tsx | 4 +-- .../NodeExecutionDynamicProvider.tsx | 5 ---- .../test/NodeExecutionCacheStatus.test.tsx | 4 ++- .../src/components/Executions/utils.ts | 2 +- .../WorkflowGraph/test/utils.test.ts | 3 +-- .../console/src/components/hooks/utils.ts | 7 +++-- 13 files changed, 52 insertions(+), 32 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionMetadata.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionMetadata.test.tsx index 1e67802ba..ead3b180d 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionMetadata.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionMetadata.test.tsx @@ -6,6 +6,7 @@ import { createMockExecution } from 'models/__mocks__/executionsData'; import * as React from 'react'; import { MemoryRouter } from 'react-router'; import { Routes } from 'routes/routes'; +import { ExecutionContext } from 'components/Executions/contexts'; import { ExecutionMetadataLabels } from '../constants'; import { ExecutionMetadata } from '../ExecutionMetadata'; @@ -28,7 +29,13 @@ describe('ExecutionMetadata', () => { const renderMetadata = () => render( - + + + , ); diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx index 84a88f982..eb2302db5 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx @@ -8,6 +8,7 @@ import { Execution } from 'models/Execution/types'; import * as React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { createTestQueryClient } from 'test/utils'; +import { ExecutionContext } from 'components/Executions/contexts'; import { tabs } from '../constants'; import { ExecutionNodeViews } from '../ExecutionNodeViews'; @@ -78,7 +79,9 @@ describe('ExecutionNodeViews', () => { const renderViews = () => render( - + + + , ); 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 de85fb80f..67ac4c53c 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx @@ -7,7 +7,6 @@ import { WorkflowNodeExecutionsContext } 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(); diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx index 69a2a9d57..79cfd5967 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionActions.tsx @@ -50,7 +50,7 @@ export const NodeExecutionActions = ({ ); const phase = getNodeFrontendPhase(execution.closure.phase, isGateNode); - const compiledNode = extractCompiledNodes(compiledWorkflowClosure).find( + const compiledNode = extractCompiledNodes(compiledWorkflowClosure)?.find( node => node.id === execution.metadata?.specNodeId || node.id === execution.id.nodeId, diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 2071cb4ca..5a4b87a42 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -259,11 +259,7 @@ export const NodeExecutionsTable: React.FC<{}> = () => { {showNodes.length > 0 ? ( showNodes.map(node => { return ( - + Tables > NodeExecutionActions', () => { beforeEach(() => { fixture = basicPythonWorkflow.generate(); - execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; + execution = cloneDeep( + fixture.workflowExecutions.top.nodeExecutions.pythonNode.data, + ); queryClient = createTestQueryClient(); insertFixture(mockServer, fixture); fetchWorkflow.mockImplementation(() => @@ -40,7 +44,9 @@ describe('Executions > Tables > NodeExecutionActions', () => { render( - + + + , ); 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 fe1419661..a6ebf92bc 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionRow.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionRow.test.tsx @@ -8,14 +8,16 @@ import { insertFixture } from 'mocks/data/insertFixture'; import { mockServer } from 'mocks/server'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { NodeExecution } from 'models/Execution/types'; -import { dTypes } from 'models/Graph/types'; +import { dNode, dTypes } from 'models/Graph/types'; +import { NodeExecutionDynamicContext } from 'components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; +import { cloneDeep } from 'lodash'; import { NodeExecutionRow } from '../NodeExecutionRow'; jest.mock('components/Workflow/workflowQueries'); const { fetchWorkflow } = require('components/Workflow/workflowQueries'); const columns = []; -const node = { +const node: dNode = { id: 'n1', scopedId: 'n1', type: dTypes.start, @@ -33,6 +35,7 @@ describe('Executions > Tables > NodeExecutionRow', () => { beforeEach(() => { fixture = basicPythonWorkflow.generate(); execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; + node.execution = cloneDeep(execution); queryClient = createTestQueryClient(); insertFixture(mockServer, fixture); fetchWorkflow.mockImplementation(() => @@ -41,10 +44,23 @@ describe('Executions > Tables > NodeExecutionRow', () => { }); const renderComponent = props => { + const { node } = props; return render( - + n.execution), + componentProps: { + ref: null, + }, + inView: false, + }} + > + + , ); @@ -53,7 +69,6 @@ describe('Executions > Tables > NodeExecutionRow', () => { const { queryByRole, queryByTestId } = renderComponent({ columns, node, - nodeExecution: execution, onToggle, }); await waitFor(() => queryByRole('listitem')); @@ -63,13 +78,12 @@ describe('Executions > Tables > NodeExecutionRow', () => { }); it('should render expander if node contains list of nodes', async () => { + node.execution!.metadata!.isParentNode = true; const mockNode = { ...node, nodes: [node, node], }; - (execution.metadata as any).isParentNode = true; - const { queryByRole, queryByTitle } = renderComponent({ columns, node: mockNode, diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx index fffad72b9..0721cd434 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDetailsContextProvider.tsx @@ -151,8 +151,8 @@ export const NodeExecutionDetailsContextProvider = ({ nodeExecution.scopedId || nodeExecution.metadata?.specNodeId || nodeExecution.id.nodeId; - const nodeDetail = executionTree.nodes.filter(n => n.scopedId === specId); - if (nodeDetail.length === 0) { + const nodeDetail = executionTree.nodes?.filter(n => n.scopedId === specId); + if (nodeDetail?.length === 0) { let details = tasks.get(nodeExecution.id.nodeId); if (details) { // we already have looked for it and found diff --git a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx index 0bb3d0278..a17861e7f 100644 --- a/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx +++ b/packages/console/src/components/Executions/contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider.tsx @@ -22,7 +22,6 @@ import { useNodeExecutionsById } from './WorkflowNodeExecutionsProvider'; export type RefType = Ref; export interface INodeExecutionDynamicContext { - context: string; node: dNode; childExecutions: WorkflowNodeExecution[]; childCount: number; @@ -36,7 +35,6 @@ export interface INodeExecutionDynamicContext { export const NodeExecutionDynamicContext = createContext({ - context: 'none', node: {} as dNode, childExecutions: [], childCount: 0, @@ -80,12 +78,10 @@ const checkEnableChildQuery = ( export type NodeExecutionDynamicProviderProps = PropsWithChildren<{ node: dNode; overrideInViewValue?: boolean; - context?: string; }>; /** Should wrap "top level" component in Execution view, will build a nodeExecutions tree for specific workflow */ export const NodeExecutionDynamicProvider = ({ node, - context, overrideInViewValue, children, }: NodeExecutionDynamicProviderProps) => { @@ -161,7 +157,6 @@ export const NodeExecutionDynamicProvider = ({ NodeExecutionCacheStatus', () => { ); it('should not render anything, if cacheStatus is undefined', async () => { - const { container } = renderComponent({ execution }); + const ex = cloneDeep(execution); + const { container } = renderComponent({ execution: ex }); await waitFor(() => container); expect(container).toBeEmptyDOMElement(); diff --git a/packages/console/src/components/Executions/utils.ts b/packages/console/src/components/Executions/utils.ts index d7329331c..9bcd9cca6 100644 --- a/packages/console/src/components/Executions/utils.ts +++ b/packages/console/src/components/Executions/utils.ts @@ -208,7 +208,7 @@ export function isExecutionArchived(execution: Execution): boolean { /** Returns true if current node (by nodeId) has 'gateNode' field in the list of nodes on compiledWorkflowClosure */ export function isNodeGateNode(nodes: CompiledNode[], id: string): boolean { - const node = nodes.find(n => n.id === id); + const node = nodes?.find(n => n.id === id); return !!node?.gateNode; } diff --git a/packages/console/src/components/WorkflowGraph/test/utils.test.ts b/packages/console/src/components/WorkflowGraph/test/utils.test.ts index 6debf085b..99c2cd3cf 100644 --- a/packages/console/src/components/WorkflowGraph/test/utils.test.ts +++ b/packages/console/src/components/WorkflowGraph/test/utils.test.ts @@ -5,13 +5,12 @@ import { mockCompiledTaskNode, } from 'models/__mocks__/graphWorkflowData'; import { dTypes } from 'models/Graph/types'; +import { isEndNode, isStartNode } from 'models/Node/utils'; import { DISPLAY_NAME_START, DISPLAY_NAME_END, getDisplayName, getNodeTypeFromCompiledNode, - isStartNode, - isEndNode, getNodeTemplateName, } from '../utils'; diff --git a/packages/console/src/components/hooks/utils.ts b/packages/console/src/components/hooks/utils.ts index 1e8f7aa5c..b4551a321 100644 --- a/packages/console/src/components/hooks/utils.ts +++ b/packages/console/src/components/hooks/utils.ts @@ -14,10 +14,9 @@ export function extractCompiledNodes( const { primary = {} as CompiledWorkflow, subWorkflows = [] } = compiledWorkflowClosure; - return subWorkflows.reduce( - (out, subWorkflow) => [...out, ...subWorkflow.template.nodes], - primary?.template?.nodes, - ); + return subWorkflows.reduce((out, subWorkflow) => { + return [...out, ...subWorkflow.template.nodes]; + }, primary?.template?.nodes); } export function extractTaskTemplates(workflow: Workflow): TaskTemplate[] { From 75f87b34ec0af30a92b0a218e0097b569f1fa6c6 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 13 Apr 2023 11:52:34 -0700 Subject: [PATCH 22/25] chore: more tests Signed-off-by: Carina Ursu --- .../ExecutionDetailsAppBarContent.test.tsx | 2 +- .../NodeExecutionDetailsPanelContent.test.tsx | 7 +- .../Tables/test/NodeExecutionsTable.test.tsx | 204 ++++++++++++++---- 3 files changed, 169 insertions(+), 44 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx index 9577f43b5..39646f3d7 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionDetailsAppBarContent.test.tsx @@ -47,7 +47,7 @@ describe('ExecutionDetailsAppBarContent', () => { - + , diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx index 6f414fdaf..57dd268f1 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { render, waitFor } from '@testing-library/react'; import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; @@ -6,12 +7,16 @@ import { insertFixture } from 'mocks/data/insertFixture'; import { mockServer } from 'mocks/server'; import { TaskExecutionPhase } from 'models/Execution/enums'; import { NodeExecution } from 'models/Execution/types'; -import * as React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { MemoryRouter } from 'react-router'; import { createTestQueryClient } from 'test/utils'; import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent'; +jest.mock('components/Executions/ExecutionDetails/ExecutionDetailsActions', () => ({ + ExecutionDetailsActions: jest.fn(() => ( +
    + )) +})); jest.mock('components/Workflow/workflowQueries'); const { fetchWorkflow } = require('components/Workflow/workflowQueries'); 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 6b14449c8..3725f1dc8 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -1,6 +1,11 @@ import { render, waitFor } from '@testing-library/react'; -import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; -import { WorkflowNodeExecutionsContext } from 'components/Executions/contexts'; +import { + NodeExecutionDetailsContextProvider, +} from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { + ExecutionContext, + WorkflowNodeExecutionsContext, +} from 'components/Executions/contexts'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import { noExecutionsFoundString } from 'common/constants'; import { mockWorkflowId } from 'mocks/data/fixtures/types'; @@ -13,6 +18,8 @@ import { QueryClient, QueryClientProvider } from 'react-query'; import { createTestQueryClient } from 'test/utils'; import { dNode } from 'models/Graph/types'; import { useNodeExecutionFiltersState } from 'components/Executions/filters/useExecutionFiltersState'; +import { Execution, NodeExecution } from 'models'; +import { listNodeExecutions, listTaskExecutions } from 'models/Execution/api'; import { NodeExecutionsTable } from '../NodeExecutionsTable'; jest.mock('components/Workflow/workflowQueries'); @@ -27,16 +34,23 @@ mockUseNodeExecutionFiltersState.mockReturnValue({ }); jest.mock('components/Executions/Tables/NodeExecutionRow', () => ({ - NodeExecutionRow: jest.fn(({ nodeExecution }) => ( + NodeExecutionRow: jest.fn(({ node }) => (
    -
    {nodeExecution?.id?.nodeId}
    +
    + {node?.execution?.id?.nodeId} +
    - {nodeExecution?.closure?.phase} + {node?.execution?.closure?.phase}
    )), })); +jest.mock('models/Execution/api', () => ({ + listNodeExecutions: jest.fn(), + listTaskExecutions: jest.fn(), +})); + const mockNodes = (n: number): dNode[] => { const nodes: dNode[] = []; for (let i = 1; i <= n; i++) { @@ -51,6 +65,7 @@ const mockNodes = (n: number): dNode[] => { } return nodes; }; +const executionId = { domain: 'domain', name: 'name', project: 'project' }; const mockExecutionsById = (n: number, phases: NodeExecutionPhase[]) => { const nodeExecutionsById = {}; @@ -63,7 +78,7 @@ const mockExecutionsById = (n: number, phases: NodeExecutionPhase[]) => { phase: phases[i - 1], }, id: { - executionId: { domain: 'domain', name: 'name', project: 'project' }, + executionId, nodeId: `node${i}`, }, inputUri: '', @@ -85,24 +100,47 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { fetchWorkflow.mockImplementation(() => Promise.resolve(fixture.workflows.top), ); + + listNodeExecutions.mockImplementation(() => { + return Promise.resolve({ + entities: Object.values([]), + }); + }); + listTaskExecutions.mockImplementation(() => { + return Promise.resolve({ + entities: Object.values([]), + }); + }); }); - const renderTable = ({ nodeExecutionsById, initialNodes, filteredNodes }) => + const renderTable = ({ nodeExecutionsById, initialNodes }) => render( - - {}, - }} - > - - - + + + {}, + setShouldUpdate: () => {}, + shouldUpdate: false, + }} + > + + + + , ); @@ -110,7 +148,6 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const { queryByText, queryByTestId } = renderTable({ initialNodes: [], nodeExecutionsById: {}, - filteredNodes: [], }); await waitFor(() => queryByText(noExecutionsFoundString)); @@ -126,14 +163,13 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const { queryAllByTestId } = renderTable({ initialNodes, nodeExecutionsById, - filteredNodes: undefined, }); - await waitFor(() => queryAllByTestId('node-execution-row')); - - expect(queryAllByTestId('node-execution-row')).toHaveLength( - initialNodes.length, - ); + await waitFor(() => { + const nodes = queryAllByTestId('node-execution-row'); + expect(nodes).toHaveLength(initialNodes.length); + return nodes; + }); const ids = queryAllByTestId('node-execution-col-id'); expect(ids).toHaveLength(initialNodes.length); const renderedPhases = queryAllByTestId('node-execution-col-phase'); @@ -147,14 +183,26 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { it('renders NodeExecutionRows with initialNodes even when filterNodes were provided, if appliedFilters is empty', async () => { const phases = [NodeExecutionPhase.FAILED, NodeExecutionPhase.SUCCEEDED]; const nodeExecutionsById = mockExecutionsById(2, phases); - const filteredNodes = mockNodes(1); + const filteredNodeExecutions = nodeExecutionsById['n1']; + listNodeExecutions.mockImplementation(() => { + return Promise.resolve({ + entities: filteredNodeExecutions, + }); + }); const { queryAllByTestId } = renderTable({ initialNodes, nodeExecutionsById, - filteredNodes, }); + await waitFor(() => + expect(listNodeExecutions).toHaveBeenCalledWith( + expect.objectContaining(executionId), + expect.objectContaining({ + filter: [], + }), + ), + ); await waitFor(() => queryAllByTestId('node-execution-row')); expect(queryAllByTestId('node-execution-row')).toHaveLength( @@ -170,35 +218,107 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { } }); - it('renders NodeExecutionRows with filterNodes if appliedFilters is not empty', async () => { - mockUseNodeExecutionFiltersState.mockReturnValueOnce({ + it('renders NodeExecutionRows with filterNodes if appliedFilters are less than original filters', async () => { + const appliedFilters = [ + { key: 'phase', operation: 'value_in', value: ['FAILED'] }, + ]; + mockUseNodeExecutionFiltersState.mockReturnValue({ filters: [], - appliedFilters: [ - { key: 'phase', operation: 'value_in', value: ['FAILED', 'SUCCEEDED'] }, - ], + appliedFilters, + }); + + const phases = [NodeExecutionPhase.FAILED, NodeExecutionPhase.SUCCEEDED]; + const nodeExecutionsById = mockExecutionsById(2, phases); + const filteredNodeExecutions = [nodeExecutionsById['n1']]; + listNodeExecutions.mockImplementation(() => { + return Promise.resolve({ + entities: filteredNodeExecutions, + }); + }); + + const { queryAllByTestId, debug, container } = renderTable({ + initialNodes, + nodeExecutionsById, + }); + + await waitFor(() => + expect(listNodeExecutions).toHaveBeenCalledWith( + expect.objectContaining(executionId), + expect.objectContaining({ + filter: appliedFilters, + }), + ), + ); + + await waitFor(() => { + const rows = queryAllByTestId('node-execution-row'); + return rows.length === filteredNodeExecutions.length; }); + expect(queryAllByTestId('node-execution-row')).toHaveLength( + filteredNodeExecutions.length, + ); + + const ids = queryAllByTestId('node-execution-col-id'); + expect(ids).toHaveLength(filteredNodeExecutions.length); + const renderedPhases = queryAllByTestId('node-execution-col-phase'); + expect(renderedPhases).toHaveLength(filteredNodeExecutions.length); + debug(container) + + for (const i in filteredNodeExecutions) { + expect(ids[i]).toHaveTextContent(filteredNodeExecutions[i].id?.nodeId); + expect(renderedPhases[i]).toHaveTextContent(phases[i].toString()); + } + }); + + it('renders NodeExecutionRows with filterNodes if appliedFilters are the same as original filters', async () => { const phases = [NodeExecutionPhase.FAILED, NodeExecutionPhase.SUCCEEDED]; + const appliedFilters = [ + { key: 'phase', operation: 'value_in', value: ['FAILED', 'SUCCEEDED'] }, + ]; + mockUseNodeExecutionFiltersState.mockReturnValue({ + filters: [], + appliedFilters, + }); + const nodeExecutionsById = mockExecutionsById(2, phases); - const filteredNodes = mockNodes(1); + const filteredNodeExecutions: NodeExecution[] = Object.values(nodeExecutionsById); + listNodeExecutions.mockImplementation(() => { + return Promise.resolve({ + entities: filteredNodeExecutions, + }); + }); const { queryAllByTestId } = renderTable({ initialNodes, nodeExecutionsById, - filteredNodes, }); - await waitFor(() => queryAllByTestId('node-execution-row')); + await waitFor(() => + expect(listNodeExecutions).toHaveBeenCalledWith( + expect.objectContaining(executionId), + expect.objectContaining({ + filter: appliedFilters, + }), + ), + ); + + await waitFor(() => { + const rows = queryAllByTestId('node-execution-row'); + return rows.length === filteredNodeExecutions.length; + }); expect(queryAllByTestId('node-execution-row')).toHaveLength( - filteredNodes.length, + filteredNodeExecutions.length, ); + const ids = queryAllByTestId('node-execution-col-id'); - expect(ids).toHaveLength(filteredNodes.length); + expect(ids).toHaveLength(filteredNodeExecutions.length); const renderedPhases = queryAllByTestId('node-execution-col-phase'); - expect(renderedPhases).toHaveLength(filteredNodes.length); - for (const i in filteredNodes) { - expect(ids[i]).toHaveTextContent(filteredNodes[i].id); + expect(renderedPhases).toHaveLength(filteredNodeExecutions.length); + + for (const i in filteredNodeExecutions) { + expect(ids[i]).toHaveTextContent(filteredNodeExecutions[i].id?.nodeId); expect(renderedPhases[i]).toHaveTextContent(phases[i].toString()); } }); From 887455a6c2b5d48fd5324b3568b030932acefaf8 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Thu, 13 Apr 2023 14:40:03 -0700 Subject: [PATCH 23/25] chore: test fixes Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionTab.tsx | 6 ++- .../Executions/Tables/NodeExecutionsTable.tsx | 7 ++-- .../Tables/test/NodeExecutionsTable.test.tsx | 40 +++++++++++++------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 13b66d08f..50d235097 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -6,6 +6,7 @@ import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { DetailsPanelContextProvider } from './DetailsPanelContext'; import { ScaleProvider } from './Timeline/scaleContext'; import { ExecutionTimelineContainer } from './Timeline/ExecutionTimelineContainer'; +import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; const useStyles = makeStyles((theme: Theme) => ({ nodesContainer: { @@ -24,12 +25,15 @@ interface ExecutionTabProps { /** Contains the available ways to visualize the nodes of a WorkflowExecution */ export const ExecutionTab: React.FC = ({ tabType }) => { const styles = useStyles(); + const filterState = useNodeExecutionFiltersState(); return (
    - {tabType === tabs.nodes.id && } + {tabType === tabs.nodes.id && ( + + )} {tabType === tabs.graph.id && } {tabType === tabs.timeline.id && }
    diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 5a4b87a42..7a7623c3b 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -21,7 +21,7 @@ import { useNodeExecutionsById, } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionRow } from './NodeExecutionRow'; -import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; +import { ExecutionFiltersState, useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { searchNode } from '../utils'; import { nodeExecutionPhaseConstants } from '../constants'; import { NodeExecutionDynamicProvider } from '../contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; @@ -115,19 +115,18 @@ const isPhaseFilter = (appliedFilters: FilterOperation[]) => { * NodeExecutions are expandable and will potentially render a list of child * TaskExecutions */ -export const NodeExecutionsTable: React.FC<{}> = () => { +export const NodeExecutionsTable: React.FC<{filterState: ExecutionFiltersState}> = ({filterState}) => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); const { execution } = useContext(ExecutionContext); - const filterState = useNodeExecutionFiltersState(); + const { appliedFilters } = filterState; const { nodeExecutionsById, initialDNodes: initialNodes } = useNodeExecutionsById(); // query to get filtered data to narrow down Table outputs const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = useExecutionNodeViewsStatePoll(execution, filterState?.appliedFilters); - const { appliedFilters } = useNodeExecutionFiltersState(); const [showNodes, setShowNodes] = useState([]); const [initialFilteredNodes, setInitialFilteredNodes] = useState< 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 3725f1dc8..7110c89fc 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -1,7 +1,5 @@ import { render, waitFor } from '@testing-library/react'; -import { - NodeExecutionDetailsContextProvider, -} from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { ExecutionContext, WorkflowNodeExecutionsContext, @@ -113,7 +111,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { }); }); - const renderTable = ({ nodeExecutionsById, initialNodes }) => + const renderTable = ({ nodeExecutionsById, initialNodes, filterState }) => render( Tables > NodeExecutionsTable', () => { shouldUpdate: false, }} > - + @@ -145,9 +143,14 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { ); it('renders empty content when there are no nodes', async () => { + const filterState = { + filters: [], + appliedFilters: [], + }; const { queryByText, queryByTestId } = renderTable({ initialNodes: [], nodeExecutionsById: {}, + filterState, }); await waitFor(() => queryByText(noExecutionsFoundString)); @@ -159,10 +162,14 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { it('renders NodeExecutionRows with initialNodes when no filteredNodes were provided', async () => { const phases = [NodeExecutionPhase.FAILED, NodeExecutionPhase.SUCCEEDED]; const nodeExecutionsById = mockExecutionsById(2, phases); - + const filterState = { + filters: [], + appliedFilters: [], + }; const { queryAllByTestId } = renderTable({ initialNodes, nodeExecutionsById, + filterState, }); await waitFor(() => { @@ -184,6 +191,10 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const phases = [NodeExecutionPhase.FAILED, NodeExecutionPhase.SUCCEEDED]; const nodeExecutionsById = mockExecutionsById(2, phases); const filteredNodeExecutions = nodeExecutionsById['n1']; + const filterState = { + filters: [], + appliedFilters: [], + }; listNodeExecutions.mockImplementation(() => { return Promise.resolve({ entities: filteredNodeExecutions, @@ -193,6 +204,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const { queryAllByTestId } = renderTable({ initialNodes, nodeExecutionsById, + filterState, }); await waitFor(() => @@ -222,11 +234,10 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const appliedFilters = [ { key: 'phase', operation: 'value_in', value: ['FAILED'] }, ]; - mockUseNodeExecutionFiltersState.mockReturnValue({ + const filterState = { filters: [], appliedFilters, - }); - + }; const phases = [NodeExecutionPhase.FAILED, NodeExecutionPhase.SUCCEEDED]; const nodeExecutionsById = mockExecutionsById(2, phases); const filteredNodeExecutions = [nodeExecutionsById['n1']]; @@ -239,6 +250,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const { queryAllByTestId, debug, container } = renderTable({ initialNodes, nodeExecutionsById, + filterState, }); await waitFor(() => @@ -263,7 +275,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { expect(ids).toHaveLength(filteredNodeExecutions.length); const renderedPhases = queryAllByTestId('node-execution-col-phase'); expect(renderedPhases).toHaveLength(filteredNodeExecutions.length); - debug(container) + debug(container); for (const i in filteredNodeExecutions) { expect(ids[i]).toHaveTextContent(filteredNodeExecutions[i].id?.nodeId); @@ -276,13 +288,14 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const appliedFilters = [ { key: 'phase', operation: 'value_in', value: ['FAILED', 'SUCCEEDED'] }, ]; - mockUseNodeExecutionFiltersState.mockReturnValue({ + const filterState = { filters: [], appliedFilters, - }); + }; const nodeExecutionsById = mockExecutionsById(2, phases); - const filteredNodeExecutions: NodeExecution[] = Object.values(nodeExecutionsById); + const filteredNodeExecutions: NodeExecution[] = + Object.values(nodeExecutionsById); listNodeExecutions.mockImplementation(() => { return Promise.resolve({ entities: filteredNodeExecutions, @@ -292,6 +305,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { const { queryAllByTestId } = renderTable({ initialNodes, nodeExecutionsById, + filterState, }); await waitFor(() => From 38f865fa4d26a6bde758b498429034d7d6b46ef0 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Fri, 14 Apr 2023 13:36:31 -0700 Subject: [PATCH 24/25] chore: more tests Signed-off-by: Carina Ursu --- .../ExecutionDetails/ExecutionTab.tsx | 2 +- .../test/ExecutionNodeViews.test.tsx | 140 +++++++++++++++--- .../NodeExecutionDetailsPanelContent.test.tsx | 13 +- .../ExecutionDetails/test/TaskNames.test.tsx | 7 + .../Executions/Tables/NodeExecutionsTable.tsx | 124 +++++++++------- .../Tables/test/NodeExecutionsTable.test.tsx | 11 +- .../test/PausedTasksComponent.test.tsx | 12 +- 7 files changed, 217 insertions(+), 92 deletions(-) diff --git a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx index 50d235097..865220744 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/ExecutionTab.tsx @@ -32,7 +32,7 @@ export const ExecutionTab: React.FC = ({ tabType }) => {
    {tabType === tabs.nodes.id && ( - + )} {tabType === tabs.graph.id && } {tabType === tabs.timeline.id && } diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx index eb2302db5..fbd47a964 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/ExecutionNodeViews.test.tsx @@ -1,21 +1,26 @@ -import { fireEvent, render, waitFor } from '@testing-library/react'; +import * as React from 'react'; +import { fireEvent, render, waitFor, screen } from '@testing-library/react'; import { filterLabels } from 'components/Executions/filters/constants'; import { nodeExecutionStatusFilters } from 'components/Executions/filters/statusFilters'; import { oneFailedTaskWorkflow } from 'mocks/data/fixtures/oneFailedTaskWorkflow'; import { insertFixture } from 'mocks/data/insertFixture'; import { mockServer } from 'mocks/server'; import { Execution } from 'models/Execution/types'; -import * as React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { createTestQueryClient } from 'test/utils'; import { ExecutionContext } from 'components/Executions/contexts'; -import { tabs } from '../constants'; +import { listNodeExecutions, listTaskExecutions } from 'models/Execution/api'; +import { NodeExecutionPhase } from 'models'; +import { mockWorkflowId } from 'mocks/data/fixtures/types'; +import { NodeExecutionDetailsContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWorkflowToDag'; import { ExecutionNodeViews } from '../ExecutionNodeViews'; +import { tabs } from '../constants'; jest.mock('components/Executions/Tables/NodeExecutionRow', () => ({ - NodeExecutionRow: jest.fn(({ nodeExecution }) => ( + NodeExecutionRow: jest.fn(({ node }) => (
    - {nodeExecution?.id?.nodeId} + {node?.execution?.id?.nodeId}
    )), })); @@ -40,6 +45,14 @@ jest.mock( NodeExecutionName: jest.fn(({ name }) =>
    {name}
    ), }), ); +jest.mock('models/Execution/api', () => ({ + listNodeExecutions: jest.fn(), + listTaskExecutions: jest.fn(), +})); + +jest.mock('components/WorkflowGraph/transformerWorkflowToDag', () => ({ + transformerWorkflowToDag: jest.fn(), +})); // ExecutionNodeViews uses query params for NE list, so we must match them // for the list to be returned properly @@ -53,26 +66,87 @@ describe('ExecutionNodeViews', () => { let queryClient: QueryClient; let execution: Execution; let fixture: ReturnType; - beforeEach(() => { fixture = oneFailedTaskWorkflow.generate(); execution = fixture.workflowExecutions.top.data; insertFixture(mockServer, fixture); const nodeExecutions = fixture.workflowExecutions.top.nodeExecutions; - - mockServer.insertNodeExecutionList( - execution.id, - Object.values(nodeExecutions).map(({ data }) => data), - baseQueryParams, - ); - mockServer.insertNodeExecutionList( - execution.id, - [nodeExecutions.failedNode.data], - { - ...baseQueryParams, - filters: 'value_in(phase,FAILED)', - }, + const nodeExecutionsArray = Object.values(nodeExecutions).map( + ({ data }) => data, ); + transformerWorkflowToDag.mockImplementation(_ => { + return { + dag: { + id: 'start-node', + scopedId: 'start-node', + value: { + id: 'start-node', + }, + type: 4, + name: 'start', + nodes: [ + { + id: 'start-node', + scopedId: 'start-node', + value: { + inputs: [], + upstreamNodeIds: [], + outputAliases: [], + id: 'start-node', + }, + type: 4, + name: 'start', + nodes: [], + edges: [], + }, + { + id: 'end-node', + scopedId: 'end-node', + value: { + inputs: [], + upstreamNodeIds: [], + outputAliases: [], + id: 'end-node', + }, + type: 5, + name: 'end', + nodes: [], + edges: [], + }, + ...nodeExecutionsArray.map(n => ({ + id: n.id.nodeId, + scopedId: n.scopedId, + execution: n, + // type: dTypes.gateNode, + name: n.id.nodeId, + type: 3, + nodes: [], + edges: [], + })), + ], + }, + staticExecutionIdsMap: {}, + }; + }); + listNodeExecutions.mockImplementation((_, filters) => { + let finalNodes = nodeExecutionsArray; + if (filters?.filter?.length) { + const phases = filters?.filter + ?.filter(f => f.key === 'phase')?.[0] + .value?.map(f => NodeExecutionPhase[f]); + finalNodes = finalNodes.filter(n => { + return phases.includes(n.closure.phase); + }); + } + return Promise.resolve({ + entities: Object.values(finalNodes), + }); + }); + listTaskExecutions.mockImplementation(() => { + return Promise.resolve({ + entities: [], + }); + }); queryClient = createTestQueryClient(); }); @@ -80,7 +154,15 @@ describe('ExecutionNodeViews', () => { render( - + + + , ); @@ -90,7 +172,7 @@ describe('ExecutionNodeViews', () => { const failedNodeName = nodeExecutions.failedNode.data.id.nodeId; const succeededNodeName = nodeExecutions.pythonNode.data.id.nodeId; - const { getByText, queryByText, getByLabelText } = renderViews(); + const { getByText, queryByText, queryAllByTestId } = renderViews(); await waitFor(() => getByText(tabs.nodes.label)); @@ -99,21 +181,31 @@ describe('ExecutionNodeViews', () => { // Ensure we are on Nodes tab await fireEvent.click(nodesTab); + await waitFor(() => { + const nodes = queryAllByTestId('node-execution-row'); + return nodes?.length === 2; + }); + await waitFor(() => queryByText(succeededNodeName)); const statusButton = await waitFor(() => getByText(filterLabels.status)); // Apply 'Failed' filter and wait for list to include only the failed item await fireEvent.click(statusButton); + const failedFilter = await waitFor(() => - getByLabelText(nodeExecutionStatusFilters.failed.label), + screen.getByLabelText(nodeExecutionStatusFilters.failed.label), ); // Wait for succeeded task to disappear and ensure failed task remains await fireEvent.click(failedFilter); - await waitFor(() => queryByText(failedNodeName)); + await waitFor(() => { + const nodes = queryAllByTestId('node-execution-row'); + return nodes?.length === 1; + }); expect(queryByText(succeededNodeName)).not.toBeInTheDocument(); + expect(queryByText(failedNodeName)).toBeInTheDocument(); // Switch to the Graph tab @@ -121,7 +213,9 @@ describe('ExecutionNodeViews', () => { await fireEvent.click(timelineTab); await waitFor(() => queryByText(succeededNodeName)); + // expect all initital nodes to be rendered expect(queryByText(succeededNodeName)).toBeInTheDocument(); + expect(queryByText(failedNodeName)).toBeInTheDocument(); // Switch back to Nodes Tab and verify filter still applied await fireEvent.click(nodesTab); diff --git a/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx b/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx index 57dd268f1..4d41a184b 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx @@ -12,11 +12,14 @@ import { MemoryRouter } from 'react-router'; import { createTestQueryClient } from 'test/utils'; import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent'; -jest.mock('components/Executions/ExecutionDetails/ExecutionDetailsActions', () => ({ - ExecutionDetailsActions: jest.fn(() => ( -
    - )) -})); +jest.mock( + 'components/Executions/ExecutionDetails/ExecutionDetailsActions', + () => ({ + ExecutionDetailsActions: jest.fn(() => ( +
    + )), + }), +); jest.mock('components/Workflow/workflowQueries'); const { fetchWorkflow } = require('components/Workflow/workflowQueries'); 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 67ac4c53c..0bc2d0f16 100644 --- a/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx +++ b/packages/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx @@ -76,7 +76,14 @@ describe('ExecutionDetails > Timeline > TaskNames', () => { {}, + setShouldUpdate: () => {}, + shouldUpdate: false, }} > diff --git a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx index 7a7623c3b..7dcea752c 100644 --- a/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx +++ b/packages/console/src/components/Executions/Tables/NodeExecutionsTable.tsx @@ -21,7 +21,7 @@ import { useNodeExecutionsById, } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionRow } from './NodeExecutionRow'; -import { ExecutionFiltersState, useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; +import { ExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { searchNode } from '../utils'; import { nodeExecutionPhaseConstants } from '../constants'; import { NodeExecutionDynamicProvider } from '../contextProvider/NodeExecutionDetails/NodeExecutionDynamicProvider'; @@ -36,9 +36,10 @@ const mergeOriginIntoNodes = (target: dNode[], origin: dNode[]) => { if (!target?.length) { return target; } + const originClone = cloneDeep(origin); const newTarget = cloneDeep(target); newTarget?.forEach(value => { - const originalNode = origin.find( + const originalNode = originClone.find( og => og.id === value.id && og.scopedId === value.scopedId, ); const newNodes = mergeOriginIntoNodes( @@ -81,12 +82,12 @@ const filterNodes = ( let initialClone = cloneDeep(initialNodes); - initialClone.forEach(n => { + for (const n of initialClone) { n.nodes = filterNodes(n.nodes, nodeExecutionsById, appliedFilters); - }); + } initialClone = initialClone.filter(node => { - const hasFilteredChildren = node.nodes?.length; + const hasFilteredChildren = !!node.nodes?.length; const shouldBeIncluded = executionMatchesPhaseFilter( nodeExecutionsById[node.scopedId], appliedFilters[0], @@ -103,7 +104,7 @@ const filterNodes = ( return initialClone; }; -const isPhaseFilter = (appliedFilters: FilterOperation[]) => { +const isPhaseFilter = (appliedFilters: FilterOperation[] = []) => { if (appliedFilters.length === 1 && appliedFilters[0].key === 'phase') { return true; } @@ -115,53 +116,53 @@ const isPhaseFilter = (appliedFilters: FilterOperation[]) => { * NodeExecutions are expandable and will potentially render a list of child * TaskExecutions */ -export const NodeExecutionsTable: React.FC<{filterState: ExecutionFiltersState}> = ({filterState}) => { +export const NodeExecutionsTable: React.FC<{ + filterState: ExecutionFiltersState; +}> = ({ filterState }) => { const commonStyles = useCommonStyles(); const tableStyles = useExecutionTableStyles(); + const columnStyles = useColumnStyles(); + const { execution } = useContext(ExecutionContext); const { appliedFilters } = filterState; + const [filteredNodeExecutions, setFilteredNodeExecutions] = + useState(); const { nodeExecutionsById, initialDNodes: initialNodes } = useNodeExecutionsById(); + const [filters, setFilters] = useState([]); + const [originalNodes, setOriginalNodes] = useState([]); + // query to get filtered data to narrow down Table outputs const { nodeExecutionsQuery: filteredNodeExecutionsQuery } = - useExecutionNodeViewsStatePoll(execution, filterState?.appliedFilters); + useExecutionNodeViewsStatePoll(execution, filters); + const { compiledWorkflowClosure } = useNodeExecutionContext(); const [showNodes, setShowNodes] = useState([]); + const [initialFilteredNodes, setInitialFilteredNodes] = useState< dNode[] | undefined >(undefined); - const [originalNodes, setOriginalNodes] = useState( - appliedFilters.length > 0 && initialFilteredNodes - ? initialFilteredNodes - : initialNodes, - ); - - const [filters, setFilters] = useState(appliedFilters); - - const [isFiltersChanged, setIsFiltersChanged] = useState(false); - - const { compiledWorkflowClosure } = useNodeExecutionContext(); - - const columnStyles = useColumnStyles(); - // Memoizing columns so they won't be re-generated unless the styles change - const compiledNodes = extractCompiledNodes(compiledWorkflowClosure); - const columns = useMemo( - () => generateColumns(columnStyles, compiledNodes), - [columnStyles, compiledNodes], - ); - - const [filteredNodeExecutions, setFilteredNodeExecutions] = - useState(); + useEffect(() => { + // keep original nodes as a record of the nodes' toggle status + setOriginalNodes(prev => { + const newOgNodes = merge(initialNodes, prev); + if (stringifyIsEqual(prev, newOgNodes)) { + return prev; + } + return newOgNodes; + }); + }, [initialNodes]); + // wait for changes to filtered node executions useEffect(() => { if (filteredNodeExecutionsQuery.isFetching) { return; } - const newFilteredNodeExecutions = isPhaseFilter(filterState.appliedFilters) + const newFilteredNodeExecutions = isPhaseFilter(filters) ? undefined : filteredNodeExecutionsQuery.data; @@ -175,20 +176,20 @@ export const NodeExecutionsTable: React.FC<{filterState: ExecutionFiltersState}> }, [filteredNodeExecutionsQuery]); useEffect(() => { - const plainNodes = convertToPlainNodes(originalNodes || []); - setOriginalNodes(ogn => { - const newNodes = - appliedFilters.length > 0 && initialFilteredNodes - ? mergeOriginIntoNodes(initialFilteredNodes, plainNodes) - : merge(initialNodes, ogn); - - if (!stringifyIsEqual(newNodes, ogn)) { - return newNodes; - } - - return ogn; - }); - + const newShownNodes = + filters.length > 0 && initialFilteredNodes + ? // if there are filtered nodes, merge original ones into them to preserve toggle status + mergeOriginIntoNodes( + cloneDeep(initialFilteredNodes), + cloneDeep(originalNodes), + ) + : // else, merge originalNodes into initialNodes to preserve toggle status + mergeOriginIntoNodes( + cloneDeep(initialNodes), + cloneDeep(originalNodes), + ); + + const plainNodes = convertToPlainNodes(newShownNodes || []); const updatedShownNodesMap = plainNodes.map(node => { const execution = nodeExecutionsById?.[node?.scopedId]; return { @@ -198,19 +199,32 @@ export const NodeExecutionsTable: React.FC<{filterState: ExecutionFiltersState}> }; }); setShowNodes(updatedShownNodesMap); - }, [initialNodes, initialFilteredNodes, originalNodes, nodeExecutionsById]); + }, [ + initialNodes, + initialFilteredNodes, + originalNodes, + nodeExecutionsById, + filters, + ]); useEffect(() => { - if (!isEqual(filters, appliedFilters)) { - setFilters(appliedFilters); - setIsFiltersChanged(true); - } else { - setIsFiltersChanged(false); - } + setFilters(prev => { + if (isEqual(prev, appliedFilters)) { + return prev; + } + return JSON.parse(JSON.stringify(appliedFilters)); + }); }, [appliedFilters]); + // Memoizing columns so they won't be re-generated unless the styles change + const compiledNodes = extractCompiledNodes(compiledWorkflowClosure); + const columns = useMemo( + () => generateColumns(columnStyles, compiledNodes), + [columnStyles, compiledNodes], + ); + useEffect(() => { - if (appliedFilters.length > 0) { + if (filters.length > 0) { // if filter was apllied, but filteredNodeExecutions is empty, we only appliied Phase filter, // and need to clear out items manually if (!filteredNodeExecutions) { @@ -218,7 +232,7 @@ export const NodeExecutionsTable: React.FC<{filterState: ExecutionFiltersState}> const filteredNodes = filterNodes( initialNodes, nodeExecutionsById, - appliedFilters, + filters, ); setInitialFilteredNodes(filteredNodes); @@ -231,7 +245,7 @@ export const NodeExecutionsTable: React.FC<{filterState: ExecutionFiltersState}> setInitialFilteredNodes(filteredNodes); } } - }, [initialNodes, filteredNodeExecutions, isFiltersChanged]); + }, [initialNodes, filteredNodeExecutions, filters]); const toggleNode = async (id: string, scopedId: string, level: number) => { searchNode(originalNodes, 0, id, scopedId, level); 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 7110c89fc..5652401bd 100644 --- a/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/packages/console/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -197,7 +197,7 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { }; listNodeExecutions.mockImplementation(() => { return Promise.resolve({ - entities: filteredNodeExecutions, + entities: [filteredNodeExecutions], }); }); @@ -262,20 +262,17 @@ describe('NodeExecutionsTableExecutions > Tables > NodeExecutionsTable', () => { ), ); + debug(container); + await waitFor(() => { const rows = queryAllByTestId('node-execution-row'); - return rows.length === filteredNodeExecutions.length; + expect(rows).toHaveLength(filteredNodeExecutions.length); }); - expect(queryAllByTestId('node-execution-row')).toHaveLength( - filteredNodeExecutions.length, - ); - const ids = queryAllByTestId('node-execution-col-id'); expect(ids).toHaveLength(filteredNodeExecutions.length); const renderedPhases = queryAllByTestId('node-execution-col-phase'); expect(renderedPhases).toHaveLength(filteredNodeExecutions.length); - debug(container); for (const i in filteredNodeExecutions) { expect(ids[i]).toHaveTextContent(filteredNodeExecutions[i].id?.nodeId); diff --git a/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx b/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx index c7a218b33..6514f8424 100644 --- a/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx +++ b/packages/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx @@ -97,7 +97,17 @@ describe('flytegraph > ReactFlow > PausedTasksComponent', () => { }} > {} }} + value={{ + nodeExecutionsById, + dagData: { + dagError: null, + mergedDag: {}, + }, + initialDNodes: props.nodes, + setCurrentNodeExecutionsById: () => {}, + setShouldUpdate: () => {}, + shouldUpdate: false, + }} > From 4895a0faa5e55b0d74b56bd7b5ec64efe389a082 Mon Sep 17 00:00:00 2001 From: Carina Ursu Date: Mon, 17 Apr 2023 14:38:26 -0700 Subject: [PATCH 25/25] chore: bump version Signed-off-by: Carina Ursu --- packages/console/package.json | 2 +- website/package.json | 2 +- yarn.lock | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/console/package.json b/packages/console/package.json index 8857c1404..28689c279 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@flyteorg/console", - "version": "0.0.20", + "version": "0.0.21", "description": "Flyteconsole main app module", "main": "./dist/index.js", "module": "./lib/index.js", diff --git a/website/package.json b/website/package.json index 3bfdcd2d6..960939e2e 100644 --- a/website/package.json +++ b/website/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@flyteorg/common": "^0.0.4", - "@flyteorg/console": "^0.0.20", + "@flyteorg/console": "^0.0.21", "long": "^4.0.0", "protobufjs": "~6.11.3", "react-ga4": "^1.4.1", diff --git a/yarn.lock b/yarn.lock index 80aa10f3f..528b45899 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1995,7 +1995,7 @@ __metadata: resolution: "@flyteconsole/client-app@workspace:website" dependencies: "@flyteorg/common": ^0.0.4 - "@flyteorg/console": ^0.0.20 + "@flyteorg/console": ^0.0.21 "@types/long": ^3.0.32 long: ^4.0.0 protobufjs: ~6.11.3 @@ -2034,7 +2034,7 @@ __metadata: languageName: unknown linkType: soft -"@flyteorg/console@^0.0.20, @flyteorg/console@workspace:packages/console": +"@flyteorg/console@^0.0.21, @flyteorg/console@workspace:packages/console": version: 0.0.0-use.local resolution: "@flyteorg/console@workspace:packages/console" dependencies: