diff --git a/package.json b/package.json index 07cbd62d6..4756fd9fd 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,9 @@ "git add" ] }, - "dependencies": {}, + "dependencies": { + "@flyteorg/flyteidl": "1.1.11" + }, "devDependencies": { "@storybook/addon-actions": "^6.4.19", "@storybook/addon-essentials": "^6.4.19", @@ -60,9 +62,9 @@ "@testing-library/jest-dom": "^5.5.0", "@testing-library/react": "^10.0.3", "@testing-library/react-hooks": "^7.0.2", - "ts-jest": "^26.3.0", "jest": "^26.0.0", - "react-hot-loader": "^4.1.2" + "react-hot-loader": "^4.1.2", + "ts-jest": "^26.3.0" }, "resolutions": { "@babel/cli": "~7.16.0", diff --git a/packages/zapp/console/src/assets/index.html b/packages/zapp/console/src/assets/index.html index b25bf9e74..074cdef27 100644 --- a/packages/zapp/console/src/assets/index.html +++ b/packages/zapp/console/src/assets/index.html @@ -12,6 +12,7 @@ { - const requestConfig = React.useContext(NodeExecutionsRequestConfigContext); - const childGroupsQuery = useAllChildNodeExecutionGroupsQuery(nodeExecutions, requestConfig); - - const renderGraphComponent = (childGroups) => { - const output: any[] = []; - for (let i = 0; i < childGroups.length; i++) { - for (let j = 0; j < childGroups[i].length; j++) { - for (let k = 0; k < childGroups[i][j].nodeExecutions.length; k++) { - output.push(childGroups[i][j].nodeExecutions[k] as NodeExecution); - } - } - } - const executions: NodeExecution[] = output.concat(nodeExecutions); - return nodeExecutions.length > 0 ? ( - - ) : null; - }; - - return ( - - {renderGraphComponent} - - ); -}; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 2a8afdd49..006db91d3 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -5,16 +5,24 @@ import { WaitForQuery } from 'components/common/WaitForQuery'; import { DataError } from 'components/Errors/DataError'; import { useTabState } from 'components/hooks/useTabState'; import { secondaryBackgroundColor } from 'components/Theme/constants'; -import { Execution, NodeExecution } from 'models/Execution/types'; +import { Execution, ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types'; +import { useContext, useEffect, useMemo, useState } from 'react'; +import { keyBy } from 'lodash'; +import { isMapTaskV1 } from 'models/Task/utils'; +import { useQueryClient } from 'react-query'; +import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; import { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails'; -import { NodeExecutionsRequestConfigContext } from '../contexts'; +import { NodeExecutionsByIdContext, NodeExecutionsRequestConfigContext } from '../contexts'; import { ExecutionFilters } from '../ExecutionFilters'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; import { NodeExecutionsTable } from '../Tables/NodeExecutionsTable'; import { tabs } from './constants'; -import { ExecutionChildrenLoader } from './ExecutionChildrenLoader'; import { useExecutionNodeViewsState } from './useExecutionNodeViewsState'; import { ExecutionNodesTimeline } from './Timeline'; +import { fetchTaskExecutionList } from '../taskExecutionQueries'; +import { getGroupedLogs } from '../TaskExecutionsList/utils'; +import { useAllTreeNodeExecutionGroupsQuery } from '../nodeExecutionQueries'; +import { ExecutionWorkflowGraph } from './ExecutionWorkflowGraph'; const useStyles = makeStyles((theme: Theme) => ({ filters: { @@ -31,8 +39,15 @@ const useStyles = makeStyles((theme: Theme) => ({ background: secondaryBackgroundColor, paddingLeft: theme.spacing(3.5), }, + loading: { + margin: 'auto', + }, })); +interface WorkflowNodeExecution extends NodeExecution { + logsByPhase?: LogsByPhase; +} + export interface ExecutionNodeViewsProps { execution: Execution; } @@ -43,11 +58,22 @@ export const ExecutionNodeViews: React.FC = ({ executio const styles = useStyles(); const filterState = useNodeExecutionFiltersState(); const tabState = useTabState(tabs, defaultTab); + const queryClient = useQueryClient(); + const requestConfig = useContext(NodeExecutionsRequestConfigContext); const { - closure: { abortMetadata }, + closure: { abortMetadata, workflowId }, } = execution; + const [nodeExecutions, setNodeExecutions] = useState([]); + const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState< + WorkflowNodeExecution[] + >([]); + + const nodeExecutionsById = useMemo(() => { + return keyBy(nodeExecutionsWithResources, 'scopedId'); + }, [nodeExecutionsWithResources]); + /* We want to maintain the filter selection when switching away from the Nodes tab and back, but do not want to filter the nodes when viewing the graph. So, we will only pass filters to the execution state when on the nodes tab. */ @@ -58,6 +84,61 @@ export const ExecutionNodeViews: React.FC = ({ executio appliedFilters, ); + useEffect(() => { + let isCurrent = true; + async function fetchData(baseNodeExecutions, queryClient) { + const newValue = await Promise.all( + baseNodeExecutions.map(async (baseNodeExecution) => { + const taskExecutions = await fetchTaskExecutionList(queryClient, baseNodeExecution.id); + + const useNewMapTaskView = taskExecutions.every((taskExecution) => { + const { + closure: { taskType, metadata, eventVersion = 0 }, + } = taskExecution; + return isMapTaskV1( + eventVersion, + metadata?.externalResources?.length ?? 0, + taskType ?? undefined, + ); + }); + const externalResources: ExternalResource[] = taskExecutions + .map((taskExecution) => taskExecution.closure.metadata?.externalResources) + .flat() + .filter((resource): resource is ExternalResource => !!resource); + + const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); + + return { + ...baseNodeExecution, + ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), + }; + }), + ); + + if (isCurrent) { + setNodeExecutionsWithResources(newValue); + } + } + + if (nodeExecutions.length > 0) { + fetchData(nodeExecutions, queryClient); + } + return () => { + isCurrent = false; + }; + }, [nodeExecutions]); + + const childGroupsQuery = useAllTreeNodeExecutionGroupsQuery( + nodeExecutionsQuery.data ?? [], + requestConfig, + ); + + useEffect(() => { + if (!childGroupsQuery.isLoading && childGroupsQuery.data) { + setNodeExecutions(childGroupsQuery.data); + } + }, [childGroupsQuery.data]); + const renderNodeExecutionsTable = (nodeExecutions: NodeExecution[]) => ( = ({ executio ); - const renderExecutionLoader = (nodeExecutions: NodeExecution[]) => { + const renderExecutionChildrenLoader = () => + nodeExecutions.length > 0 ? : null; + + const renderExecutionLoader = () => { return ( - + + {renderExecutionChildrenLoader} + ); }; - const renderExecutionsTimeline = (nodeExecutions: NodeExecution[]) => ( - + const renderExecutionsTimeline = () => ( + + {() => } + ); + const TimelineLoading = () => { + return ( +
+ +
+ ); + }; + return ( <> @@ -87,29 +184,31 @@ export const ExecutionNodeViews: React.FC = ({ executio - -
- {tabState.value === tabs.nodes.id && ( - <> -
- -
+ + +
+ {tabState.value === tabs.nodes.id && ( + <> +
+ +
+ + {renderNodeExecutionsTable} + + + )} + {tabState.value === tabs.graph.id && ( + + {renderExecutionLoader} + + )} + {tabState.value === tabs.timeline.id && ( - {renderNodeExecutionsTable} + {renderExecutionsTimeline} - - )} - {tabState.value === tabs.graph.id && ( - - {renderExecutionLoader} - - )} - {tabState.value === tabs.timeline.id && ( - - {renderExecutionsTimeline} - - )} -
+ )} +
+
); diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx index b0181cdec..72165b735 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/ExecutionWorkflowGraph.tsx @@ -3,46 +3,27 @@ import { WaitForQuery } from 'components/common/WaitForQuery'; import { DataError } from 'components/Errors/DataError'; import { makeWorkflowQuery } from 'components/Workflow/workflowQueries'; import { WorkflowGraph } from 'components/WorkflowGraph/WorkflowGraph'; -import { keyBy } from 'lodash'; import { TaskExecutionPhase } from 'models/Execution/enums'; -import { ExternalResource, LogsByPhase, NodeExecution } from 'models/Execution/types'; import { endNodeId, startNodeId } from 'models/Node/constants'; -import { isMapTaskV1 } from 'models/Task/utils'; import { Workflow, WorkflowId } from 'models/Workflow/types'; import * as React from 'react'; -import { useEffect, useMemo, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useQuery, useQueryClient } from 'react-query'; -import { NodeExecutionsContext } from '../contexts'; -import { fetchTaskExecutionList } from '../taskExecutionQueries'; -import { getGroupedLogs } from '../TaskExecutionsList/utils'; +import { NodeExecutionsByIdContext } from '../contexts'; import { NodeExecutionDetailsPanelContent } from './NodeExecutionDetailsPanelContent'; export interface ExecutionWorkflowGraphProps { - nodeExecutions: NodeExecution[]; workflowId: WorkflowId; } -interface WorkflowNodeExecution extends NodeExecution { - logsByPhase?: LogsByPhase; -} - /** Wraps a WorkflowGraph, customizing it to also show execution statuses */ -export const ExecutionWorkflowGraph: React.FC = ({ - nodeExecutions, - workflowId, -}) => { +export const ExecutionWorkflowGraph: React.FC = ({ workflowId }) => { const queryClient = useQueryClient(); const workflowQuery = useQuery(makeWorkflowQuery(queryClient, workflowId)); - const [nodeExecutionsWithResources, setNodeExecutionsWithResources] = useState< - WorkflowNodeExecution[] - >([]); const [selectedNodes, setSelectedNodes] = useState([]); + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); - const nodeExecutionsById = useMemo( - () => keyBy(nodeExecutionsWithResources, 'scopedId'), - [nodeExecutionsWithResources], - ); // Note: flytegraph allows multiple selection, but we only support showing // a single item in the details panel const selectedExecution = selectedNodes.length @@ -57,49 +38,6 @@ export const ExecutionWorkflowGraph: React.FC = ({ const [selectedPhase, setSelectedPhase] = useState(undefined); const [isDetailsTabClosed, setIsDetailsTabClosed] = useState(!selectedExecution); - useEffect(() => { - let isCurrent = true; - async function fetchData(nodeExecutions, queryClient) { - const newValue = await Promise.all( - nodeExecutions.map(async (nodeExecution) => { - const taskExecutions = await fetchTaskExecutionList(queryClient, nodeExecution.id); - - const useNewMapTaskView = taskExecutions.every((taskExecution) => { - const { - closure: { taskType, metadata, eventVersion = 0 }, - } = taskExecution; - return isMapTaskV1( - eventVersion, - metadata?.externalResources?.length ?? 0, - taskType ?? undefined, - ); - }); - const externalResources: ExternalResource[] = taskExecutions - .map((taskExecution) => taskExecution.closure.metadata?.externalResources) - .flat() - .filter((resource): resource is ExternalResource => !!resource); - - const logsByPhase: LogsByPhase = getGroupedLogs(externalResources); - - return { - ...nodeExecution, - ...(useNewMapTaskView && logsByPhase.size > 0 && { logsByPhase }), - }; - }), - ); - - if (isCurrent) { - setNodeExecutionsWithResources(newValue); - } - } - - fetchData(nodeExecutions, queryClient); - - return () => { - isCurrent = false; - }; - }, [nodeExecutions]); - useEffect(() => { setIsDetailsTabClosed(!selectedExecution); }, [selectedExecution]); @@ -126,18 +64,15 @@ export const ExecutionWorkflowGraph: React.FC = ({ selectedPhase={selectedPhase} onPhaseSelectionChanged={setSelectedPhase} isDetailsTabClosed={isDetailsTabClosed} - nodeExecutionsById={nodeExecutionsById} workflow={workflow} /> ); return ( <> - - - {renderGraph} - - + + {renderGraph} + {selectedExecution && ( > = (props) => { const { node, config, selected } = props; - const nodeExecutions = React.useContext(NodeExecutionsContext); - const nodeExecution = nodeExecutions[node.id]; + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + const nodeExecution = nodeExecutionsById[node.id]; const phase = nodeExecution ? nodeExecution.closure.phase : NodeExecutionPhase.UNDEFINED; const { badgeColor: color } = getNodeExecutionPhaseConstants(phase); diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ChartHeader.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ChartHeader.tsx new file mode 100644 index 000000000..1fafd08c8 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ChartHeader.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import * as moment from 'moment-timezone'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum'; +import { useScaleContext } from './scaleContext'; +import { TimeZone } from './helpers'; + +interface StyleProps { + chartWidth: number; + labelInterval: number; +} + +const useStyles = makeStyles((_theme) => ({ + chartHeader: (props: StyleProps) => ({ + height: 41, + display: 'flex', + alignItems: 'center', + width: `${props.chartWidth}px`, + }), + taskDurationsLabelItem: (props: StyleProps) => ({ + fontSize: 12, + fontFamily: 'Open Sans', + fontWeight: 'bold', + color: COLOR_SPECTRUM.gray40.color, + paddingLeft: 10, + width: `${props.labelInterval}px`, + }), +})); + +interface HeaderProps extends StyleProps { + chartTimezone: string; + totalDurationSec: number; + startedAt: Date; +} + +export const ChartHeader = (props: HeaderProps) => { + const styles = useStyles(props); + + const { chartInterval: chartTimeInterval, setMaxValue } = useScaleContext(); + const { startedAt, chartTimezone, totalDurationSec } = props; + + React.useEffect(() => { + setMaxValue(props.totalDurationSec); + }, [props.totalDurationSec, setMaxValue]); + + const labels = React.useMemo(() => { + const len = Math.ceil(totalDurationSec / chartTimeInterval); + const lbs = len > 0 ? new Array(len).fill('') : []; + return lbs.map((_, idx) => { + const time = moment.utc(new Date(startedAt.getTime() + idx * chartTimeInterval * 1000)); + return chartTimezone === TimeZone.UTC + ? time.format('hh:mm:ss A') + : time.local().format('hh:mm:ss A'); + }); + }, [chartTimezone, startedAt, chartTimeInterval, totalDurationSec]); + + return ( +
+ {labels.map((label) => { + return ( +
+ {label} +
+ ); + })} +
+ ); +}; diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx index 677cf7e2a..14af5b17e 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/ExecutionTimeline.tsx @@ -6,12 +6,16 @@ import { transformerWorkflowToDag } from 'components/WorkflowGraph/transformerWo import { isEndNode, isStartNode, isExpanded } from 'components/WorkflowGraph/utils'; import { tableHeaderColor } from 'components/Theme/constants'; import { timestampToDate } from 'common/utils'; -import { NodeExecution } from 'models/Execution/types'; import { dNode } from 'models/Graph/types'; +import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workflowQueries'; +import { useQuery } from 'react-query'; +import { createRef, useContext, useEffect, useRef, useState } from 'react'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { checkForDynamicExecutions } from 'components/common/utils'; import { convertToPlainNodes } from './helpers'; -import { ChartHeader } from './chartHeader'; +import { ChartHeader } from './ChartHeader'; import { useScaleContext } from './scaleContext'; -import { TaskNames } from './taskNames'; +import { TaskNames } from './TaskNames'; import { getChartDurationData } from './TimelineChart/chartData'; import { TimelineChart } from './TimelineChart'; @@ -67,40 +71,50 @@ const useStyles = makeStyles((theme) => ({ const INTERVAL_LENGTH = 110; interface ExProps { - nodeExecutions: NodeExecution[]; chartTimezone: string; } -export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTimezone }) => { - const [chartWidth, setChartWidth] = React.useState(0); - const [labelInterval, setLabelInterval] = React.useState(INTERVAL_LENGTH); - const durationsRef = React.useRef(null); - const durationsLabelsRef = React.useRef(null); - const taskNamesRef = React.createRef(); +export const ExecutionTimeline: React.FC = ({ chartTimezone }) => { + const [chartWidth, setChartWidth] = useState(0); + const [labelInterval, setLabelInterval] = useState(INTERVAL_LENGTH); + const durationsRef = useRef(null); + const durationsLabelsRef = useRef(null); + const taskNamesRef = createRef(); - const [originalNodes, setOriginalNodes] = React.useState([]); - const [showNodes, setShowNodes] = React.useState([]); - const [startedAt, setStartedAt] = React.useState(new Date()); + const [originalNodes, setOriginalNodes] = useState([]); + const [showNodes, setShowNodes] = useState([]); + const [startedAt, setStartedAt] = useState(new Date()); const { compiledWorkflowClosure } = useNodeExecutionContext(); const { chartInterval: chartTimeInterval } = useScaleContext(); + const { staticExecutionIdsMap } = compiledWorkflowClosure + ? transformerWorkflowToDag(compiledWorkflowClosure) + : []; - React.useEffect(() => { + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); + + const dynamicParents = checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap); + + const { data: dynamicWorkflows } = useQuery( + makeNodeExecutionDynamicWorkflowQuery(dynamicParents), + ); + + useEffect(() => { const nodes: dNode[] = compiledWorkflowClosure - ? transformerWorkflowToDag(compiledWorkflowClosure).dag.nodes + ? transformerWorkflowToDag(compiledWorkflowClosure, dynamicWorkflows).dag.nodes : []; // we remove start/end node info in the root dNode list during first assignment const initializeNodes = convertToPlainNodes(nodes); setOriginalNodes(initializeNodes); - }, [compiledWorkflowClosure]); + }, [dynamicWorkflows, compiledWorkflowClosure]); - React.useEffect(() => { + useEffect(() => { const initializeNodes = convertToPlainNodes(originalNodes); const updatedShownNodesMap = initializeNodes.map((node) => { - const index = nodeExecutions.findIndex((exe) => exe.scopedId === node.scopedId); + const execution = nodeExecutionsById[node.scopedId]; return { ...node, - execution: index >= 0 ? nodeExecutions[index] : undefined, + execution, }; }); setShowNodes(updatedShownNodesMap); @@ -110,12 +124,12 @@ export const ExecutionTimeline: React.FC = ({ nodeExecutions, chartTime if (firstStartedAt) { setStartedAt(timestampToDate(firstStartedAt)); } - }, [originalNodes, nodeExecutions]); + }, [originalNodes, nodeExecutionsById]); const { items: barItemsData, totalDurationSec } = getChartDurationData(showNodes, startedAt); const styles = useStyles({ chartWidth: chartWidth, itemsShown: showNodes.length }); - React.useEffect(() => { + useEffect(() => { // Sync width of elements and intervals of ChartHeader (time labels) and TimelineChart const calcWidth = Math.ceil(totalDurationSec / chartTimeInterval) * INTERVAL_LENGTH; if (durationsRef.current && calcWidth < durationsRef.current.clientWidth) { diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx new file mode 100644 index 000000000..a2bcd4187 --- /dev/null +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/TaskNames.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import { makeStyles, Theme, Typography } from '@material-ui/core'; + +import { RowExpander } from 'components/Executions/Tables/RowExpander'; +import { getNodeTemplateName } from 'components/WorkflowGraph/utils'; +import { dNode } from 'models/Graph/types'; +import { NodeExecutionName } from './NodeExecutionName'; +import { NodeExecutionsTimelineContext } from './context'; + +const useStyles = makeStyles((theme: Theme) => ({ + taskNamesList: { + overflowY: 'scroll', + flex: 1, + }, + namesContainer: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'left', + padding: '0 10px', + height: 56, + width: 256, + borderBottom: `1px solid ${theme.palette.divider}`, + whiteSpace: 'nowrap', + }, + namesContainerExpander: { + display: 'flex', + marginTop: 'auto', + marginBottom: 'auto', + }, + namesContainerBody: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'center', + whiteSpace: 'nowrap', + height: '100%', + overflow: 'hidden', + }, + displayName: { + marginTop: 4, + textOverflow: 'ellipsis', + width: '100%', + overflow: 'hidden', + }, + leaf: { + width: 30, + }, +})); + +interface TaskNamesProps { + nodes: dNode[]; + onScroll: () => void; + onToggle: (id: string, scopeId: string, level: number) => void; +} + +export const TaskNames = React.forwardRef((props, ref) => { + const state = React.useContext(NodeExecutionsTimelineContext); + const { nodes, onScroll, onToggle } = props; + const styles = useStyles(); + + return ( +
+ {nodes.map((node) => { + const templateName = getNodeTemplateName(node); + const nodeLevel = node?.level ?? 0; + return ( +
+
+ {node.nodes?.length ? ( + onToggle(node.id, node.scopedId, nodeLevel)} + /> + ) : ( +
+ )} +
+ +
+ + + {templateName} + +
+
+ ); + })} +
+ ); +}); diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx index e0349ddab..e0180b7a1 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/Timeline/index.tsx @@ -1,12 +1,8 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core'; -import { NodeExecution, NodeExecutionIdentifier } from 'models/Execution/types'; -import { WaitForQuery } from 'components/common/WaitForQuery'; -import { NodeExecutionsRequestConfigContext } from 'components/Executions/contexts'; -import { useAllTreeNodeExecutionGroupsQuery } from 'components/Executions/nodeExecutionQueries'; -import { DataError } from 'components/Errors/DataError'; +import { NodeExecutionIdentifier } from 'models/Execution/types'; import { DetailsPanel } from 'components/common/DetailsPanel'; -import { LargeLoadingSpinner } from 'components/common/LoadingSpinner'; +import { useMemo, useState } from 'react'; import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent'; import { NodeExecutionsTimelineContext } from './context'; import { ExecutionTimelineFooter } from './ExecutionTimelineFooter'; @@ -25,58 +21,28 @@ const useStyles = makeStyles(() => ({ flex: '1 1 0', overflowY: 'auto', }, - loading: { - margin: 'auto', - }, })); -interface TimelineProps { - nodeExecutions: NodeExecution[]; -} - -export const ExecutionNodesTimeline = (props: TimelineProps) => { +export const ExecutionNodesTimeline = () => { const styles = useStyles(); - const [selectedExecution, setSelectedExecution] = React.useState( - null, - ); - const [chartTimezone, setChartTimezone] = React.useState(TimeZone.Local); + const [selectedExecution, setSelectedExecution] = useState(null); + const [chartTimezone, setChartTimezone] = useState(TimeZone.Local); const onCloseDetailsPanel = () => setSelectedExecution(null); const handleTimezoneChange = (tz) => setChartTimezone(tz); - const requestConfig = React.useContext(NodeExecutionsRequestConfigContext); - const childGroupsQuery = useAllTreeNodeExecutionGroupsQuery(props.nodeExecutions, requestConfig); - - const timelineContext = React.useMemo( + const timelineContext = useMemo( () => ({ selectedExecution, setSelectedExecution }), [selectedExecution, setSelectedExecution], ); - const renderExecutionsTimeline = (nodeExecutions: NodeExecution[]) => { - return ; - }; - - const TimelineLoading = () => { - return ( -
- -
- ); - }; - return (
- - {renderExecutionsTimeline} - + ;
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx index d1e68dba2..f99d01f8d 100644 --- a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx +++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/Timeline.test.tsx @@ -1,5 +1,6 @@ import ThemeProvider from '@material-ui/styles/ThemeProvider'; import { render, waitFor } from '@testing-library/react'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { muiTheme } from 'components/Theme/muiTheme'; import { oneFailedTaskWorkflow } from 'mocks/data/fixtures/oneFailedTaskWorkflow'; import { insertFixture } from 'mocks/data/insertFixture'; @@ -16,6 +17,10 @@ jest.mock('../ExecutionWorkflowGraph.tsx', () => ({ ExecutionWorkflowGraph: () => null, })); +jest.mock('../Timeline/ExecutionTimeline.tsx', () => ({ + ExecutionTimeline: () => null, +})); + jest.mock('chart.js', () => ({ Chart: { register: () => null }, Tooltip: { positioners: { cursor: () => null } }, @@ -59,7 +64,9 @@ describe('ExecutionDetails > Timeline', () => { render( - + + + , ); diff --git a/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx b/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx index b2338437f..f674002b9 100644 --- a/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -3,8 +3,7 @@ import { formatDateLocalTimezone, formatDateUTC, millisecondsToHMS } from 'commo import { timestampToDate } from 'common/utils'; import { useCommonStyles } from 'components/common/styles'; import { isEqual } from 'lodash'; -import { CatalogCacheStatus, NodeExecutionPhase } from 'models/Execution/enums'; -import { TaskNodeMetadata } from 'models/Execution/types'; +import { NodeExecutionPhase } from 'models/Execution/enums'; import * as React from 'react'; import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; @@ -107,15 +106,6 @@ const DisplayType: React.FC = ({ execution }) => return {type}; }; -const hiddenCacheStatuses = [CatalogCacheStatus.CACHE_MISS, CatalogCacheStatus.CACHE_DISABLED]; -function hasCacheStatus(taskNodeMetadata?: TaskNodeMetadata): taskNodeMetadata is TaskNodeMetadata { - if (!taskNodeMetadata) { - return false; - } - const { cacheStatus } = taskNodeMetadata; - return !hiddenCacheStatuses.includes(cacheStatus); -} - export function generateColumns( styles: ReturnType, ): NodeExecutionColumnDefinition[] { diff --git a/packages/zapp/console/src/components/Executions/contexts.ts b/packages/zapp/console/src/components/Executions/contexts.ts index 691d464ee..faeb47b6d 100644 --- a/packages/zapp/console/src/components/Executions/contexts.ts +++ b/packages/zapp/console/src/components/Executions/contexts.ts @@ -9,6 +9,7 @@ export interface ExecutionContextData { export const ExecutionContext = React.createContext( {} as ExecutionContextData, ); -export const NodeExecutionsContext = React.createContext>({}); + +export const NodeExecutionsByIdContext = React.createContext>({}); export const NodeExecutionsRequestConfigContext = React.createContext({}); diff --git a/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts b/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts index f31766060..556832414 100644 --- a/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts +++ b/packages/zapp/console/src/components/Executions/nodeExecutionQueries.ts @@ -280,86 +280,6 @@ function fetchChildNodeExecutionGroups( return fetchGroupsForTaskExecutionNode(queryClient, nodeExecution, config); } -/** - * Query returns all children for a list of `nodeExecutions` - * Will recursively gather all children for anyone that isParent() - */ -async function fetchAllChildNodeExecutions( - queryClient: QueryClient, - nodeExecutions: NodeExecution[], - config: RequestConfig, -): Promise> { - const executionGroups: Array = await Promise.all( - nodeExecutions.map((exe) => fetchChildNodeExecutionGroups(queryClient, exe, config)), - ); - - /** Recursive check for nested/dynamic nodes */ - const childrenFromChildrenNodes: NodeExecution[] = []; - executionGroups.map((group) => - group.map((attempt) => { - attempt.nodeExecutions.map((execution) => { - if (isParentNode(execution)) { - childrenFromChildrenNodes.push(execution); - } - }); - }), - ); - - /** Request and concact data from children */ - if (childrenFromChildrenNodes.length > 0) { - const childGroups = await fetchAllChildNodeExecutions( - queryClient, - childrenFromChildrenNodes, - config, - ); - for (const group in childGroups) { - executionGroups.push(childGroups[group]); - } - } - return executionGroups; -} - -/** - * - * @param nodeExecutions list of parent node executionId's - * @param config - * @returns - */ -export function useAllChildNodeExecutionGroupsQuery( - nodeExecutions: NodeExecution[], - config: RequestConfig, -): QueryObserverResult, Error> { - const queryClient = useQueryClient(); - const shouldEnableFn = (groups) => { - if (groups.length > 0) { - return groups.some((group) => { - // non-empty groups are wrapped in array - const unwrappedGroup = Array.isArray(group) ? group[0] : group; - if (unwrappedGroup?.nodeExecutions?.length > 0) { - /* Return true is any executions are not yet terminal (ie, they can change) */ - return unwrappedGroup.nodeExecutions.some((ne) => { - return !nodeExecutionIsTerminal(ne); - }); - } else { - return false; - } - }); - } else { - return false; - } - }; - - const key = `${nodeExecutions?.[0]?.scopedId}-${nodeExecutions?.[0]?.closure?.phase}`; - - return useConditionalQuery>( - { - queryKey: [QueryType.NodeExecutionChildList, key, config], - queryFn: () => fetchAllChildNodeExecutions(queryClient, nodeExecutions, config), - }, - shouldEnableFn, - ); -} - /** Fetches and groups `NodeExecution`s which are direct children of the given * `NodeExecution`. */ @@ -438,15 +358,29 @@ export function useAllTreeNodeExecutionGroupsQuery( ): QueryObserverResult { const queryClient = useQueryClient(); const shouldEnableFn = (groups) => { - if (nodeExecutions.some((ne) => !nodeExecutionIsTerminal(ne))) { - return true; + if (groups.length > 0) { + return groups.some((group) => { + // non-empty groups are wrapped in array + const unwrappedGroup = Array.isArray(group) ? group[0] : group; + if (unwrappedGroup?.nodeExecutions?.length > 0) { + /* Return true is any executions are not yet terminal (ie, they can change) */ + return unwrappedGroup.nodeExecutions.some((ne) => { + return !nodeExecutionIsTerminal(ne); + }); + } else { + return false; + } + }); + } else { + return false; } - return groups.some((group) => !nodeExecutionIsTerminal(group)); }; + const key = `${nodeExecutions?.[0]?.scopedId}-${nodeExecutions?.[0]?.closure?.phase}`; + return useConditionalQuery( { - queryKey: [QueryType.NodeExecutionTreeList, nodeExecutions[0]?.id, config], + queryKey: [QueryType.NodeExecutionTreeList, key, config], queryFn: () => fetchAllTreeNodeExecutions(queryClient, nodeExecutions, config), refetchInterval: executionRefreshIntervalMs, }, diff --git a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx index 2458aa45e..8c83285b3 100644 --- a/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx +++ b/packages/zapp/console/src/components/Launch/LaunchForm/MapInput.tsx @@ -155,16 +155,16 @@ export const MapInput = (props: InputProps) => { setData((data) => [...data, getNewMapItem(data.length)]); }; - const updateUpperStream = () => { + const updateUpperStream = (newData: MapInputItem[]) => { let newError = false; - data.forEach((item) => { + newData.forEach((item) => { if (item.id === null || !item.key?.length || !item.value?.length) newError = true; else { if (data.findIndex(({ key, id }) => id !== item.id && key === item.key) >= 0) newError = true; } }); - const newPairs = data + const newPairs = newData .filter((item) => { // we filter out delted values and items with errors or empty keys/values return item.id !== null && !!item.key && !!item.value; @@ -182,32 +182,29 @@ export const MapInput = (props: InputProps) => { const onSetKey = (id: number | null, key: string) => { if (id === null) return; - setData((data) => { - data[id].key = key; - return [...data]; - }); - updateUpperStream(); + const newData = [...data]; + newData[id].key = key; + setData([...newData]); + updateUpperStream([...newData]); }; const onSetValue = (id: number | null, value: string) => { if (id === null) return; - setData((data) => { - data[id].value = value; - return [...data]; - }); - updateUpperStream(); + const newData = [...data]; + newData[id].value = value; + setData([...newData]); + updateUpperStream([...newData]); }; const onDeleteItem = (id: number | null) => { if (id === null) return; - setData((data) => { - const dataIndex = data.findIndex((item) => item.id === id); - if (dataIndex >= 0 && dataIndex < data.length) { - return [...data.splice(0, dataIndex), ...data.splice(dataIndex + 1)]; - } - return [...data]; - }); - updateUpperStream(); + const newData = [...data]; + const dataIndex = newData.findIndex((item) => item.id === id); + if (dataIndex >= 0 && dataIndex < newData.length) { + newData[dataIndex].id = null; + } + setData([...newData]); + updateUpperStream([...newData]); }; const isValid = (id: number | null, value: string) => { diff --git a/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx b/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx index e8b7137a5..b5e985e40 100644 --- a/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx +++ b/packages/zapp/console/src/components/LaunchPlan/SearchableLaunchPlanNameList.tsx @@ -4,10 +4,11 @@ import { useNamedEntityListStyles } from 'components/common/SearchableNamedEntit import { useCommonStyles } from 'components/common/styles'; import { separatorColor, primaryTextColor, launchPlanLabelColor } from 'components/Theme/constants'; import * as React from 'react'; +import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { Routes } from 'routes/routes'; import { debounce } from 'lodash'; -import { Typography, FormGroup } from '@material-ui/core'; +import { FormGroup } from '@material-ui/core'; import { ResourceType } from 'models/Common/types'; import { MuiLaunchPlanIcon } from '@flyteconsole/ui-atoms'; import { LaunchPlanListStructureItem } from './types'; @@ -120,18 +121,22 @@ export const SearchableLaunchPlanNameList: React.FC { const styles = useStyles(); - const [search, setSearch] = React.useState(''); + const [search, setSearch] = useState(''); const { results, setSearchString } = useSearchableListState({ items: launchPlans, propertyGetter: ({ id }) => id.name, }); + useEffect(() => { + const debouncedSearch = debounce(() => setSearchString(search), 1000); + debouncedSearch(); + }, [search]); + const onSearchChange = (event: React.ChangeEvent) => { const searchString = event.target.value; setSearch(searchString); - const debouncedSearch = debounce(() => setSearchString(searchString), 1000); - debouncedSearch(); }; + const onClear = () => setSearch(''); return ( diff --git a/packages/zapp/console/src/components/Literals/__stories__/helpers/typeGenerators.ts b/packages/zapp/console/src/components/Literals/__stories__/helpers/typeGenerators.ts index edf237870..4974f3ab1 100644 --- a/packages/zapp/console/src/components/Literals/__stories__/helpers/typeGenerators.ts +++ b/packages/zapp/console/src/components/Literals/__stories__/helpers/typeGenerators.ts @@ -1,8 +1,5 @@ import { Core } from 'flyteidl'; -import { - blobScalars, - schemaScalars, -} from '../scalarValues'; +import { blobScalars, schemaScalars } from '../scalarValues'; // SIMPLE type GeneratedSimpleType = { diff --git a/packages/zapp/console/src/components/Literals/test/helpers/mock_simpleTypes.ts b/packages/zapp/console/src/components/Literals/test/helpers/mock_simpleTypes.ts index 0bd9d543f..16e7e8f80 100644 --- a/packages/zapp/console/src/components/Literals/test/helpers/mock_simpleTypes.ts +++ b/packages/zapp/console/src/components/Literals/test/helpers/mock_simpleTypes.ts @@ -1,17 +1,17 @@ import { Core } from 'flyteidl'; export function extractSimpleTypes() { - const simpleTypes= Object.keys(Core.SimpleType).map((key) => ({ - [key]: { - type: 'simple', - simple: Core.SimpleType[key], - }, - })).reduce((acc, v) => ({...acc, ...v}), {}); + const simpleTypes = Object.keys(Core.SimpleType) + .map((key) => ({ + [key]: { + type: 'simple', + simple: Core.SimpleType[key], + }, + })) + .reduce((acc, v) => ({ ...acc, ...v }), {}); return simpleTypes; } const simple: Core.SimpleType[] = extractSimpleTypes() as any; -export { - simple -}; +export { simple }; diff --git a/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx b/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx index 5d4da6a19..2d4514a99 100644 --- a/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx +++ b/packages/zapp/console/src/components/Workflow/SearchableWorkflowNameList.tsx @@ -5,6 +5,7 @@ import { useNamedEntityListStyles } from 'components/common/SearchableNamedEntit import { useCommonStyles } from 'components/common/styles'; import { separatorColor, primaryTextColor, workflowLabelColor } from 'components/Theme/constants'; import * as React from 'react'; +import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { Routes } from 'routes/routes'; import { Shimmer } from 'components/common/Shimmer'; @@ -23,7 +24,6 @@ import ArchiveOutlined from '@material-ui/icons/ArchiveOutlined'; import { useMutation } from 'react-query'; import { NamedEntityState } from 'models/enums'; import { updateWorkflowState } from 'models/Workflow/api'; -import { useState } from 'react'; import { useSnackbar } from 'notistack'; import { padExecutionPaths, padExecutions } from 'common/utils'; import { WorkflowListStructureItem } from './types'; @@ -322,18 +322,22 @@ export const SearchableWorkflowNameList: React.FC { const styles = useStyles(); - const [search, setSearch] = React.useState(''); + const [search, setSearch] = useState(''); const { results, setSearchString } = useSearchableListState({ items: workflows, propertyGetter: ({ id }) => id.name, }); + useEffect(() => { + const debouncedSearch = debounce(() => setSearchString(search), 1000); + debouncedSearch(); + }, [search]); + const onSearchChange = (event: React.ChangeEvent) => { const searchString = event.target.value; setSearch(searchString); - const debouncedSearch = debounce(() => setSearchString(searchString), 1000); - debouncedSearch(); }; + const onClear = () => setSearch(''); return ( diff --git a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx index ff0035bb4..fad6e0902 100644 --- a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx +++ b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx @@ -11,6 +11,9 @@ import { makeNodeExecutionDynamicWorkflowQuery } from 'components/Workflow/workf import { createDebugLogger } from 'common/log'; import { CompiledNode } from 'models/Node/types'; import { TaskExecutionPhase } from 'models/Execution/enums'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; +import { useContext } from 'react'; +import { checkForDynamicExecutions } from 'components/common/utils'; import { transformerWorkflowToDag } from './transformerWorkflowToDag'; export interface WorkflowGraphProps { @@ -19,7 +22,6 @@ export interface WorkflowGraphProps { selectedPhase?: TaskExecutionPhase; isDetailsTabClosed: boolean; workflow: Workflow; - nodeExecutionsById?: any; } interface PrepareDAGResult { @@ -63,42 +65,12 @@ export const WorkflowGraph: React.FC = (props) => { onPhaseSelectionChanged, selectedPhase, isDetailsTabClosed, - nodeExecutionsById, workflow, } = props; + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); const { dag, staticExecutionIdsMap, error } = workflowToDag(workflow); - /** - * Note: - * Dynamic nodes are deteremined at runtime and thus do not come - * down as part of the workflow closure. We can detect and place - * dynamic nodes by finding orphan execution id's and then mapping - * those executions into the dag by using the executions 'uniqueParentId' - * to render that node as a subworkflow - */ - const checkForDynamicExeuctions = (allExecutions, staticExecutions) => { - const parentsToFetch = {}; - for (const executionId in allExecutions) { - if (!staticExecutions[executionId]) { - const dynamicExecution = allExecutions[executionId]; - const dynamicExecutionId = dynamicExecution.metadata.specNodeId || dynamicExecution.id; - const uniqueParentId = dynamicExecution.fromUniqueParentId; - if (uniqueParentId) { - if (parentsToFetch[uniqueParentId]) { - parentsToFetch[uniqueParentId].push(dynamicExecutionId); - } else { - parentsToFetch[uniqueParentId] = [dynamicExecutionId]; - } - } - } - } - const result = {}; - for (const parentId in parentsToFetch) { - result[parentId] = allExecutions[parentId]; - } - return result; - }; - const dynamicParents = checkForDynamicExeuctions(nodeExecutionsById, staticExecutionIdsMap); + const dynamicParents = checkForDynamicExecutions(nodeExecutionsById, staticExecutionIdsMap); const dynamicWorkflowQuery = useQuery(makeNodeExecutionDynamicWorkflowQuery(dynamicParents)); const renderReactFlowGraph = (dynamicWorkflows) => { debug('DynamicWorkflows:', dynamicWorkflows); @@ -118,7 +90,6 @@ export const WorkflowGraph: React.FC = (props) => { return ( ({ ReactFlowWrapper: jest.fn(({ children }) => ( @@ -27,7 +26,6 @@ describe('WorkflowGraph', () => { onNodeSelectionChanged={jest.fn} onPhaseSelectionChanged={jest.fn} workflow={workflow} - nodeExecutionsById={nodeExecutionsById} isDetailsTabClosed={true} /> , diff --git a/packages/zapp/console/src/components/WorkflowGraph/utils.ts b/packages/zapp/console/src/components/WorkflowGraph/utils.ts index 2930f6062..d8e104652 100644 --- a/packages/zapp/console/src/components/WorkflowGraph/utils.ts +++ b/packages/zapp/console/src/components/WorkflowGraph/utils.ts @@ -27,22 +27,6 @@ export function isExpanded(node: any) { return !!node.expanded; } -/** - * Utility funciton assumes (loose) parity between [a]->[b] if matching - * keys have matching values. - * @param a object - * @param b object - * @returns boolean - */ -export const checkIfObjectsAreSame = (a, b) => { - for (const k in a) { - if (a[k] != b[k]) { - return false; - } - } - return true; -}; - /** * Returns a display name from either workflows or nodes * @param context input can be either CompiledWorkflow or CompiledNode @@ -110,11 +94,12 @@ export const getNodeTypeFromCompiledNode = (node: CompiledNode): dTypes => { }; export const getSubWorkflowFromId = (id, workflow) => { + const _ = require('lodash'); const { subWorkflows } = workflow; /* Find current matching entitity from subWorkflows */ for (const k in subWorkflows) { const subWorkflowId = subWorkflows[k].template.id; - if (checkIfObjectsAreSame(subWorkflowId, id)) { + if (_.isEqual(subWorkflowId, id)) { return subWorkflows[k]; } } @@ -122,11 +107,12 @@ export const getSubWorkflowFromId = (id, workflow) => { }; export const getTaskTypeFromCompiledNode = (taskNode: TaskNode, tasks: CompiledTask[]) => { + const _ = require('lodash'); for (let i = 0; i < tasks.length; i++) { const compiledTask: CompiledTask = tasks[i]; const taskTemplate: TaskTemplate = compiledTask.template; const templateId: Identifier = taskTemplate.id; - if (checkIfObjectsAreSame(templateId, taskNode.referenceId)) { + if (_.isEqual(templateId, taskNode.referenceId)) { return compiledTask; } } diff --git a/packages/zapp/console/src/components/common/FilterableNamedEntityList.tsx b/packages/zapp/console/src/components/common/FilterableNamedEntityList.tsx index 74afadbb9..fe713433f 100644 --- a/packages/zapp/console/src/components/common/FilterableNamedEntityList.tsx +++ b/packages/zapp/console/src/components/common/FilterableNamedEntityList.tsx @@ -2,6 +2,7 @@ import { Checkbox, debounce, FormControlLabel, FormGroup } from '@material-ui/co import { makeStyles, Theme } from '@material-ui/core/styles'; import { NamedEntity } from 'models/Common/types'; import * as React from 'react'; +import { useEffect, useState } from 'react'; import { NoResults } from './NoResults'; import { SearchableInput, SearchResult } from './SearchableList'; import { useCommonStyles } from './styles'; @@ -60,18 +61,21 @@ export const FilterableNamedEntityList: React.FC archiveCheckboxLabel, }) => { const styles = useStyles(); - const [search, setSearch] = React.useState(''); + const [search, setSearch] = useState(''); const { results, setSearchString } = useSearchableListState({ items: names, propertyGetter: ({ id }) => id.name, }); + useEffect(() => { + const debouncedSearch = debounce(() => setSearchString(search), 1000); + debouncedSearch(); + }, [search]); + const onSearchChange = (event: React.ChangeEvent) => { const searchString = event.target.value; setSearch(searchString); - const debouncedSearch = debounce(() => setSearchString(searchString), 1000); - debouncedSearch(); }; const onClear = () => setSearch(''); diff --git a/packages/zapp/console/src/components/common/utils.ts b/packages/zapp/console/src/components/common/utils.ts index c6c521874..4612bddbe 100644 --- a/packages/zapp/console/src/components/common/utils.ts +++ b/packages/zapp/console/src/components/common/utils.ts @@ -16,3 +16,34 @@ export function measureText(fontDefinition: string, text: string) { context.font = fontDefinition; return context.measureText(text); } + +/** + * Note: + * Dynamic nodes are deteremined at runtime and thus do not come + * down as part of the workflow closure. We can detect and place + * dynamic nodes by finding orphan execution id's and then mapping + * those executions into the dag by using the executions 'uniqueParentId' + * to render that node as a subworkflow + */ +export const checkForDynamicExecutions = (allExecutions, staticExecutions) => { + const parentsToFetch = {}; + for (const executionId in allExecutions) { + if (!staticExecutions[executionId]) { + const dynamicExecution = allExecutions[executionId]; + const dynamicExecutionId = dynamicExecution.metadata.specNodeId || dynamicExecution.id; + const uniqueParentId = dynamicExecution.fromUniqueParentId; + if (uniqueParentId) { + if (parentsToFetch[uniqueParentId]) { + parentsToFetch[uniqueParentId].push(dynamicExecutionId); + } else { + parentsToFetch[uniqueParentId] = [dynamicExecutionId]; + } + } + } + } + const result = {}; + for (const parentId in parentsToFetch) { + result[parentId] = allExecutions[parentId]; + } + return result; +}; diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx index 2fb41b801..b84d2f8bc 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useContext } from 'react'; import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2'; +import { NodeExecutionsByIdContext } from 'components/Executions/contexts'; import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types'; import { getRFBackground } from './utils'; import { ReactFlowWrapper } from './ReactFlowWrapper'; @@ -50,9 +51,9 @@ const ReactFlowGraphComponent = (props) => { onPhaseSelectionChanged, selectedPhase, isDetailsTabClosed, - nodeExecutionsById, dynamicWorkflows, } = props; + const nodeExecutionsById = useContext(NodeExecutionsByIdContext); const [state, setState] = useState({ data, dynamicWorkflows, diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx index d7c8bd13a..b2a806eff 100644 --- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx +++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx @@ -41,7 +41,7 @@ export const ReactFlowWrapper: React.FC = ({ edges: rfGraphJson.edges, version: version, reactFlowInstance: null, - needFitView: false + needFitView: false, }); useEffect(() => { @@ -49,7 +49,7 @@ export const ReactFlowWrapper: React.FC = ({ ...state, shouldUpdate: true, nodes: rfGraphJson.nodes, - edges: rfGraphJson.edges.map(edge => ({ ...edge, zIndex: 0 })), + edges: rfGraphJson.edges.map((edge) => ({ ...edge, zIndex: 0 })), })); }, [rfGraphJson]); @@ -96,8 +96,8 @@ export const ReactFlowWrapper: React.FC = ({ }; const onNodeClick = () => { - setState((state) => ({ ...state, needFitView: false })) - } + setState((state) => ({ ...state, needFitView: false })); + }; return ( =2.2.7 <3": +through@2, "through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -18589,6 +18826,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -18788,6 +19032,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -19496,7 +19745,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -wcwidth@^1.0.0: +wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=