diff --git a/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx b/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx index 286f1fbd3..d818f1434 100644 --- a/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx +++ b/src/components/Executions/ExecutionDetails/ExecutionNodeViews.tsx @@ -7,6 +7,7 @@ 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 { NodeExecutionDetailsContextProvider } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionsRequestConfigContext } from '../contexts'; import { ExecutionFilters } from '../ExecutionFilters'; import { useNodeExecutionFiltersState } from '../filters/useExecutionFiltersState'; @@ -85,29 +86,33 @@ export const ExecutionNodeViews: React.FC = ({ -
- {tabState.value === tabs.nodes.id && ( - <> -
- -
+ +
+ {tabState.value === tabs.nodes.id && ( + <> +
+ +
+ + {renderNodeExecutionsTable} + + + )} + {tabState.value === tabs.graph.id && ( - {renderNodeExecutionsTable} + {renderExecutionLoader} - - )} - {tabState.value === tabs.graph.id && ( - - {renderExecutionLoader} - - )} -
+ )} +
+ ); }; diff --git a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx index 793789d7d..c31290ecf 100644 --- a/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx +++ b/src/components/Executions/ExecutionDetails/NodeExecutionDetailsPanelContent.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { IconButton, Typography } from '@material-ui/core'; import { makeStyles, Theme } from '@material-ui/core/styles'; import Tab from '@material-ui/core/Tab'; @@ -32,7 +32,7 @@ import { } from '../nodeExecutionQueries'; import { TaskExecutionsList } from '../TaskExecutionsList/TaskExecutionsList'; import { NodeExecutionDetails } from '../types'; -import { useNodeExecutionDetails } from '../useNodeExecutionDetails'; +import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { NodeExecutionInputs } from './NodeExecutionInputs'; import { NodeExecutionOutputs } from './NodeExecutionOutputs'; import { NodeExecutionTaskDetails } from './NodeExecutionTaskDetails'; @@ -210,6 +210,14 @@ const NodeExecutionTabs: React.FC<{ const styles = useStyles(); const tabState = useTabState(tabIds, defaultTab); + if (tabState.value === tabIds.task && !taskTemplate) { + // Reset tab value, if task tab is selected, while no taskTemplate is avaible + // can happen when user switches between nodeExecutions without closing the drawer + tabState.onChange(() => { + /* */ + }, defaultTab); + } + let tabContent: JSX.Element | null = null; switch (tabState.value) { case tabIds.executions: { @@ -293,15 +301,23 @@ export const NodeExecutionDetailsPanelContent: React.FC { - const [mounted, setMounted] = useState(true); + const isMounted = useRef(false); useEffect(() => { + isMounted.current = true; return () => { - setMounted(false); + isMounted.current = false; }; }, []); + const queryClient = useQueryClient(); + const detailsContext = useNodeExecutionContext(); + const [isReasonsVisible, setReasonsVisible] = React.useState(false); const [dag, setDag] = React.useState(null); + const [details, setDetails] = React.useState< + NodeExecutionDetails | undefined + >(); + const nodeExecutionQuery = useQuery({ ...makeNodeExecutionQuery(nodeExecutionId), // The selected NodeExecution has been fetched at this point, we don't want to @@ -309,6 +325,19 @@ export const NodeExecutionDetailsPanelContent: React.FC { + let isCurrent = true; + detailsContext.getNodeExecutionDetails(nodeExecution).then(res => { + if (isCurrent) { + setDetails(res); + } + }); + + return () => { + isCurrent = false; + }; + }); + React.useEffect(() => { setReasonsVisible(false); }, [nodeExecutionId]); @@ -326,7 +355,7 @@ export const NodeExecutionDetailsPanelContent: React.FC - ); - const taskTemplate = detailsQuery.data - ? detailsQuery.data.taskTemplate - : null; + const displayName = details?.displayName ?? ; const isRunningPhase = React.useMemo(() => { return ( @@ -400,17 +421,14 @@ export const NodeExecutionDetailsPanelContent: React.FC - + ) : null; const tabsContent = nodeExecution ? ( ) : null; return ( diff --git a/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx b/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx index 5b01df693..71aa5dca1 100644 --- a/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx +++ b/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx @@ -3,8 +3,10 @@ import { cacheStatusMessages, viewSourceExecutionString } from 'components/Executions/constants'; +import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { Core } from 'flyteidl'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; +import { mockWorkflowId } from 'mocks/data/fixtures/types'; import { insertFixture } from 'mocks/data/insertFixture'; import { mockServer } from 'mocks/server'; import { NodeExecution, TaskNodeMetadata } from 'models/Execution/types'; @@ -17,6 +19,9 @@ import { makeIdentifier } from 'test/modelUtils'; import { createTestQueryClient } from 'test/utils'; import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent'; +jest.mock('components/Workflow/workflowQueries'); +const { fetchWorkflow } = require('components/Workflow/workflowQueries'); + describe('NodeExecutionDetails', () => { let fixture: ReturnType; let execution: NodeExecution; @@ -27,6 +32,9 @@ describe('NodeExecutionDetails', () => { execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); queryClient = createTestQueryClient(); }); @@ -34,9 +42,13 @@ describe('NodeExecutionDetails', () => { render( - + + + ); diff --git a/src/components/Executions/Tables/__stories__/NodeExecutionsTable.stories.tsx b/src/components/Executions/Tables/__stories__/NodeExecutionsTable.stories.tsx index e1ccbe541..37d5b374a 100644 --- a/src/components/Executions/Tables/__stories__/NodeExecutionsTable.stories.tsx +++ b/src/components/Executions/Tables/__stories__/NodeExecutionsTable.stories.tsx @@ -1,5 +1,6 @@ import { makeStyles, Theme } from '@material-ui/core/styles'; import { storiesOf } from '@storybook/react'; +import { NodeExecutionDetailsContext } from 'components/Executions/contextProvider/NodeExecutionDetails'; import { makeNodeExecutionListQuery } from 'components/Executions/nodeExecutionQueries'; import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow'; import * as React from 'react'; @@ -19,6 +20,14 @@ const useStyles = makeStyles((theme: Theme) => ({ const fixture = basicPythonWorkflow.generate(); const workflowExecution = fixture.workflowExecutions.top.data; +const getNodeExecutionDetails = async () => { + return { + displayId: 'node0', + displayName: 'basic.byton.workflow.unique.task_name', + displayType: 'Python-Task' + }; +}; + const stories = storiesOf('Tables/NodeExecutionsTable', module); stories.addDecorator(story => { return
{story()}
; @@ -28,9 +37,21 @@ stories.add('Basic', () => { makeNodeExecutionListQuery(useQueryClient(), workflowExecution.id) ); return query.data ? ( - + + + ) : (
); }); -stories.add('With no items', () => ); +stories.add('With no items', () => { + return ( + + + + ); +}); diff --git a/src/components/Executions/Tables/constants.ts b/src/components/Executions/Tables/constants.ts index 01f7e4a2b..75f36a211 100644 --- a/src/components/Executions/Tables/constants.ts +++ b/src/components/Executions/Tables/constants.ts @@ -9,7 +9,7 @@ export const workflowExecutionsTableColumnWidths = { export const nodeExecutionsTableColumnWidths = { duration: 100, - logs: 225, + logs: 100, type: 144, nodeId: 144, name: 380, diff --git a/src/components/Executions/Tables/nodeExecutionColumns.tsx b/src/components/Executions/Tables/nodeExecutionColumns.tsx index 56a331a4b..cb85543a6 100644 --- a/src/components/Executions/Tables/nodeExecutionColumns.tsx +++ b/src/components/Executions/Tables/nodeExecutionColumns.tsx @@ -6,16 +6,14 @@ import { } from 'common/formatters'; import { timestampToDate } from 'common/utils'; import { useCommonStyles } from 'components/common/styles'; -import { WaitForQuery } from 'components/common/WaitForQuery'; import { Core } from 'flyteidl'; import { isEqual } from 'lodash'; import { NodeExecutionPhase } from 'models/Execution/enums'; import { TaskNodeMetadata } from 'models/Execution/types'; import * as React from 'react'; +import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails'; import { ExecutionStatusBadge } from '../ExecutionStatusBadge'; import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus'; -import { NodeExecutionDetails } from '../types'; -import { useNodeExecutionDetails } from '../useNodeExecutionDetails'; import { getNodeExecutionTimingMS } from '../utils'; import { SelectNodeExecutionLink } from './SelectNodeExecutionLink'; import { useColumnStyles } from './styles'; @@ -24,11 +22,25 @@ import { NodeExecutionColumnDefinition } from './types'; -const NodeExecutionName: React.FC = ({ +const ExecutionName: React.FC = ({ execution, state }) => { - const detailsQuery = useNodeExecutionDetails(execution); + const detailsContext = useNodeExecutionContext(); + const [displayName, setDisplayName] = React.useState(); + + React.useEffect(() => { + let isCurrent = true; + detailsContext.getNodeExecutionDetails(execution).then(res => { + if (isCurrent) { + setDisplayName(res.displayName); + } + }); + return () => { + isCurrent = false; + }; + }); + const commonStyles = useCommonStyles(); const styles = useColumnStyles(); @@ -36,63 +48,70 @@ const NodeExecutionName: React.FC = ({ state.selectedExecution != null && isEqual(execution.id, state.selectedExecution); - const renderReadableName = ({ displayName }: NodeExecutionDetails) => { - const truncatedName = displayName?.split('.').pop() || ''; - const readableName = isSelected ? ( - - {truncatedName} - - ) : ( - - ); - return ( - <> - {readableName} - - {displayName} - - - ); - }; + const name = displayName ?? execution.id.nodeId; + const truncatedName = name?.split('.').pop() || name; + + const readableName = isSelected ? ( + + {truncatedName} + + ) : ( + + ); return ( <> - - {renderReadableName} - + {readableName} + + {displayName} + ); }; -const NodeExecutionDisplayId: React.FC = ({ - execution -}) => { - const detailsQuery = useNodeExecutionDetails(execution); - const extractDisplayId = ({ displayId }: NodeExecutionDetails) => - displayId || execution.id.nodeId; - return {extractDisplayId}; +const DisplayId: React.FC = ({ execution }) => { + const detailsContext = useNodeExecutionContext(); + const [displayId, setDisplayId] = React.useState(); + + React.useEffect(() => { + let isCurrent = true; + detailsContext.getNodeExecutionDetails(execution).then(res => { + if (isCurrent) { + setDisplayId(res.displayId); + } + }); + return () => { + isCurrent = false; + }; + }); + + return <>{displayId ?? execution.id.nodeId}; }; -const NodeExecutionDisplayType: React.FC = ({ +const DisplayType: React.FC = ({ execution }) => { - const detailsQuery = useNodeExecutionDetails(execution); - const extractDisplayType = ({ displayType }: NodeExecutionDetails) => ( - - {displayType || execution.id.nodeId} - - ); - return ( - {extractDisplayType} - ); + const detailsContext = useNodeExecutionContext(); + const [type, setType] = React.useState(); + + React.useEffect(() => { + let isCurrent = true; + detailsContext.getNodeExecutionDetails(execution).then(res => { + if (isCurrent) { + setType(res.displayType); + } + }); + return () => { + isCurrent = false; + }; + }); + + return {type}; }; const hiddenCacheStatuses = [ @@ -114,19 +133,19 @@ export function generateColumns( ): NodeExecutionColumnDefinition[] { return [ { - cellRenderer: props => , + cellRenderer: props => , className: styles.columnName, key: 'name', label: 'task name' }, { - cellRenderer: props => , + cellRenderer: props => , className: styles.columnNodeId, key: 'nodeId', label: 'node id' }, { - cellRenderer: props => , + cellRenderer: props => , className: styles.columnType, key: 'type', label: 'type' diff --git a/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx b/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx index 19846e6f5..022b29f2e 100644 --- a/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx +++ b/src/components/Executions/Tables/test/NodeExecutionsTable.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, getAllByRole, + getAllByText, getByText, getByTitle, render, @@ -8,6 +9,8 @@ import { waitFor } from '@testing-library/react'; import { cacheStatusMessages } from 'components/Executions/constants'; +import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails'; +import { UNKNOWN_DETAILS } from 'components/Executions/contextProvider/NodeExecutionDetails/types'; import { ExecutionContext, ExecutionContextData, @@ -26,6 +29,7 @@ import { dynamicPythonTaskWorkflow } from 'mocks/data/fixtures/dynamicPythonWorkflow'; import { oneFailedTaskWorkflow } from 'mocks/data/fixtures/oneFailedTaskWorkflow'; +import { mockWorkflowId } from 'mocks/data/fixtures/types'; import { insertFixture } from 'mocks/data/insertFixture'; import { notFoundError } from 'mocks/errors'; import { mockServer } from 'mocks/server'; @@ -48,6 +52,10 @@ import { } from 'test/utils'; import { titleStrings } from '../constants'; import { NodeExecutionsTable } from '../NodeExecutionsTable'; +import * as moduleApi from 'components/Executions/contextProvider/NodeExecutionDetails/getTaskThroughExecution'; + +jest.mock('components/Workflow/workflowQueries'); +const { fetchWorkflow } = require('components/Workflow/workflowQueries'); describe('NodeExecutionsTable', () => { let workflowExecution: Execution; @@ -113,12 +121,111 @@ describe('NodeExecutionsTable', () => { value={requestConfig} > - + + + ); + describe('when rendering the DetailsPanel', () => { + let nodeExecution: NodeExecution; + let fixture: ReturnType; + beforeEach(() => { + fixture = basicPythonWorkflow.generate(); + workflowExecution = fixture.workflowExecutions.top.data; + insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); + + executionContext = { + execution: workflowExecution + }; + nodeExecution = + fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; + }); + + const updateNodeExecutions = (executions: NodeExecution[]) => { + executions.forEach(mockServer.insertNodeExecution); + mockServer.insertNodeExecutionList( + fixture.workflowExecutions.top.data.id, + executions + ); + }; + + it('should render updated state if selected nodeExecution object changes', async () => { + nodeExecution.closure.phase = NodeExecutionPhase.RUNNING; + updateNodeExecutions([nodeExecution]); + const truncatedName = + fixture.tasks.python.id.name.split('.').pop() || ''; + // Render table, click first node + const { container } = renderTable(); + const detailsPanel = await selectNode( + container, + truncatedName, + nodeExecution.id.nodeId + ); + expect(getByText(detailsPanel, 'Running')).toBeInTheDocument(); + + const updatedExecution = cloneDeep(nodeExecution); + updatedExecution.closure.phase = NodeExecutionPhase.FAILED; + updateNodeExecutions([updatedExecution]); + await waitFor(() => expect(getByText(detailsPanel, 'Failed'))); + }); + + describe('with nested children', () => { + let fixture: ReturnType; + beforeEach(() => { + fixture = dynamicPythonNodeExecutionWorkflow.generate(); + workflowExecution = fixture.workflowExecutions.top.data; + insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); + executionContext = { execution: workflowExecution }; + }); + + it('should correctly render details for nested executions', async () => { + const childNodeExecution = + fixture.workflowExecutions.top.nodeExecutions.dynamicNode + .nodeExecutions.firstChild.data; + const { container } = renderTable(); + const dynamicTaskNameEl = await waitFor(() => + getByText(container, fixture.tasks.dynamic.id.name) + ); + const dynamicRowEl = findNearestAncestorByRole( + dynamicTaskNameEl, + 'listitem' + ); + const parentNodeEl = await expandParentNode(dynamicRowEl); + const truncatedName = + fixture.tasks.python.id.name.split('.').pop() || ''; + await selectNode( + parentNodeEl[0], + truncatedName, + childNodeExecution.id.nodeId + ); + + // Wait for Details Panel to render and then for the nodeId header + const detailsPanel = await waitFor(() => + screen.getByTestId('details-panel') + ); + await waitFor(() => + expect( + getByText(detailsPanel, childNodeExecution.id.nodeId) + ) + ); + expect( + getByText(detailsPanel, fixture.tasks.python.id.name) + ).toBeInTheDocument(); + }); + }); + }); + describe('for basic executions', () => { let fixture: ReturnType; @@ -126,6 +233,9 @@ describe('NodeExecutionsTable', () => { fixture = basicPythonWorkflow.generate(); workflowExecution = fixture.workflowExecutions.top.data; insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); executionContext = { execution: workflowExecution @@ -167,10 +277,10 @@ describe('NodeExecutionsTable', () => { const { container } = renderTable(); const pythonNodeNameEl = await waitFor(() => - getByText(container, nodeExecution.id.nodeId) + getAllByText(container, nodeExecution.id.nodeId) ); const rowEl = findNearestAncestorByRole( - pythonNodeNameEl, + pythonNodeNameEl?.[0], 'listitem' ); await waitFor(() => @@ -208,11 +318,12 @@ describe('NodeExecutionsTable', () => { it(`renders correct icon for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { taskNodeMetadata.cacheStatus = cacheStatusValue; updateNodeExecutions([cachedNodeExecution]); - const { getByTitle } = await renderTable(); + const { getByTitle } = renderTable(); + await waitFor(() => expect( getByTitle(cacheStatusMessages[cacheStatusValue]) - ) + ).toBeDefined() ); }) ); @@ -224,10 +335,10 @@ describe('NodeExecutionsTable', () => { it(`renders no icon for ${Core.CatalogCacheStatus[cacheStatusValue]}`, async () => { taskNodeMetadata.cacheStatus = cacheStatusValue; updateNodeExecutions([cachedNodeExecution]); - const { getByText, queryByTitle } = await renderTable(); - await waitFor(() => - getByText(cachedNodeExecution.id.nodeId) - ); + const { getByText, queryByTitle } = renderTable(); + await waitFor(() => { + getByText(cachedNodeExecution.id.nodeId); + }); expect( queryByTitle(cacheStatusMessages[cacheStatusValue]) ).toBeNull(); @@ -243,6 +354,9 @@ describe('NodeExecutionsTable', () => { fixture = dynamicPythonNodeExecutionWorkflow.generate(); workflowExecution = fixture.workflowExecutions.top.data; insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); executionContext = { execution: workflowExecution }; }); @@ -338,6 +452,9 @@ describe('NodeExecutionsTable', () => { beforeEach(() => { fixture = dynamicPythonTaskWorkflow.generate(); workflowExecution = fixture.workflowExecutions.top.data; + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); executionContext = { execution: workflowExecution }; @@ -384,13 +501,33 @@ describe('NodeExecutionsTable', () => { describe('without isParentNode flag, using workflowNodeMetadata', () => { let fixture: ReturnType; + let mockGetTaskThroughExecution: any; + beforeEach(() => { fixture = dynamicExternalSubWorkflow.generate(); insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); workflowExecution = fixture.workflowExecutions.top.data; executionContext = { execution: workflowExecution }; + + mockGetTaskThroughExecution = jest.spyOn( + moduleApi, + 'getTaskThroughExecution' + ); + mockGetTaskThroughExecution.mockImplementation(() => { + return Promise.resolve({ + ...UNKNOWN_DETAILS, + displayName: fixture.workflows.sub.id.name + }); + }); + }); + + afterEach(() => { + mockGetTaskThroughExecution.mockReset(); }); it('correctly renders children', async () => { @@ -441,6 +578,9 @@ describe('NodeExecutionsTable', () => { fixture = oneFailedTaskWorkflow.generate(); workflowExecution = fixture.workflowExecutions.top.data; insertFixture(mockServer, fixture); + fetchWorkflow.mockImplementation(() => + Promise.resolve(fixture.workflows.top) + ); // Adding a request filter to only show failed NodeExecutions requestConfig = { filter: [ @@ -458,7 +598,6 @@ describe('NodeExecutionsTable', () => { [nodeExecutions.failedNode.data], { filters: 'eq(phase,FAILED)' } ); - executionContext = { execution: workflowExecution }; @@ -477,93 +616,4 @@ describe('NodeExecutionsTable', () => { ).toBeNull(); }); }); - - describe('when rendering the DetailsPanel', () => { - let nodeExecution: NodeExecution; - let fixture: ReturnType; - beforeEach(() => { - fixture = basicPythonWorkflow.generate(); - workflowExecution = fixture.workflowExecutions.top.data; - insertFixture(mockServer, fixture); - - executionContext = { - execution: workflowExecution - }; - nodeExecution = - fixture.workflowExecutions.top.nodeExecutions.pythonNode.data; - }); - - const updateNodeExecutions = (executions: NodeExecution[]) => { - executions.forEach(mockServer.insertNodeExecution); - mockServer.insertNodeExecutionList( - fixture.workflowExecutions.top.data.id, - executions - ); - }; - - it('should render updated state if selected nodeExecution object changes', async () => { - nodeExecution.closure.phase = NodeExecutionPhase.RUNNING; - updateNodeExecutions([nodeExecution]); - const truncatedName = - fixture.tasks.python.id.name.split('.').pop() || ''; - // Render table, click first node - const { container } = renderTable(); - const detailsPanel = await selectNode( - container, - truncatedName, - nodeExecution.id.nodeId - ); - expect(getByText(detailsPanel, 'Running')).toBeInTheDocument(); - - const updatedExecution = cloneDeep(nodeExecution); - updatedExecution.closure.phase = NodeExecutionPhase.FAILED; - updateNodeExecutions([updatedExecution]); - await waitFor(() => expect(getByText(detailsPanel, 'Failed'))); - }); - - describe('with nested children', () => { - let fixture: ReturnType; - beforeEach(() => { - fixture = dynamicPythonNodeExecutionWorkflow.generate(); - workflowExecution = fixture.workflowExecutions.top.data; - insertFixture(mockServer, fixture); - executionContext = { execution: workflowExecution }; - }); - - it('should correctly render details for nested executions', async () => { - const childNodeExecution = - fixture.workflowExecutions.top.nodeExecutions.dynamicNode - .nodeExecutions.firstChild.data; - const { container } = renderTable(); - const dynamicTaskNameEl = await waitFor(() => - getByText(container, fixture.tasks.dynamic.id.name) - ); - const dynamicRowEl = findNearestAncestorByRole( - dynamicTaskNameEl, - 'listitem' - ); - const parentNodeEl = await expandParentNode(dynamicRowEl); - const truncatedName = - fixture.tasks.python.id.name.split('.').pop() || ''; - await selectNode( - parentNodeEl[0], - truncatedName, - childNodeExecution.id.nodeId - ); - - // Wait for Details Panel to render and then for the nodeId header - const detailsPanel = await waitFor(() => - screen.getByTestId('details-panel') - ); - await waitFor(() => - expect( - getByText(detailsPanel, childNodeExecution.id.nodeId) - ) - ); - expect( - getByText(detailsPanel, fixture.tasks.python.id.name) - ).toBeInTheDocument(); - }); - }); - }); }); diff --git a/src/components/Executions/TaskExecutionDetails/TaskExecutionNodes.tsx b/src/components/Executions/TaskExecutionDetails/TaskExecutionNodes.tsx index fed47cfd8..414f182ca 100644 --- a/src/components/Executions/TaskExecutionDetails/TaskExecutionNodes.tsx +++ b/src/components/Executions/TaskExecutionDetails/TaskExecutionNodes.tsx @@ -80,6 +80,10 @@ export const TaskExecutionNodes: React.FC = ({ const renderNodeExecutionsTable = (nodeExecutions: NodeExecution[]) => ( + {/* TODO: legacy code - looks like it's never called + If code still in use NodeExecutionsTable should be wrapped by + NodeExecutionDetailsContextProvider here or in one of the parent components. + */} ); diff --git a/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx b/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx new file mode 100644 index 000000000..c340a1b84 --- /dev/null +++ b/src/components/Executions/contextProvider/NodeExecutionDetails/createExecutionArray.tsx @@ -0,0 +1,119 @@ +import { Core } from 'flyteidl'; +import { + NodeExecutionDetails, + NodeExecutionDisplayType +} from 'components/Executions/types'; +import { flattenBranchNodes } from 'components/Executions/utils'; +import { Workflow } from 'models/Workflow/types'; +import { Identifier } from 'models/Common/types'; +import { CompiledNode } from 'models/Node/types'; +import { CompiledTask } from 'models/Task/types'; +import { endNodeId, startNodeId } from 'models/Node/constants'; +import { isIdEqual, UNKNOWN_DETAILS } from './types'; + +interface NodeExecutionInfo extends NodeExecutionDetails { + parentTemplate: Identifier; +} + +export interface CurrentExecutionDetails { + executionId: Identifier; + nodes: NodeExecutionInfo[]; +} + +const isParentNode = (type: string) => + type === NodeExecutionDisplayType.Workflow; + +const getNodeDetails = ( + node: CompiledNode, + tasks: CompiledTask[] +): NodeExecutionDetails => { + if (node.taskNode) { + const templateName = node.taskNode.referenceId.name; + const task = tasks.find(t => t.template.id.name === templateName); + return { + displayId: node.id, + displayName: templateName, + displayType: + task?.template.type ?? NodeExecutionDisplayType.UnknownTask, + taskTemplate: task?.template + }; + } + + if (node.workflowNode) { + const info = + node.workflowNode.launchplanRef ?? node.workflowNode.subWorkflowRef; + return { + displayId: node.id, + displayName: info?.name ?? 'N/A', + displayType: NodeExecutionDisplayType.Workflow + }; + } + + // TODO: https://github.com/lyft/flyte/issues/655 + if (node.branchNode) { + return { + displayId: node.id, + displayName: 'branchNode', + displayType: NodeExecutionDisplayType.BranchNode + }; + } + + return UNKNOWN_DETAILS; +}; + +export function createExecutionDetails( + workflow: Workflow +): { nodes: CurrentExecutionDetails; map: Map } { + const mapNodeIdToTemplate = new Map(); + const result: CurrentExecutionDetails = { + executionId: workflow.id, + nodes: [] + }; + + if (!workflow.closure?.compiledWorkflow) { + return { nodes: result, map: mapNodeIdToTemplate }; + } + + const { + primary, + subWorkflows = [], + tasks = [] + } = workflow.closure?.compiledWorkflow; + + if (!isIdEqual(primary.template.id, workflow.id)) { + console.log('WRONG'); + } + + const allWorkflows = [primary, ...subWorkflows]; + allWorkflows.forEach(w => { + const nodes = w.template.nodes; + nodes + .map(flattenBranchNodes) + .flat() + .forEach(n => { + if (n.id === startNodeId || n.id === endNodeId) { + // skip start and end nodes + return; + } + + const details = getNodeDetails(n, tasks); + if ( + details?.displayName && + isParentNode(details?.displayType) + ) { + const info = + n.workflowNode?.launchplanRef ?? + n.workflowNode?.subWorkflowRef; + if (info) { + mapNodeIdToTemplate.set(n.id, info); + } + } + result.nodes.push({ + parentTemplate: w.template.id, + ...details + }); + }); + }); + + return { nodes: result, map: mapNodeIdToTemplate }; +} diff --git a/src/components/Executions/contextProvider/NodeExecutionDetails/getTaskThroughExecution.ts b/src/components/Executions/contextProvider/NodeExecutionDetails/getTaskThroughExecution.ts new file mode 100644 index 000000000..b0d3becfd --- /dev/null +++ b/src/components/Executions/contextProvider/NodeExecutionDetails/getTaskThroughExecution.ts @@ -0,0 +1,43 @@ +import { fetchTaskExecutionList } from 'components/Executions/taskExecutionQueries'; +import { + NodeExecutionDetails, + NodeExecutionDisplayType +} from 'components/Executions/types'; +import { fetchTaskTemplate } from 'components/Task/taskQueries'; +import { NodeExecution } from 'models/Execution/types'; +import { TaskTemplate } from 'models/Task/types'; +import { QueryClient } from 'react-query/types/core/queryClient'; + +export const getTaskThroughExecution = async ( + queryClient: QueryClient, + nodeExecution: NodeExecution +): Promise => { + const taskExecutions = await fetchTaskExecutionList( + queryClient, + nodeExecution.id + ); + + let taskTemplate: TaskTemplate | undefined = undefined; + if (taskExecutions && taskExecutions.length > 0) { + taskTemplate = await fetchTaskTemplate( + queryClient, + taskExecutions[0].id.taskId + ); + if (!taskTemplate) { + console.error( + `ERROR: Unexpected missing task template while fetching NodeExecution details: ${JSON.stringify( + taskExecutions[0].id.taskId + )}` + ); + } + } + + const taskDetails: NodeExecutionDetails = { + displayId: nodeExecution.id.nodeId, + displayName: taskExecutions?.[0]?.id.taskId.name, + displayType: taskTemplate?.type ?? NodeExecutionDisplayType.Unknown, + taskTemplate: taskTemplate + }; + + return taskDetails; +}; diff --git a/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx b/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx new file mode 100644 index 000000000..29ca60496 --- /dev/null +++ b/src/components/Executions/contextProvider/NodeExecutionDetails/index.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import { createContext, useContext, useEffect, useRef, useState } from 'react'; +import { Core } from 'flyteidl'; +import { Identifier } from 'models/Common/types'; +import { NodeExecution } from 'models/Execution/types'; +import { useQueryClient } from 'react-query'; +import { fetchWorkflow } from 'components/Workflow/workflowQueries'; +import { NodeExecutionDetails } from '../../types'; +import { isIdEqual, UNKNOWN_DETAILS } from './types'; +import { + createExecutionDetails, + CurrentExecutionDetails +} from './createExecutionArray'; +import { getTaskThroughExecution } from './getTaskThroughExecution'; + +interface NodeExecutionDetailsState { + getNodeExecutionDetails: ( + nodeExecution?: NodeExecution + ) => Promise; +} + +/** Use this Context to redefine Provider returns in storybooks */ +export const NodeExecutionDetailsContext = createContext< + NodeExecutionDetailsState +>({ + /** Default values used if ContextProvider wasn't initialized. */ + getNodeExecutionDetails: async () => { + console.error( + 'ERROR: No NodeExecutionDetailsContextProvider was found in parent components.' + ); + return UNKNOWN_DETAILS; + } +}); + +/** 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 [parentMap, setParentMap] = useState( + new Map() + ); + const [tasks, setTasks] = useState(new Map()); + + const resetState = () => { + setExecutionTree(null); + setParentMap(new Map()); + }; + + 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 workflow = await fetchWorkflow(queryClient, workflowId); + if (!workflow) { + resetState(); + return; + } + + const { nodes: tree, map } = createExecutionDetails(workflow); + if (isCurrent) { + setExecutionTree(tree); + setParentMap(map); + } + } + + fetchData(); + + // This handles the unmount case + return () => { + isCurrent = false; + resetState(); + }; + }, [queryClient, resourceType, project, domain, name, version]); + + const checkForDynamicTasks = 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.metadata?.specNodeId || nodeExecution.id.nodeId; + const parentId = nodeExecution.parentId; + + let nodeDetail = executionTree.nodes.filter( + n => n.displayId === specId + ); + if (nodeDetail.length > 1) { + // more than one result - we will try to filter by parent info + // if there is no parent_id - we are dealing with the root. + const parentTemplate = parentId + ? parentMap.get(parentId) ?? executionTree.executionId + : executionTree.executionId; + nodeDetail = nodeDetail.filter(n => + isIdEqual(n.parentTemplate, parentTemplate) + ); + } + + 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 + details = await checkForDynamicTasks(nodeExecution); + return details; + } + + return nodeDetail?.[0] ?? UNKNOWN_DETAILS; + }; + + return ( + + {props.children} + + ); +}; diff --git a/src/components/Executions/contextProvider/NodeExecutionDetails/types.ts b/src/components/Executions/contextProvider/NodeExecutionDetails/types.ts new file mode 100644 index 000000000..f9d50cda7 --- /dev/null +++ b/src/components/Executions/contextProvider/NodeExecutionDetails/types.ts @@ -0,0 +1,17 @@ +import { NodeExecutionDisplayType } from 'components/Executions/types'; +import { Core } from 'flyteidl'; + +export const UNKNOWN_DETAILS = { + displayId: 'unknownNode', + displayType: NodeExecutionDisplayType.Unknown +}; + +export function isIdEqual(lhs: Core.IIdentifier, rhs: Core.IIdentifier) { + return ( + lhs.resourceType === rhs.resourceType && + lhs.project === rhs.project && + lhs.domain === rhs.domain && + lhs.name === rhs.name && + lhs.version === rhs.version + ); +} diff --git a/src/components/Executions/nodeExecutionQueries.ts b/src/components/Executions/nodeExecutionQueries.ts index 14c34ed4e..c6ce60a20 100644 --- a/src/components/Executions/nodeExecutionQueries.ts +++ b/src/components/Executions/nodeExecutionQueries.ts @@ -273,6 +273,7 @@ async function fetchGroupsForParentNodeExecution( let scopedId: string | undefined = nodeExecution.metadata?.specNodeId; if (scopedId != undefined) { + child['parentId'] = scopedId; scopedId += `-${child.metadata?.retryGroup}-${child.metadata?.specNodeId}`; child['scopedId'] = scopedId; } else { diff --git a/src/components/Executions/types.ts b/src/components/Executions/types.ts index 3803aee4a..c9c79f46b 100644 --- a/src/components/Executions/types.ts +++ b/src/components/Executions/types.ts @@ -4,12 +4,6 @@ import { NodeExecutionMetadata, WorkflowNodeMetadata } from 'models/Execution/types'; -import { - BranchNode, - CompiledNode, - TaskNode, - WorkflowNode -} from 'models/Node/types'; import { TaskTemplate } from 'models/Task/types'; export interface ExecutionPhaseConstants { @@ -34,18 +28,6 @@ export enum NodeExecutionDisplayType { MpiTask = 'MPI Task' } -export interface CompiledTaskNode extends CompiledNode { - taskNode: TaskNode; -} - -export interface CompiledWorkflowNode extends CompiledNode { - workflowNode: WorkflowNode; -} - -export interface CompiledBranchNode extends CompiledNode { - branchNode: BranchNode; -} - export interface ParentNodeExecution extends NodeExecution { metadata: NodeExecutionMetadata & { isParentNode: true; @@ -56,10 +38,6 @@ export interface WorkflowNodeExecutionClosure extends NodeExecutionClosure { workflowNodeMetadata: WorkflowNodeMetadata; } -export interface WorkflowNodeExecution extends NodeExecution { - closure: WorkflowNodeExecutionClosure; -} - export interface NodeExecutionDetails { displayId?: string; displayName?: string; diff --git a/src/components/Executions/useNodeExecutionDetails.ts b/src/components/Executions/useNodeExecutionDetails.ts deleted file mode 100644 index 2186a9108..000000000 --- a/src/components/Executions/useNodeExecutionDetails.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { log } from 'common/log'; -import { QueryType } from 'components/data/types'; -import { fetchTaskTemplate } from 'components/Task/taskQueries'; -import { fetchWorkflow } from 'components/Workflow/workflowQueries'; -import { Identifier } from 'models/Common/types'; -import { NodeExecution } from 'models/Execution/types'; -import { CompiledNode } from 'models/Node/types'; -import { TaskTemplate } from 'models/Task/types'; -import { CompiledWorkflow, Workflow } from 'models/Workflow/types'; -import { QueryClient, useQuery, useQueryClient } from 'react-query'; -import { fetchTaskExecutionList } from './taskExecutionQueries'; -import { - CompiledBranchNode, - CompiledWorkflowNode, - NodeExecutionDetails, - NodeExecutionDisplayType, - WorkflowNodeExecution -} from './types'; -import { fetchWorkflowExecution } from './useWorkflowExecution'; -import { - getNodeExecutionSpecId, - isCompiledBranchNode, - isCompiledTaskNode, - isCompiledWorkflowNode, - isWorkflowNodeExecution, - flattenBranchNodes -} from './utils'; -function createExternalWorkflowNodeExecutionDetails( - workflow: Workflow -): NodeExecutionDetails { - return { - displayId: workflow.id.name, - displayType: NodeExecutionDisplayType.Workflow - }; -} - -function createWorkflowNodeExecutionDetails( - nodeExecution: NodeExecution, - node: CompiledWorkflowNode -): NodeExecutionDetails { - const displayType = NodeExecutionDisplayType.Workflow; - let displayId = ''; - let displayName = ''; - const { launchplanRef, subWorkflowRef } = node.workflowNode; - const identifier = (launchplanRef - ? launchplanRef - : subWorkflowRef) as Identifier; - if (!identifier) { - log.warn( - `Unexpected workflow node with no ref: ${getNodeExecutionSpecId( - nodeExecution - )}` - ); - } else { - displayId = node.id; - displayName = identifier.name; - } - - return { - displayId, - displayName, - displayType - }; -} - -// TODO: https://github.com/lyft/flyte/issues/655 -function createBranchNodeExecutionDetails( - node: CompiledBranchNode -): NodeExecutionDetails { - return { - displayId: node.id, - displayName: 'branchNode', - displayType: NodeExecutionDisplayType.BranchNode - }; -} - -function createTaskNodeExecutionDetails( - taskTemplate: TaskTemplate, - displayId: string | undefined -): NodeExecutionDetails { - return { - taskTemplate, - displayId: displayId, - displayName: taskTemplate.id.name, - displayType: taskTemplate.type - }; -} - -function createUnknownNodeExecutionDetails(): NodeExecutionDetails { - return { - displayId: '', - displayType: NodeExecutionDisplayType.Unknown - }; -} - -async function fetchExternalWorkflowNodeExecutionDetails( - queryClient: QueryClient, - nodeExecution: WorkflowNodeExecution -): Promise { - const workflowExecution = await fetchWorkflowExecution( - queryClient, - nodeExecution.closure.workflowNodeMetadata.executionId - ); - const workflow = await fetchWorkflow( - queryClient, - workflowExecution.closure.workflowId - ); - - return createExternalWorkflowNodeExecutionDetails(workflow); -} - -function findCompiledNode( - nodeId: string, - compiledWorkflows: CompiledWorkflow[] -) { - for (let i = 0; i < compiledWorkflows.length; i += 1) { - const found = compiledWorkflows[i].template.nodes - .map(flattenBranchNodes) - .flat() - .find(({ id }) => id === nodeId); - if (found) { - return found; - } - } - return undefined; -} - -function findNodeInWorkflow( - nodeId: string, - workflow: Workflow -): CompiledNode | undefined { - if (!workflow.closure?.compiledWorkflow) { - return undefined; - } - const { primary, subWorkflows = [] } = workflow.closure?.compiledWorkflow; - return findCompiledNode(nodeId, [primary, ...subWorkflows]); -} - -async function fetchTaskNodeExecutionDetails( - queryClient: QueryClient, - taskId: Identifier, - displayId: string | undefined -) { - const taskTemplate = await fetchTaskTemplate(queryClient, taskId); - if (!taskTemplate) { - throw new Error( - `Unexpected missing task template while fetching NodeExecution details: ${JSON.stringify( - taskId - )}` - ); - } - return createTaskNodeExecutionDetails(taskTemplate, displayId); -} - -async function fetchNodeExecutionDetailsFromNodeSpec( - queryClient: QueryClient, - nodeExecution: NodeExecution -): Promise { - const nodeId = getNodeExecutionSpecId(nodeExecution); - const workflowExecution = await fetchWorkflowExecution( - queryClient, - nodeExecution.id.executionId - ); - const workflow = await fetchWorkflow( - queryClient, - workflowExecution.closure.workflowId - ); - - // If the source workflow spec has a node matching this execution, we - // can parse out the node information and set our details based on that. - const compiledNode = findNodeInWorkflow(nodeId, workflow); - if (compiledNode) { - if (isCompiledTaskNode(compiledNode)) { - return fetchTaskNodeExecutionDetails( - queryClient, - compiledNode.taskNode.referenceId, - compiledNode.id - ); - } - if (isCompiledWorkflowNode(compiledNode)) { - return createWorkflowNodeExecutionDetails( - nodeExecution, - compiledNode - ); - } - if (isCompiledBranchNode(compiledNode)) { - return createBranchNodeExecutionDetails(compiledNode); - } - } - - // Fall back to attempting to locate a task execution for this node and - // subsequently fetching its task spec. - const taskExecutions = await fetchTaskExecutionList( - queryClient, - nodeExecution.id - ); - if (taskExecutions.length > 0) { - return fetchTaskNodeExecutionDetails( - queryClient, - taskExecutions[0].id.taskId, - undefined - ); - } - - return createUnknownNodeExecutionDetails(); -} - -async function doFetchNodeExecutionDetails( - queryClient: QueryClient, - nodeExecution: NodeExecution -) { - try { - if (isWorkflowNodeExecution(nodeExecution)) { - return fetchExternalWorkflowNodeExecutionDetails( - queryClient, - nodeExecution - ); - } - - // Attempt to find node information in the source workflow spec - // or via any associated TaskExecution's task spec. - return fetchNodeExecutionDetailsFromNodeSpec( - queryClient, - nodeExecution - ); - } catch (e) { - return createUnknownNodeExecutionDetails(); - } -} - -export function fetchNodeExecutionDetails( - queryClient: QueryClient, - nodeExecution: NodeExecution -) { - return queryClient.fetchQuery({ - queryKey: [QueryType.NodeExecutionDetails, nodeExecution.id], - queryFn: () => doFetchNodeExecutionDetails(queryClient, nodeExecution) - }); -} - -export function useNodeExecutionDetails(nodeExecution?: NodeExecution) { - const queryClient = useQueryClient(); - return useQuery({ - enabled: !!nodeExecution, - // Once we successfully map these details, we don't need to do it again. - staleTime: Infinity, - queryKey: [QueryType.NodeExecutionDetails, nodeExecution?.id], - queryFn: () => doFetchNodeExecutionDetails(queryClient, nodeExecution!) - }); -} diff --git a/src/components/Executions/utils.ts b/src/components/Executions/utils.ts index d7b964261..d9b665cbf 100644 --- a/src/components/Executions/utils.ts +++ b/src/components/Executions/utils.ts @@ -23,14 +23,7 @@ import { taskExecutionPhaseConstants, workflowExecutionPhaseConstants } from './constants'; -import { - CompiledBranchNode, - CompiledTaskNode, - CompiledWorkflowNode, - ExecutionPhaseConstants, - ParentNodeExecution, - WorkflowNodeExecution -} from './types'; +import { ExecutionPhaseConstants, ParentNodeExecution } from './types'; /** Given an execution phase, returns a set of constants (i.e. color, display * string) used to represent it in various UI components. @@ -44,13 +37,6 @@ export function getWorkflowExecutionPhaseConstants( ); } -/** Maps a `WorkflowExecutionPhase` value to a corresponding color string */ -export function workflowExecutionPhaseToColor( - phase: WorkflowExecutionPhase -): string { - return getWorkflowExecutionPhaseConstants(phase).badgeColor; -} - /** Given an execution phase, returns a set of constants (i.e. color, display * string) used to represent it in various UI components. */ @@ -63,11 +49,6 @@ export function getNodeExecutionPhaseConstants( ); } -/** Maps a `NodeExecutionPhase` value to a corresponding color string */ -export function nodeExecutionPhaseToColor(phase: NodeExecutionPhase): string { - return getNodeExecutionPhaseConstants(phase).badgeColor; -} - /** Given an execution phase, returns a set of constants (i.e. color, display * string) used to represent it in various UI components. */ @@ -80,11 +61,6 @@ export function getTaskExecutionPhaseConstants( ); } -/** Maps a `TaskExecutionPhase` value to a corresponding color string */ -export function taskExecutionPhaseToColor(phase: TaskExecutionPhase): string { - return getTaskExecutionPhaseConstants(phase).badgeColor; -} - /** Determines if a workflow execution can be considered finalized and will not * change state again. */ @@ -113,11 +89,6 @@ export const taskExecutionIsTerminal = (taskExecution: TaskExecution) => taskExecution.closure && terminalTaskExecutionStates.includes(taskExecution.closure.phase); -/** Returns a NodeId from a given NodeExecution */ -export function getNodeExecutionSpecId(nodeExecution: NodeExecution): string { - return nodeExecution.metadata?.specNodeId ?? nodeExecution.id.nodeId; -} - interface GetExecutionDurationMSArgs { closure: BaseExecutionClosure; isTerminal: boolean; @@ -161,30 +132,6 @@ export function isParentNode( ); } -export function isWorkflowNodeExecution( - nodeExecution: NodeExecution -): nodeExecution is WorkflowNodeExecution { - return nodeExecution.closure.workflowNodeMetadata != null; -} - -export function isCompiledTaskNode( - node: CompiledNode -): node is CompiledTaskNode { - return node.taskNode != null; -} - -export function isCompiledWorkflowNode( - node: CompiledNode -): node is CompiledWorkflowNode { - return node.workflowNode != null; -} - -export function isCompiledBranchNode( - node: CompiledNode -): node is CompiledBranchNode { - return node.branchNode != null; -} - export function flattenBranchNodes(node: CompiledNode): CompiledNode[] { const ifElse = node.branchNode?.ifElse; if (!ifElse) { diff --git a/src/mocks/data/fixtures/types.ts b/src/mocks/data/fixtures/types.ts index 92ee5f500..1c7d1fc87 100644 --- a/src/mocks/data/fixtures/types.ts +++ b/src/mocks/data/fixtures/types.ts @@ -6,6 +6,14 @@ import { import { LaunchPlan } from 'models/Launch/types'; import { Task } from 'models/Task/types'; import { Workflow } from 'models/Workflow/types'; +import { Identifier } from 'models/Common/types'; + +export const mockWorkflowId: Identifier = { + project: 'project', + domain: 'domain', + name: 'specific.workflow.name_wf', + version: '0.1' +}; /** Represents a TaskExecution and its associated children. */ export interface MockTaskExecutionData { diff --git a/src/models/Execution/types.ts b/src/models/Execution/types.ts index 6da247bd6..110c8fbbc 100644 --- a/src/models/Execution/types.ts +++ b/src/models/Execution/types.ts @@ -91,6 +91,7 @@ export interface NodeExecution extends Admin.INodeExecution { inputUri: string; closure: NodeExecutionClosure; metadata?: NodeExecutionMetadata; + parentId?: string; scopedId?: string; } diff --git a/src/models/Graph/constants.ts b/src/models/Graph/constants.ts deleted file mode 100644 index 228d2f94f..000000000 --- a/src/models/Graph/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const nodeIds = { - end: 'end-node', - start: 'start-node' -}; diff --git a/src/models/Graph/convertFlyteGraphToDAG.ts b/src/models/Graph/convertFlyteGraphToDAG.ts index ce2cda62f..1bd8678d9 100644 --- a/src/models/Graph/convertFlyteGraphToDAG.ts +++ b/src/models/Graph/convertFlyteGraphToDAG.ts @@ -2,8 +2,8 @@ import { createDebugLogger } from 'common/log'; import { createTimer } from 'common/timer'; import { cloneDeep, keyBy, values } from 'lodash'; import { identifierToString } from 'models/Common/utils'; +import { startNodeId } from 'models/Node/constants'; import { CompiledWorkflowClosure } from 'models/Workflow/types'; -import { nodeIds } from './constants'; import { DAGNode } from './types'; const log = createDebugLogger('models/Workflow'); @@ -16,7 +16,6 @@ const log = createDebugLogger('models/Workflow'); export function convertFlyteGraphToDAG( workflow: CompiledWorkflowClosure ): DAGNode[] { - const timer = createTimer(); const { @@ -75,7 +74,7 @@ export function convertFlyteGraphToDAG( // Filter out any nodes with no parents (except for the start node) const result = values(nodeMap).filter( - n => n.id === nodeIds.start || (n.parentIds && n.parentIds.length > 0) + n => n.id === startNodeId || (n.parentIds && n.parentIds.length > 0) ); log(`Compilation time: ${timer.timeStringMS}`);