- {nodes.map((node) => {
- const templateName = getNodeTemplateName(node);
- const nodeLevel = node?.level ?? 0;
- return (
-
-
- {node.nodes?.length ? (
-
onToggle(node.id, node.scopedId, nodeLevel)}
- />
- ) : (
-
- )}
-
+ return (
+
+ {nodes.map((node) => {
+ const templateName = getNodeTemplateName(node);
+ const nodeLevel = node?.level ?? 0;
+ return (
+
+
+
+ {node.nodes?.length ? (
+
onToggle(node.id, node.scopedId, nodeLevel)}
+ />
+ ) : (
+
+ )}
+
-
-
-
- {templateName}
-
+
+
+
+ {templateName}
+
+
+
+ {onAction && (
+
+ onAction(node.id)}>
+
+
+
+ )}
-
- );
- })}
-
- );
-});
+ );
+ })}
+
+ );
+ },
+);
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx
index b94e31dcc..08f4b98c6 100644
--- a/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx
+++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/strings.tsx
@@ -3,6 +3,7 @@ import { createLocalizedString } from '@flyteconsole/locale';
const str = {
rerun: 'RERUN',
flyteDeck: 'Flyte Deck',
+ resume: 'Resume',
};
export { patternKey } from '@flyteconsole/locale';
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx
deleted file mode 100644
index 02570b12a..000000000
--- a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetails.test.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { render, waitFor } from '@testing-library/react';
-import { cacheStatusMessages, viewSourceExecutionString } from 'components/Executions/constants';
-import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails';
-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 { ResourceType } from 'models/Common/types';
-import { CatalogCacheStatus } from 'models/Execution/enums';
-import { NodeExecution, TaskNodeMetadata } from 'models/Execution/types';
-import { mockExecution as mockTaskExecution } from 'models/Execution/__mocks__/mockTaskExecutionsData';
-import * as React from 'react';
-import { QueryClient, QueryClientProvider } from 'react-query';
-import { MemoryRouter } from 'react-router';
-import { Routes } from 'routes/routes';
-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;
- let queryClient: QueryClient;
-
- beforeEach(() => {
- fixture = basicPythonWorkflow.generate();
- execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data;
- insertFixture(mockServer, fixture);
- fetchWorkflow.mockImplementation(() => Promise.resolve(fixture.workflows.top));
- queryClient = createTestQueryClient();
- });
-
- const renderComponent = () =>
- render(
-
-
-
-
-
-
- ,
- );
-
- it('renders name for task nodes', async () => {
- const { name } = fixture.tasks.python.id;
- const { getByText } = renderComponent();
- await waitFor(() => expect(getByText(name)));
- });
-
- describe('with cache information', () => {
- let taskNodeMetadata: TaskNodeMetadata;
- beforeEach(() => {
- taskNodeMetadata = {
- cacheStatus: CatalogCacheStatus.CACHE_MISS,
- catalogKey: {
- datasetId: makeIdentifier({
- resourceType: ResourceType.DATASET,
- }),
- sourceTaskExecution: { ...mockTaskExecution.id },
- },
- };
- execution.closure.taskNodeMetadata = taskNodeMetadata;
- mockServer.insertNodeExecution(execution);
- });
-
- [
- CatalogCacheStatus.CACHE_DISABLED,
- CatalogCacheStatus.CACHE_HIT,
- CatalogCacheStatus.CACHE_LOOKUP_FAILURE,
- CatalogCacheStatus.CACHE_MISS,
- CatalogCacheStatus.CACHE_POPULATED,
- CatalogCacheStatus.CACHE_PUT_FAILURE,
- ].forEach((cacheStatusValue) =>
- it(`renders correct status for ${CatalogCacheStatus[cacheStatusValue]}`, async () => {
- taskNodeMetadata.cacheStatus = cacheStatusValue;
- mockServer.insertNodeExecution(execution);
- const { getByText } = renderComponent();
- await waitFor(() => expect(getByText(cacheStatusMessages[cacheStatusValue])));
- }),
- );
-
- it('renders source execution link for cache hits', async () => {
- taskNodeMetadata.cacheStatus = CatalogCacheStatus.CACHE_HIT;
- const sourceWorkflowExecutionId =
- taskNodeMetadata.catalogKey!.sourceTaskExecution.nodeExecutionId.executionId;
- const { getByText } = renderComponent();
- const linkEl = await waitFor(() => getByText(viewSourceExecutionString));
- expect(linkEl.getAttribute('href')).toBe(
- Routes.ExecutionDetails.makeUrl(sourceWorkflowExecutionId),
- );
- });
- });
-});
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx
new file mode 100644
index 000000000..4dc9eeec5
--- /dev/null
+++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/NodeExecutionDetailsPanelContent.test.tsx
@@ -0,0 +1,50 @@
+import { render, waitFor } from '@testing-library/react';
+import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails';
+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 { TaskExecutionPhase } from 'models/Execution/enums';
+import { NodeExecution } from 'models/Execution/types';
+import * as React from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { MemoryRouter } from 'react-router';
+import { createTestQueryClient } from 'test/utils';
+import { NodeExecutionDetailsPanelContent } from '../NodeExecutionDetailsPanelContent';
+
+jest.mock('components/Workflow/workflowQueries');
+const { fetchWorkflow } = require('components/Workflow/workflowQueries');
+
+describe('NodeExecutionDetailsPanelContent', () => {
+ let fixture: ReturnType;
+ let execution: NodeExecution;
+ let queryClient: QueryClient;
+
+ beforeEach(() => {
+ fixture = basicPythonWorkflow.generate();
+ execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data;
+ insertFixture(mockServer, fixture);
+ fetchWorkflow.mockImplementation(() => Promise.resolve(fixture.workflows.top));
+ queryClient = createTestQueryClient();
+ });
+
+ const renderComponent = () =>
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ it('renders name for task nodes', async () => {
+ const { name } = fixture.tasks.python.id;
+ const { getByText } = renderComponent();
+ await waitFor(() => expect(getByText(name)).toBeInTheDocument());
+ });
+});
diff --git a/packages/zapp/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx
new file mode 100644
index 000000000..51da24a06
--- /dev/null
+++ b/packages/zapp/console/src/components/Executions/ExecutionDetails/test/TaskNames.test.tsx
@@ -0,0 +1,55 @@
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import { dTypes } from 'models/Graph/types';
+import { TaskNames } from '../Timeline/TaskNames';
+
+const onToggle = jest.fn();
+const onAction = jest.fn();
+
+const node1 = {
+ id: 'n1',
+ scopedId: 'n1',
+ type: dTypes.staticNode,
+ name: 'node1',
+ nodes: [],
+ edges: [],
+};
+
+const node2 = {
+ id: 'n2',
+ scopedId: 'n2',
+ type: dTypes.gateNode,
+ name: 'node2',
+ nodes: [],
+ edges: [],
+};
+
+describe('ExecutionDetails > Timeline > TaskNames', () => {
+ const renderComponent = (props) => render();
+
+ it('should render task names list', () => {
+ const nodes = [node1, node2];
+ const { getAllByTestId } = renderComponent({ nodes, onToggle });
+ expect(getAllByTestId('task-name-item').length).toEqual(nodes.length);
+ });
+
+ it('should render task names list with resume buttons if onAction prop is passed', () => {
+ const nodes = [node1, node2];
+ const { getAllByTestId, getAllByTitle } = renderComponent({ nodes, onToggle, onAction });
+ expect(getAllByTestId('task-name-item').length).toEqual(nodes.length);
+ expect(getAllByTitle('Resume').length).toEqual(nodes.length);
+ });
+
+ it('should render task names list with expanders if nodes contain nested nodes list', () => {
+ const nestedNodes = [
+ { id: 't1', scopedId: 'n1', type: dTypes.task, name: 'task1', nodes: [], edges: [] },
+ ];
+ const nodes = [
+ { ...node1, nodes: nestedNodes },
+ { ...node2, nodes: nestedNodes },
+ ];
+ const { getAllByTestId, getAllByTitle } = renderComponent({ nodes, onToggle });
+ expect(getAllByTestId('task-name-item').length).toEqual(nodes.length);
+ expect(getAllByTitle('Expand row').length).toEqual(nodes.length);
+ });
+});
diff --git a/packages/zapp/console/src/components/Executions/NodeExecutionCacheStatus.tsx b/packages/zapp/console/src/components/Executions/NodeExecutionCacheStatus.tsx
index a16960dec..11171b62e 100644
--- a/packages/zapp/console/src/components/Executions/NodeExecutionCacheStatus.tsx
+++ b/packages/zapp/console/src/components/Executions/NodeExecutionCacheStatus.tsx
@@ -1,68 +1,11 @@
-import { SvgIconProps, Tooltip, Typography } from '@material-ui/core';
-import { makeStyles, Theme } from '@material-ui/core/styles';
-import CachedOutlined from '@material-ui/icons/CachedOutlined';
-import ErrorOutlined from '@material-ui/icons/ErrorOutlined';
-import InfoOutlined from '@material-ui/icons/InfoOutlined';
-import classnames from 'classnames';
-import { assertNever } from 'common/utils';
-import { PublishedWithChangesOutlined } from 'components/common/PublishedWithChanges';
-import { useCommonStyles } from 'components/common/styles';
import { NodeExecutionDetails } from 'components/Executions/types';
import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails';
import { CatalogCacheStatus } from 'models/Execution/enums';
-import { NodeExecution, TaskExecutionIdentifier } from 'models/Execution/types';
-import { MapCacheIcon } from '@flyteconsole/ui-atoms';
+import { NodeExecution } from 'models/Execution/types';
import * as React from 'react';
import { isMapTaskType } from 'models/Task/utils';
-import { Link as RouterLink } from 'react-router-dom';
-import { Routes } from 'routes/routes';
-import {
- cacheStatusMessages,
- unknownCacheStatusString,
- viewSourceExecutionString,
-} from './constants';
-
-const useStyles = makeStyles((theme: Theme) => ({
- cacheStatus: {
- alignItems: 'center',
- display: 'flex',
- marginTop: theme.spacing(1),
- },
- sourceExecutionLink: {
- fontWeight: 'normal',
- },
-}));
-
-/** Renders the appropriate icon for a given CatalogCacheStatus */
-export const NodeExecutionCacheStatusIcon: React.FC<
- SvgIconProps & {
- status: CatalogCacheStatus;
- }
-> = React.forwardRef(({ status, ...props }, ref) => {
- switch (status) {
- case CatalogCacheStatus.CACHE_DISABLED:
- case CatalogCacheStatus.CACHE_MISS: {
- return ;
- }
- case CatalogCacheStatus.CACHE_HIT: {
- return ;
- }
- case CatalogCacheStatus.CACHE_POPULATED: {
- return ;
- }
- case CatalogCacheStatus.CACHE_LOOKUP_FAILURE:
- case CatalogCacheStatus.CACHE_PUT_FAILURE: {
- return ;
- }
- case CatalogCacheStatus.MAP_CACHE: {
- return ;
- }
- default: {
- assertNever(status);
- return null;
- }
- }
-});
+import { useEffect, useState } from 'react';
+import { CacheStatus } from './CacheStatus';
export interface NodeExecutionCacheStatusProps {
execution: NodeExecution;
@@ -85,9 +28,9 @@ export const NodeExecutionCacheStatus: React.FC =
}) => {
const taskNodeMetadata = execution.closure?.taskNodeMetadata;
const detailsContext = useNodeExecutionContext();
- const [nodeDetails, setNodeDetails] = React.useState();
+ const [nodeDetails, setNodeDetails] = useState();
- React.useEffect(() => {
+ useEffect(() => {
let isCurrent = true;
detailsContext.getNodeExecutionDetails(execution).then((res) => {
if (isCurrent) {
@@ -110,74 +53,11 @@ export const NodeExecutionCacheStatus: React.FC =
return null;
}
- const sourceTaskExecution = taskNodeMetadata.catalogKey?.sourceTaskExecution;
-
return (
);
};
-
-export interface CacheStatusProps {
- cacheStatus: CatalogCacheStatus | null | undefined;
- /** `normal` will render an icon with description message beside it
- * `iconOnly` will render just the icon with the description as a tooltip
- */
- variant?: 'normal' | 'iconOnly';
- sourceTaskExecution?: TaskExecutionIdentifier;
- iconStyles?: React.CSSProperties;
-}
-
-export const CacheStatus: React.FC = ({
- cacheStatus,
- sourceTaskExecution,
- variant = 'normal',
- iconStyles,
-}) => {
- const commonStyles = useCommonStyles();
- const styles = useStyles();
-
- if (cacheStatus == null) {
- return null;
- }
-
- const message = cacheStatusMessages[cacheStatus] || unknownCacheStatusString;
-
- const sourceExecutionId = sourceTaskExecution;
- const sourceExecutionLink = sourceExecutionId ? (
-
- {viewSourceExecutionString}
-
- ) : null;
-
- return variant === 'iconOnly' ? (
-
-
-
- ) : (
-
-
-
- {message}
-
- {sourceExecutionLink}
-
- );
-};
diff --git a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx
index 430f82704..32f7fcb0c 100644
--- a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx
+++ b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionActions.tsx
@@ -2,6 +2,7 @@ import { IconButton, Tooltip } from '@material-ui/core';
import { NodeExecution } from 'models/Execution/types';
import * as React from 'react';
import InputsAndOutputsIcon from '@material-ui/icons/Tv';
+import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';
import { RerunIcon } from '@flyteconsole/ui-atoms';
import { Identifier, ResourceIdentifier } from 'models/Common/types';
import { LaunchFormDialog } from 'components/Launch/LaunchForm/LaunchFormDialog';
@@ -9,38 +10,49 @@ import { getTask } from 'models/Task/api';
import { useNodeExecutionData } from 'components/hooks/useNodeExecution';
import { TaskInitialLaunchParameters } from 'components/Launch/LaunchForm/types';
import { literalsToLiteralValueMap } from 'components/Launch/LaunchForm/utils';
+import { useEffect, useState } from 'react';
+import { NodeExecutionPhase } from 'models/Execution/enums';
import { NodeExecutionsTableState } from './types';
import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails';
import { NodeExecutionDetails } from '../types';
import t from './strings';
+import { getNodeFrontendPhase, isNodeGateNode } from '../utils';
interface NodeExecutionActionsProps {
execution: NodeExecution;
state: NodeExecutionsTableState;
}
-export const NodeExecutionActions = (props: NodeExecutionActionsProps): JSX.Element => {
- const { execution, state } = props;
+export const NodeExecutionActions = ({
+ execution,
+ state,
+}: NodeExecutionActionsProps): JSX.Element => {
+ const { compiledWorkflowClosure, getNodeExecutionDetails } = useNodeExecutionContext();
- const detailsContext = useNodeExecutionContext();
- const [showLaunchForm, setShowLaunchForm] = React.useState(false);
- const [nodeExecutionDetails, setNodeExecutionDetails] = React.useState<
+ const [showLaunchForm, setShowLaunchForm] = useState(false);
+ const [nodeExecutionDetails, setNodeExecutionDetails] = useState<
NodeExecutionDetails | undefined
- >();
- const [initialParameters, setInitialParameters] = React.useState<
+ >(undefined);
+ const [initialParameters, setInitialParameters] = useState<
TaskInitialLaunchParameters | undefined
>(undefined);
const executionData = useNodeExecutionData(execution.id);
const id = nodeExecutionDetails?.taskTemplate?.id;
- React.useEffect(() => {
- detailsContext.getNodeExecutionDetails(execution).then((res) => {
+ const isGateNode = isNodeGateNode(
+ compiledWorkflowClosure?.primary.template.nodes ?? [],
+ execution.id,
+ );
+ const phase = getNodeFrontendPhase(execution.closure.phase, isGateNode);
+
+ useEffect(() => {
+ getNodeExecutionDetails(execution).then((res) => {
setNodeExecutionDetails(res);
});
});
- React.useEffect(() => {
+ useEffect(() => {
if (!id) {
return;
}
@@ -73,6 +85,10 @@ export const NodeExecutionActions = (props: NodeExecutionActionsProps): JSX.Elem
setShowLaunchForm(true);
};
+ const resumeAction = () => {
+ // TODO https://github.com/flyteorg/flyteconsole/issues/587 Launch form for node id
+ };
+
const renderRerunAction = () => {
if (!id || !initialParameters) {
return <>>;
@@ -97,6 +113,13 @@ export const NodeExecutionActions = (props: NodeExecutionActionsProps): JSX.Elem
return (
+ {phase === NodeExecutionPhase.PAUSED && (
+
+
+
+
+
+ )}
diff --git a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionRow.tsx b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionRow.tsx
index e988c0136..cdc81bd4f 100644
--- a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionRow.tsx
+++ b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionRow.tsx
@@ -6,6 +6,7 @@ import { useTheme } from 'components/Theme/useTheme';
import { isEqual } from 'lodash';
import { NodeExecution } from 'models/Execution/types';
import * as React from 'react';
+import { useContext, useState } from 'react';
import { NodeExecutionPhase } from 'models/Execution/enums';
import { NodeExecutionsRequestConfigContext } from '../contexts';
import { useChildNodeExecutionGroupsQuery } from '../nodeExecutionQueries';
@@ -56,10 +57,10 @@ export const NodeExecutionRow: React.FC = ({
style,
}) => {
const theme = useTheme();
- const { columns, state } = React.useContext(NodeExecutionsTableContext);
- const requestConfig = React.useContext(NodeExecutionsRequestConfigContext);
+ const { columns, state } = useContext(NodeExecutionsTableContext);
+ const requestConfig = useContext(NodeExecutionsRequestConfigContext);
- const [expanded, setExpanded] = React.useState(false);
+ const [expanded, setExpanded] = useState(false);
const toggleExpanded = () => {
setExpanded(!expanded);
};
diff --git a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionsTable.tsx b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionsTable.tsx
index 6e69a97d3..8c0cf1268 100644
--- a/packages/zapp/console/src/components/Executions/Tables/NodeExecutionsTable.tsx
+++ b/packages/zapp/console/src/components/Executions/Tables/NodeExecutionsTable.tsx
@@ -16,6 +16,7 @@ import { NodeExecutionRow } from './NodeExecutionRow';
import { NoExecutionsContent } from './NoExecutionsContent';
import { useColumnStyles, useExecutionTableStyles } from './styles';
import { NodeExecutionsByIdContext } from '../contexts';
+import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails';
export interface NodeExecutionsTableProps {
setSelectedExecution: (execution: NodeExecutionIdentifier | null) => void;
@@ -41,6 +42,7 @@ export const NodeExecutionsTable: React.FC = ({
const commonStyles = useCommonStyles();
const tableStyles = useExecutionTableStyles();
const nodeExecutionsById = useContext(NodeExecutionsByIdContext);
+ const { compiledWorkflowClosure } = useNodeExecutionContext();
useEffect(() => {
if (nodeExecutionsById) {
@@ -81,7 +83,10 @@ export const NodeExecutionsTable: React.FC = ({
const columnStyles = useColumnStyles();
// Memoizing columns so they won't be re-generated unless the styles change
- const columns = useMemo(() => generateColumns(columnStyles), [columnStyles]);
+ const columns = useMemo(
+ () => generateColumns(columnStyles, compiledWorkflowClosure?.primary.template.nodes ?? []),
+ [columnStyles],
+ );
const tableContext = useMemo(
() => ({ columns, state: { selectedExecution, setSelectedExecution } }),
[columns, selectedExecution, setSelectedExecution],
diff --git a/packages/zapp/console/src/components/Executions/Tables/constants.ts b/packages/zapp/console/src/components/Executions/Tables/constants.ts
index 8e7238444..e8e3c6159 100644
--- a/packages/zapp/console/src/components/Executions/Tables/constants.ts
+++ b/packages/zapp/console/src/components/Executions/Tables/constants.ts
@@ -9,7 +9,7 @@ export const workflowExecutionsTableColumnWidths = {
export const nodeExecutionsTableColumnWidths = {
duration: 100,
- logs: 100,
+ logs: 138,
type: 144,
nodeId: 144,
name: 380,
diff --git a/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx b/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx
index 6508c033e..b85ce7a2b 100644
--- a/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx
+++ b/packages/zapp/console/src/components/Executions/Tables/nodeExecutionColumns.tsx
@@ -3,17 +3,19 @@ import { formatDateLocalTimezone, formatDateUTC, millisecondsToHMS } from 'commo
import { timestampToDate } from 'common/utils';
import { useCommonStyles } from 'components/common/styles';
import { isEqual } from 'lodash';
-import { NodeExecutionPhase } from 'models/Execution/enums';
import * as React from 'react';
import { useEffect, useState } from 'react';
+import { CompiledNode } from 'models/Node/types';
+import { NodeExecutionPhase } from 'models/Execution/enums';
import { useNodeExecutionContext } from '../contextProvider/NodeExecutionDetails';
import { ExecutionStatusBadge } from '../ExecutionStatusBadge';
import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus';
-import { getNodeExecutionTimingMS } from '../utils';
+import { getNodeExecutionTimingMS, getNodeFrontendPhase, isNodeGateNode } from '../utils';
import { NodeExecutionActions } from './NodeExecutionActions';
import { SelectNodeExecutionLink } from './SelectNodeExecutionLink';
import { useColumnStyles } from './styles';
import { NodeExecutionCellRendererData, NodeExecutionColumnDefinition } from './types';
+import t from '../strings';
const ExecutionName: React.FC = ({ execution, state }) => {
const detailsContext = useNodeExecutionContext();
@@ -110,39 +112,45 @@ const DisplayType: React.FC = ({ execution }) =>
export function generateColumns(
styles: ReturnType,
+ nodes: CompiledNode[],
): NodeExecutionColumnDefinition[] {
return [
{
cellRenderer: (props) => ,
className: styles.columnName,
key: 'name',
- label: 'task name',
+ label: t('nameLabel'),
},
{
cellRenderer: (props) => ,
className: styles.columnNodeId,
key: 'nodeId',
- label: 'node id',
+ label: t('nodeIdLabel'),
},
{
cellRenderer: (props) => ,
className: styles.columnType,
key: 'type',
- label: 'type',
+ label: t('typeLabel'),
},
{
- cellRenderer: ({ execution }) => (
- <>
-
-
- >
- ),
+ cellRenderer: ({ execution }) => {
+ const isGateNode = isNodeGateNode(nodes, execution.id);
+ const phase = getNodeFrontendPhase(
+ execution.closure?.phase ?? NodeExecutionPhase.UNDEFINED,
+ isGateNode,
+ );
+
+ return (
+ <>
+
+
+ >
+ );
+ },
className: styles.columnStatus,
key: 'phase',
- label: 'status',
+ label: t('phaseLabel'),
},
{
cellRenderer: ({ execution: { closure } }) => {
@@ -162,7 +170,7 @@ export function generateColumns(
},
className: styles.columnStartedAt,
key: 'startedAt',
- label: 'start time',
+ label: t('startedAtLabel'),
},
{
cellRenderer: ({ execution }) => {
@@ -181,10 +189,10 @@ export function generateColumns(
label: () => (
<>
- duration
+ {t('durationLabel')}
- Queued Time
+ {t('queuedTimeLabel')}
>
),
diff --git a/packages/zapp/console/src/components/Executions/Tables/strings.tsx b/packages/zapp/console/src/components/Executions/Tables/strings.tsx
index 402c312c7..c540a5e85 100644
--- a/packages/zapp/console/src/components/Executions/Tables/strings.tsx
+++ b/packages/zapp/console/src/components/Executions/Tables/strings.tsx
@@ -1,8 +1,16 @@
import { createLocalizedString } from '@flyteconsole/locale';
const str = {
- inputsAndOutputsTooltip: 'View Inputs & Outpus',
+ durationLabel: 'duration',
+ inputsAndOutputsTooltip: 'View Inputs & Outputs',
+ nameLabel: 'task name',
+ nodeIdLabel: 'node id',
+ phaseLabel: 'status',
+ queuedTimeLabel: 'Queued Time',
rerunTooltip: 'Rerun',
+ resumeTooltip: 'Resume',
+ startedAtLabel: 'start time',
+ typeLabel: 'type',
};
export { patternKey } from '@flyteconsole/locale';
diff --git a/packages/zapp/console/src/components/Executions/Tables/test/NodeExecutionActions.test.tsx b/packages/zapp/console/src/components/Executions/Tables/test/NodeExecutionActions.test.tsx
new file mode 100644
index 000000000..eb1e68999
--- /dev/null
+++ b/packages/zapp/console/src/components/Executions/Tables/test/NodeExecutionActions.test.tsx
@@ -0,0 +1,58 @@
+import * as React from 'react';
+import { render, waitFor } from '@testing-library/react';
+import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails';
+import { mockWorkflowId } from 'mocks/data/fixtures/types';
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { createTestQueryClient } from 'test/utils';
+import { insertFixture } from 'mocks/data/insertFixture';
+import { mockServer } from 'mocks/server';
+import { basicPythonWorkflow } from 'mocks/data/fixtures/basicPythonWorkflow';
+import { NodeExecution } from 'models/Execution/types';
+import { NodeExecutionActions } from '../NodeExecutionActions';
+
+jest.mock('components/Workflow/workflowQueries');
+const { fetchWorkflow } = require('components/Workflow/workflowQueries');
+
+const state = { selectedExecution: null, setSelectedExeccution: jest.fn() };
+
+describe('Executions > Tables > NodeExecutionActions', () => {
+ let queryClient: QueryClient;
+ let fixture: ReturnType;
+ let execution: NodeExecution;
+
+ beforeEach(() => {
+ fixture = basicPythonWorkflow.generate();
+ execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data;
+ queryClient = createTestQueryClient();
+ insertFixture(mockServer, fixture);
+ fetchWorkflow.mockImplementation(() => Promise.resolve(fixture.workflows.top));
+ });
+
+ const renderComponent = (props) =>
+ render(
+
+
+
+
+ ,
+ );
+
+ it('should render rerun action, if id can be determined', async () => {
+ const { queryByTitle } = renderComponent({ execution, state });
+ await waitFor(() => queryByTitle('Rerun'));
+
+ expect(queryByTitle('View Inputs & Outputs')).toBeInTheDocument();
+ expect(queryByTitle('Rerun')).toBeInTheDocument();
+ expect(queryByTitle('Resume')).not.toBeInTheDocument();
+ });
+
+ it('should render resume action, if the status is PAUSED', async () => {
+ const mockExecution = { ...execution, closure: { phase: 100 } };
+ const { queryByTitle } = renderComponent({ execution: mockExecution, state });
+ await waitFor(() => queryByTitle('Resume'));
+
+ expect(queryByTitle('View Inputs & Outputs')).toBeInTheDocument();
+ expect(queryByTitle('Rerun')).toBeInTheDocument();
+ expect(queryByTitle('Resume')).toBeInTheDocument();
+ });
+});
diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts
index 1990f4890..18bfacd83 100644
--- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts
+++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/test/utils.spec.ts
@@ -1,4 +1,4 @@
-import { getTaskLogName, getTaskIndex } from 'components/Executions/TaskExecutionsList/utils';
+import { getTaskLogName } from 'components/Executions/TaskExecutionsList/utils';
import { Event } from 'flyteidl';
import { TaskExecutionPhase } from 'models/Execution/enums';
import { obj } from 'test/utils';
@@ -101,19 +101,6 @@ describe('getTaskRetryAttemptsForIndex', () => {
});
});
-describe('getTaskIndex', () => {
- it('should return index if selected log has a match in externalResources list', () => {
- const index = 3;
- const log = getMockMapTaskLogItem(TaskExecutionPhase.SUCCEEDED, true, index, 1).logs?.[0];
-
- // TS check
- if (log) {
- const result1 = getTaskIndex(MockMapTaskExecution, log);
- expect(result1).toStrictEqual(index);
- }
- });
-});
-
describe('getTaskLogName', () => {
it('should return correct names', () => {
const taskName1 = 'task_name_1';
diff --git a/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts b/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts
index 7beb824ee..e10662bd6 100644
--- a/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts
+++ b/packages/zapp/console/src/components/Executions/TaskExecutionsList/utils.ts
@@ -1,6 +1,6 @@
import { ExternalResource, LogsByPhase, TaskExecution } from 'models/Execution/types';
import { leftPaddedNumber } from 'common/formatters';
-import { Core, Event } from 'flyteidl';
+import { Event } from 'flyteidl';
import { TaskExecutionPhase } from 'models/Execution/enums';
/** Generates a unique name for a task execution, suitable for display in a
@@ -92,27 +92,6 @@ export const getTaskRetryAtemptsForIndex = (
return filtered;
};
-export function getTaskIndex(
- taskExecution: TaskExecution,
- selectedLog: Core.ITaskLog,
-): number | null {
- const externalResources = taskExecution.closure.metadata?.externalResources ?? [];
- for (const item of externalResources) {
- const logs = item.logs ?? [];
- for (const log of logs) {
- if (log.uri) {
- if (log.name === selectedLog.name && log.uri === selectedLog.uri) {
- return item.index ?? 0;
- }
- } else if (log.name === selectedLog.name) {
- return item.index ?? 0;
- }
- }
- }
-
- return null;
-}
-
export function getTaskLogName(taskName: string, taskLogName: string): string {
const lastDotIndex = taskName.lastIndexOf('.');
const prefix = lastDotIndex !== -1 ? taskName.slice(lastDotIndex + 1) : taskName;
diff --git a/packages/zapp/console/src/components/Executions/constants.ts b/packages/zapp/console/src/components/Executions/constants.ts
index 2b2a88f35..ded16e059 100644
--- a/packages/zapp/console/src/components/Executions/constants.ts
+++ b/packages/zapp/console/src/components/Executions/constants.ts
@@ -1,4 +1,5 @@
import {
+ graphStatusColors,
negativeTextColor,
positiveTextColor,
secondaryTextColor,
@@ -22,53 +23,63 @@ export const workflowExecutionPhaseConstants: {
[key in WorkflowExecutionPhase]: ExecutionPhaseConstants;
} = {
[WorkflowExecutionPhase.ABORTED]: {
- badgeColor: statusColors.SKIPPED,
text: t('aborted'),
+ badgeColor: statusColors.SKIPPED,
+ nodeColor: graphStatusColors.ABORTED,
textColor: negativeTextColor,
},
[WorkflowExecutionPhase.ABORTING]: {
- badgeColor: statusColors.SKIPPED,
text: t('aborting'),
+ badgeColor: statusColors.SKIPPED,
+ nodeColor: graphStatusColors.ABORTED,
textColor: negativeTextColor,
},
[WorkflowExecutionPhase.FAILING]: {
- badgeColor: statusColors.FAILURE,
text: t('failing'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILING,
textColor: negativeTextColor,
},
[WorkflowExecutionPhase.FAILED]: {
- badgeColor: statusColors.FAILURE,
text: t('failed'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILED,
textColor: negativeTextColor,
},
[WorkflowExecutionPhase.QUEUED]: {
- badgeColor: statusColors.QUEUED,
text: t('queued'),
+ badgeColor: statusColors.QUEUED,
+ nodeColor: graphStatusColors.QUEUED,
textColor: secondaryTextColor,
},
[WorkflowExecutionPhase.RUNNING]: {
- badgeColor: statusColors.RUNNING,
text: t('running'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.RUNNING,
textColor: secondaryTextColor,
},
[WorkflowExecutionPhase.SUCCEEDED]: {
- badgeColor: statusColors.SUCCESS,
text: t('succeeded'),
+ badgeColor: statusColors.SUCCESS,
+ nodeColor: graphStatusColors.SUCCEEDED,
textColor: positiveTextColor,
},
[WorkflowExecutionPhase.SUCCEEDING]: {
- badgeColor: statusColors.SUCCESS,
text: t('succeeding'),
+ badgeColor: statusColors.SUCCESS,
+ nodeColor: graphStatusColors.SUCCEEDED,
textColor: positiveTextColor,
},
[WorkflowExecutionPhase.TIMED_OUT]: {
- badgeColor: statusColors.FAILURE,
text: t('timedOut'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILED,
textColor: negativeTextColor,
},
[WorkflowExecutionPhase.UNDEFINED]: {
- badgeColor: statusColors.UNKNOWN,
text: t('unknown'),
+ badgeColor: statusColors.UNKNOWN,
+ nodeColor: graphStatusColors.UNDEFINED,
textColor: secondaryTextColor,
},
};
@@ -78,58 +89,75 @@ export const nodeExecutionPhaseConstants: {
[key in NodeExecutionPhase]: ExecutionPhaseConstants;
} = {
[NodeExecutionPhase.ABORTED]: {
- badgeColor: statusColors.FAILURE,
text: t('aborted'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.ABORTED,
textColor: negativeTextColor,
},
[NodeExecutionPhase.FAILING]: {
- badgeColor: statusColors.FAILURE,
text: t('failing'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILING,
textColor: negativeTextColor,
},
[NodeExecutionPhase.FAILED]: {
- badgeColor: statusColors.FAILURE,
text: t('failed'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILED,
textColor: negativeTextColor,
},
[NodeExecutionPhase.QUEUED]: {
- badgeColor: statusColors.RUNNING,
text: t('queued'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.QUEUED,
textColor: secondaryTextColor,
},
[NodeExecutionPhase.RUNNING]: {
- badgeColor: statusColors.RUNNING,
text: t('running'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.RUNNING,
textColor: secondaryTextColor,
},
[NodeExecutionPhase.DYNAMIC_RUNNING]: {
- badgeColor: statusColors.RUNNING,
text: t('running'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.RUNNING,
textColor: secondaryTextColor,
},
[NodeExecutionPhase.SUCCEEDED]: {
- badgeColor: statusColors.SUCCESS,
text: t('succeeded'),
+ badgeColor: statusColors.SUCCESS,
+ nodeColor: graphStatusColors.SUCCEEDED,
textColor: positiveTextColor,
},
[NodeExecutionPhase.TIMED_OUT]: {
- badgeColor: statusColors.FAILURE,
text: t('timedOut'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILED,
textColor: negativeTextColor,
},
[NodeExecutionPhase.SKIPPED]: {
- badgeColor: statusColors.UNKNOWN,
text: t('skipped'),
+ badgeColor: statusColors.UNKNOWN,
+ nodeColor: graphStatusColors.UNDEFINED,
textColor: secondaryTextColor,
},
[NodeExecutionPhase.RECOVERED]: {
- badgeColor: statusColors.SUCCESS,
text: t('recovered'),
+ badgeColor: statusColors.SUCCESS,
+ nodeColor: graphStatusColors.SUCCEEDED,
textColor: positiveTextColor,
},
+ [NodeExecutionPhase.PAUSED]: {
+ text: t('paused'),
+ badgeColor: statusColors.PAUSED,
+ nodeColor: graphStatusColors.PAUSED,
+ textColor: secondaryTextColor,
+ },
[NodeExecutionPhase.UNDEFINED]: {
- badgeColor: statusColors.UNKNOWN,
text: t('unknown'),
+ badgeColor: statusColors.UNKNOWN,
+ nodeColor: graphStatusColors.UNDEFINED,
textColor: secondaryTextColor,
},
};
@@ -139,43 +167,51 @@ export const taskExecutionPhaseConstants: {
[key in TaskExecutionPhase]: ExecutionPhaseConstants;
} = {
[TaskExecutionPhase.ABORTED]: {
- badgeColor: statusColors.FAILURE,
text: t('aborted'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.ABORTED,
textColor: negativeTextColor,
},
[TaskExecutionPhase.FAILED]: {
- badgeColor: statusColors.FAILURE,
text: t('failed'),
+ badgeColor: statusColors.FAILURE,
+ nodeColor: graphStatusColors.FAILED,
textColor: negativeTextColor,
},
[TaskExecutionPhase.WAITING_FOR_RESOURCES]: {
- badgeColor: statusColors.RUNNING,
text: t('waiting'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.RUNNING,
textColor: secondaryTextColor,
},
[TaskExecutionPhase.QUEUED]: {
- badgeColor: statusColors.RUNNING,
text: t('queued'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.QUEUED,
textColor: secondaryTextColor,
},
[TaskExecutionPhase.INITIALIZING]: {
- badgeColor: statusColors.RUNNING,
text: t('initializing'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.RUNNING,
textColor: secondaryTextColor,
},
[TaskExecutionPhase.RUNNING]: {
- badgeColor: statusColors.RUNNING,
text: t('running'),
+ badgeColor: statusColors.RUNNING,
+ nodeColor: graphStatusColors.RUNNING,
textColor: secondaryTextColor,
},
[TaskExecutionPhase.SUCCEEDED]: {
- badgeColor: statusColors.SUCCESS,
text: t('succeeded'),
+ badgeColor: statusColors.SUCCESS,
+ nodeColor: graphStatusColors.SUCCEEDED,
textColor: positiveTextColor,
},
[TaskExecutionPhase.UNDEFINED]: {
- badgeColor: statusColors.UNKNOWN,
text: t('unknown'),
+ badgeColor: statusColors.UNKNOWN,
+ nodeColor: graphStatusColors.UNDEFINED,
textColor: secondaryTextColor,
},
};
diff --git a/packages/zapp/console/src/components/Executions/strings.ts b/packages/zapp/console/src/components/Executions/strings.ts
index 8856bb3ec..6d7504195 100644
--- a/packages/zapp/console/src/components/Executions/strings.ts
+++ b/packages/zapp/console/src/components/Executions/strings.ts
@@ -15,6 +15,7 @@ const str = {
succeeded: 'Succeeded',
succeeding: 'Succeeding',
timedOut: 'Timed Out',
+ paused: 'Paused',
unknown: 'Unknown',
cacheDisabledMessage: 'Caching was disabled for this execution.',
cacheHitMessage: 'Output for this execution was read from cache.',
diff --git a/packages/zapp/console/src/components/Executions/test/CacheStatus.test.tsx b/packages/zapp/console/src/components/Executions/test/CacheStatus.test.tsx
new file mode 100644
index 000000000..88603683f
--- /dev/null
+++ b/packages/zapp/console/src/components/Executions/test/CacheStatus.test.tsx
@@ -0,0 +1,89 @@
+import { render } from '@testing-library/react';
+import { cacheStatusMessages, viewSourceExecutionString } from 'components/Executions/constants';
+import { CatalogCacheStatus } from 'models/Execution/enums';
+import { mockWorkflowExecutionId } from 'models/Execution/__mocks__/constants';
+import * as React from 'react';
+import { MemoryRouter } from 'react-router';
+import { Routes } from 'routes/routes';
+import { CacheStatus } from '../CacheStatus';
+
+describe('Executions > CacheStatus', () => {
+ const renderComponent = (props) =>
+ render(
+
+
+ ,
+ );
+
+ describe('check renders', () => {
+ it('should not render anything, if cacheStatus is null', () => {
+ const cacheStatus = null;
+ const { container } = renderComponent({ cacheStatus });
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should not render anything, if cacheStatus is undefined', () => {
+ const cacheStatus = undefined;
+ const { container } = renderComponent({ cacheStatus });
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render text with icon, if no variant was provided', () => {
+ const cacheStatus = CatalogCacheStatus.CACHE_POPULATED;
+ const { queryByText, queryByTestId } = renderComponent({ cacheStatus });
+
+ expect(queryByText(cacheStatusMessages[cacheStatus])).toBeInTheDocument();
+ expect(queryByTestId('cache-icon')).toBeInTheDocument();
+ });
+
+ it('should render text with icon, if variant = normal', () => {
+ const cacheStatus = CatalogCacheStatus.CACHE_POPULATED;
+ const { queryByText, queryByTestId } = renderComponent({ cacheStatus, variant: 'normal' });
+
+ expect(queryByText(cacheStatusMessages[cacheStatus])).toBeInTheDocument();
+ expect(queryByTestId('cache-icon')).toBeInTheDocument();
+ });
+
+ it('should not render text, if variant = iconOnly', () => {
+ const cacheStatus = CatalogCacheStatus.CACHE_POPULATED;
+ const { queryByText, queryByTestId } = renderComponent({ cacheStatus, variant: 'iconOnly' });
+
+ expect(queryByText(cacheStatusMessages[cacheStatus])).not.toBeInTheDocument();
+ expect(queryByTestId('cache-icon')).toBeInTheDocument();
+ });
+
+ it('should render source execution link for cache hits', () => {
+ const cacheStatus = CatalogCacheStatus.CACHE_HIT;
+ const sourceTaskExecutionId = {
+ taskId: { ...mockWorkflowExecutionId, version: '1' },
+ nodeExecutionId: { nodeId: 'n1', executionId: mockWorkflowExecutionId },
+ };
+ const { getByText } = renderComponent({ cacheStatus, sourceTaskExecutionId });
+ const linkEl = getByText(viewSourceExecutionString);
+
+ expect(linkEl.getAttribute('href')).toEqual(
+ Routes.ExecutionDetails.makeUrl(mockWorkflowExecutionId),
+ );
+ });
+ });
+
+ describe('check cache statuses', () => {
+ describe.each`
+ cacheStatus | expected
+ ${CatalogCacheStatus.CACHE_DISABLED} | ${cacheStatusMessages[CatalogCacheStatus.CACHE_DISABLED]}
+ ${CatalogCacheStatus.CACHE_HIT} | ${cacheStatusMessages[CatalogCacheStatus.CACHE_HIT]}
+ ${CatalogCacheStatus.CACHE_LOOKUP_FAILURE} | ${cacheStatusMessages[CatalogCacheStatus.CACHE_LOOKUP_FAILURE]}
+ ${CatalogCacheStatus.CACHE_MISS} | ${cacheStatusMessages[CatalogCacheStatus.CACHE_MISS]}
+ ${CatalogCacheStatus.CACHE_POPULATED} | ${cacheStatusMessages[CatalogCacheStatus.CACHE_POPULATED]}
+ ${CatalogCacheStatus.CACHE_PUT_FAILURE} | ${cacheStatusMessages[CatalogCacheStatus.CACHE_PUT_FAILURE]}
+ `('for each case', ({ cacheStatus, expected }) => {
+ it(`renders correct text ${expected} for status ${cacheStatus}`, async () => {
+ const { queryByText } = renderComponent({ cacheStatus });
+
+ expect(queryByText(cacheStatusMessages[cacheStatus])).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/packages/zapp/console/src/components/Executions/test/NodeExecutionCacheStatus.test.tsx b/packages/zapp/console/src/components/Executions/test/NodeExecutionCacheStatus.test.tsx
new file mode 100644
index 000000000..bef42ac0e
--- /dev/null
+++ b/packages/zapp/console/src/components/Executions/test/NodeExecutionCacheStatus.test.tsx
@@ -0,0 +1,71 @@
+import { render, waitFor } from '@testing-library/react';
+import { cacheStatusMessages } from 'components/Executions/constants';
+import { NodeExecutionDetailsContextProvider } from 'components/Executions/contextProvider/NodeExecutionDetails';
+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 { CatalogCacheStatus } from 'models/Execution/enums';
+import { NodeExecution } from 'models/Execution/types';
+import * as React from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
+import { MemoryRouter } from 'react-router';
+import { createTestQueryClient } from 'test/utils';
+import { NodeExecutionCacheStatus } from '../NodeExecutionCacheStatus';
+
+jest.mock('models/Task/utils');
+jest.mock('components/Workflow/workflowQueries');
+const { fetchWorkflow } = require('components/Workflow/workflowQueries');
+
+// TODO add test to cover mapTask branch
+describe('Executions > NodeExecutionCacheStatus', () => {
+ let fixture: ReturnType;
+ let execution: NodeExecution;
+ let queryClient: QueryClient;
+
+ beforeEach(() => {
+ fixture = basicPythonWorkflow.generate();
+ insertFixture(mockServer, fixture);
+ execution = fixture.workflowExecutions.top.nodeExecutions.pythonNode.data;
+ fetchWorkflow.mockImplementation(() => Promise.resolve(fixture.workflows.top));
+ queryClient = createTestQueryClient();
+ });
+
+ const renderComponent = (props) =>
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ it('should not render anything, if cacheStatus is undefined', async () => {
+ const { container } = renderComponent({ execution });
+ await waitFor(() => container);
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should not render anything, if cacheStatus is null', async () => {
+ const mockExecution = {
+ ...execution,
+ closure: { taskNodeMetadata: { cacheStatus: null } },
+ };
+ const { container } = renderComponent({ execution: mockExecution });
+ await waitFor(() => container);
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('should render cache hit status text, if execution has cacheStatus CACHE_HIT', async () => {
+ const cacheStatus = CatalogCacheStatus.CACHE_HIT;
+ const mockExecution = { ...execution, closure: { taskNodeMetadata: { cacheStatus } } };
+ const { queryByText } = renderComponent({ execution: mockExecution });
+ await waitFor(() => queryByText(cacheStatusMessages[cacheStatus]));
+
+ expect(queryByText(cacheStatusMessages[cacheStatus])).toBeInTheDocument();
+ });
+});
diff --git a/packages/zapp/console/src/components/Executions/test/utils.test.ts b/packages/zapp/console/src/components/Executions/test/utils.test.ts
index 9588653c0..e0465016d 100644
--- a/packages/zapp/console/src/components/Executions/test/utils.test.ts
+++ b/packages/zapp/console/src/components/Executions/test/utils.test.ts
@@ -8,11 +8,14 @@ import { Execution, NodeExecution, TaskExecution } from 'models/Execution/types'
import { createMockNodeExecutions } from 'models/Execution/__mocks__/mockNodeExecutionsData';
import { createMockTaskExecutionsListResponse } from 'models/Execution/__mocks__/mockTaskExecutionsData';
import { createMockWorkflowExecutionsListResponse } from 'models/Execution/__mocks__/mockWorkflowExecutionsData';
+import { mockNodes, mockNodesWithGateNode } from 'models/Node/__mocks__/mockNodeData';
import { long, waitFor } from 'test/utils';
import {
getNodeExecutionTimingMS,
+ getNodeFrontendPhase,
getTaskExecutionTimingMS,
getWorkflowExecutionTimingMS,
+ isNodeGateNode,
} from '../utils';
const getMockWorkflowExecution = () => createMockWorkflowExecutionsListResponse(1).executions[0];
@@ -163,3 +166,53 @@ describe('getTaskExecutionTimingMS', () => {
expect(firstResult!.duration).toBeLessThan(secondResult!.duration);
});
});
+
+describe('isNodeGateNode', () => {
+ const executionId = { project: 'project', domain: 'domain', name: 'name' };
+
+ it('should return true if nodeId is in the list and has a gateNode field', () => {
+ expect(isNodeGateNode(mockNodesWithGateNode, { nodeId: 'GateNode', executionId })).toBeTruthy();
+ });
+
+ it('should return false if nodeId is in the list, but a gateNode field is missing', () => {
+ expect(isNodeGateNode(mockNodes, { nodeId: 'BasicNode', executionId })).toBeFalsy();
+ });
+
+ it('should return false if nodeId is not in the list, but has a gateNode field', () => {
+ expect(isNodeGateNode(mockNodes, { nodeId: 'GateNode', executionId })).toBeFalsy();
+ });
+
+ it('should return false if nodeId is a gateNode, but the list is empty', () => {
+ expect(isNodeGateNode([], { nodeId: 'GateNode', executionId })).toBeFalsy();
+ });
+
+ it('should return false if nodeId is not a gateNode and the list is empty', () => {
+ expect(isNodeGateNode([], { nodeId: 'BasicNode', executionId })).toBeFalsy();
+ });
+});
+
+describe('getNodeFrontendPhase', () => {
+ it('should return PAUSED if node is a gateNode in the RUNNING phase', () => {
+ expect(getNodeFrontendPhase(NodeExecutionPhase.RUNNING, true)).toEqual(
+ NodeExecutionPhase.PAUSED,
+ );
+ });
+
+ it('should return phase if node is a gateNode not in the RUNNING phase', () => {
+ expect(getNodeFrontendPhase(NodeExecutionPhase.FAILED, true)).toEqual(
+ NodeExecutionPhase.FAILED,
+ );
+ });
+
+ it('should return RUNNING if node is not a gateNode in the RUNNING phase', () => {
+ expect(getNodeFrontendPhase(NodeExecutionPhase.RUNNING, false)).toEqual(
+ NodeExecutionPhase.RUNNING,
+ );
+ });
+
+ it('should return phase if node is not a gateNode not in the RUNNING phase', () => {
+ expect(getNodeFrontendPhase(NodeExecutionPhase.SUCCEEDED, false)).toEqual(
+ NodeExecutionPhase.SUCCEEDED,
+ );
+ });
+});
diff --git a/packages/zapp/console/src/components/Executions/types.ts b/packages/zapp/console/src/components/Executions/types.ts
index 35791f7c4..f48fa635b 100644
--- a/packages/zapp/console/src/components/Executions/types.ts
+++ b/packages/zapp/console/src/components/Executions/types.ts
@@ -7,8 +7,9 @@ import {
import { TaskTemplate } from 'models/Task/types';
export interface ExecutionPhaseConstants {
- badgeColor: string;
text: string;
+ badgeColor: string;
+ nodeColor: string;
textColor: string;
}
diff --git a/packages/zapp/console/src/components/Executions/utils.ts b/packages/zapp/console/src/components/Executions/utils.ts
index 31c1fa340..61ddca548 100644
--- a/packages/zapp/console/src/components/Executions/utils.ts
+++ b/packages/zapp/console/src/components/Executions/utils.ts
@@ -15,6 +15,7 @@ import {
BaseExecutionClosure,
Execution,
NodeExecution,
+ NodeExecutionIdentifier,
TaskExecution,
} from 'models/Execution/types';
import { CompiledNode } from 'models/Node/types';
@@ -163,3 +164,20 @@ export function isExecutionArchived(execution: Execution): boolean {
const state = execution.closure.stateChangeDetails?.state ?? null;
return !!state && state === ExecutionState.EXECUTION_ARCHIVED;
}
+
+/** Returns true if current node (by nodeId) has 'gateNode' field in the list of nodes on compiledWorkflowClosure */
+export function isNodeGateNode(
+ nodes: CompiledNode[],
+ executionId: NodeExecutionIdentifier,
+): boolean {
+ const node = nodes.find((n) => n.id === executionId.nodeId);
+ return !!node?.gateNode;
+}
+
+/** Transforms phase to Paused for gate nodes in the running state, otherwise returns the phase unchanged */
+export function getNodeFrontendPhase(
+ phase: NodeExecutionPhase,
+ isGateNode: boolean,
+): NodeExecutionPhase {
+ return isGateNode && phase === NodeExecutionPhase.RUNNING ? NodeExecutionPhase.PAUSED : phase;
+}
diff --git a/packages/zapp/console/src/components/Theme/constants.ts b/packages/zapp/console/src/components/Theme/constants.ts
index 39d879978..b89f4a489 100644
--- a/packages/zapp/console/src/components/Theme/constants.ts
+++ b/packages/zapp/console/src/components/Theme/constants.ts
@@ -55,6 +55,18 @@ export const statusColors = {
SKIPPED: COLOR_SPECTRUM.sunset20.color,
UNKNOWN: COLOR_SPECTRUM.gray20.color,
WARNING: COLOR_SPECTRUM.yellow40.color,
+ PAUSED: COLOR_SPECTRUM.amber30.color,
+};
+
+export const graphStatusColors = {
+ FAILED: '#e90000',
+ FAILING: '#f2a4ad',
+ SUCCEEDED: '#37b789',
+ ABORTED: '#be25d7',
+ RUNNING: '#2892f4',
+ QUEUED: '#dfd71b',
+ PAUSED: '#f5a684',
+ UNDEFINED: '#4a2839',
};
export type TaskColorMap = Record;
diff --git a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx
index 443b09778..5038160c2 100644
--- a/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx
+++ b/packages/zapp/console/src/components/WorkflowGraph/WorkflowGraph.tsx
@@ -1,8 +1,10 @@
import * as React from 'react';
import ReactFlowGraphComponent from 'components/flytegraph/ReactFlow/ReactFlowGraphComponent';
+import { Error } from 'models/Common/types';
import { NonIdealState } from 'components/common/NonIdealState';
import { CompiledNode } from 'models/Node/types';
import { TaskExecutionPhase } from 'models/Execution/enums';
+import { dNode } from 'models/Graph/types';
export interface WorkflowGraphProps {
onNodeSelectionChanged: (selectedNodes: string[]) => void;
@@ -12,6 +14,7 @@ export interface WorkflowGraphProps {
mergedDag: any;
error: Error | null;
dynamicWorkflows: any;
+ initialNodes: dNode[];
}
export interface DynamicWorkflowMapping {
rootGraphNodeId: CompiledNode;
@@ -26,6 +29,7 @@ export const WorkflowGraph: React.FC = ({
mergedDag,
error,
dynamicWorkflows,
+ initialNodes,
}) => {
if (error) {
return ;
@@ -39,6 +43,7 @@ export const WorkflowGraph: React.FC = ({
onPhaseSelectionChanged={onPhaseSelectionChanged}
selectedPhase={selectedPhase}
isDetailsTabClosed={isDetailsTabClosed}
+ initialNodes={initialNodes}
/>
);
};
diff --git a/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx b/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx
index 338cd0491..b6f9cae71 100644
--- a/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx
+++ b/packages/zapp/console/src/components/WorkflowGraph/test/WorkflowGraph.test.tsx
@@ -37,6 +37,7 @@ describe('WorkflowGraph', () => {
}}
error={null}
dynamicWorkflows={[]}
+ initialNodes={[]}
/>
,
);
diff --git a/packages/zapp/console/src/components/WorkflowGraph/test/utils.test.ts b/packages/zapp/console/src/components/WorkflowGraph/test/utils.test.ts
index 7a5776680..04f6a657f 100644
--- a/packages/zapp/console/src/components/WorkflowGraph/test/utils.test.ts
+++ b/packages/zapp/console/src/components/WorkflowGraph/test/utils.test.ts
@@ -23,6 +23,7 @@ describe('getDisplayName', () => {
});
});
+// TODO add tests for `launchplan` branch
describe('getNodeTypeFromCompiledNode', () => {
const branchNode = {
branchNode: {},
@@ -30,24 +31,31 @@ describe('getNodeTypeFromCompiledNode', () => {
const workflowNode = {
workflowNode: {},
};
+ const gateNode = {
+ gateNode: {},
+ };
const mockBranchNode = { ...mockCompiledTaskNode, ...branchNode };
const mockWorkflowNode = { ...mockCompiledTaskNode, ...workflowNode };
+ const mockGateNode = { ...mockCompiledTaskNode, ...gateNode };
- it('should return dTypes.start when is start-node', () => {
+ it('should return dTypes.start when node is start-node', () => {
expect(getNodeTypeFromCompiledNode(mockCompiledStartNode)).toBe(dTypes.start);
});
- it('should return dTypes.end when is end-node', () => {
+ it('should return dTypes.end when node is end-node', () => {
expect(getNodeTypeFromCompiledNode(mockCompiledEndNode)).toBe(dTypes.end);
});
- it('should return *dTypes.subworkflow (branch is typed as subworkflow for graph) when is node has branchNodes', () => {
+ it('should return *dTypes.subworkflow (branch is typed as subworkflow for graph) when node has branchNodes', () => {
expect(getNodeTypeFromCompiledNode(mockBranchNode)).toBe(dTypes.subworkflow);
});
- it('should return dTypes.subworkflow when is node has workflowNode', () => {
+ it('should return dTypes.subworkflow when node is workflowNode', () => {
expect(getNodeTypeFromCompiledNode(mockWorkflowNode)).toBe(dTypes.subworkflow);
});
- it('should return dTypes.task when is node is taskNode', () => {
+ it('should return dTypes.task when node is taskNode', () => {
expect(getNodeTypeFromCompiledNode(mockCompiledTaskNode)).toBe(dTypes.task);
});
+ it('should return dTypes.gateNode when node is gateNode', () => {
+ expect(getNodeTypeFromCompiledNode(mockGateNode)).toBe(dTypes.gateNode);
+ });
});
describe('isStartNode', () => {
diff --git a/packages/zapp/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx b/packages/zapp/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx
index a3e7ec999..82cf3b7b8 100644
--- a/packages/zapp/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx
+++ b/packages/zapp/console/src/components/WorkflowGraph/transformerWorkflowToDag.tsx
@@ -68,8 +68,8 @@ export const transformerWorkflowToDag = (
let scopedId = '';
if (isStartOrEndNode(compiledNode) && parentDNode && !isStartOrEndNode(parentDNode)) {
scopedId = `${parentDNode.scopedId}-${compiledNode.id}`;
- } else if (parentDNode && parentDNode.type != dTypes.start) {
- if (parentDNode.type == dTypes.branch || parentDNode.type == dTypes.subworkflow) {
+ } else if (parentDNode && parentDNode.type !== dTypes.start) {
+ if (parentDNode.type === dTypes.branch || parentDNode.type === dTypes.subworkflow) {
scopedId = `${parentDNode.scopedId}-0-${compiledNode.id}`;
} else {
scopedId = `${parentDNode.scopedId}-${compiledNode.id}`;
@@ -88,6 +88,7 @@ export const transformerWorkflowToDag = (
name: getDisplayName(compiledNode),
nodes: [],
edges: [],
+ gateNode: compiledNode.gateNode,
} as dNode;
staticExecutionIdsMap[output.scopedId] = compiledNode;
diff --git a/packages/zapp/console/src/components/WorkflowGraph/utils.ts b/packages/zapp/console/src/components/WorkflowGraph/utils.ts
index 2ea656021..d5743021f 100644
--- a/packages/zapp/console/src/components/WorkflowGraph/utils.ts
+++ b/packages/zapp/console/src/components/WorkflowGraph/utils.ts
@@ -93,6 +93,8 @@ export const getNodeTypeFromCompiledNode = (node: CompiledNode): dTypes => {
} else {
return dTypes.subworkflow;
}
+ } else if (node.gateNode) {
+ return dTypes.gateNode;
} else {
return dTypes.task;
}
diff --git a/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx b/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx
index 20a091894..09f2fad63 100644
--- a/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx
+++ b/packages/zapp/console/src/components/common/MapTaskExecutionsList/TaskNameList.tsx
@@ -2,10 +2,10 @@ import * as React from 'react';
import { Typography } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { Core } from 'flyteidl';
-import { getTaskIndex, getTaskLogName } from 'components/Executions/TaskExecutionsList/utils';
+import { getTaskLogName } from 'components/Executions/TaskExecutionsList/utils';
import { MapTaskExecution, TaskExecution } from 'models/Execution/types';
import { noLogsFoundString } from 'components/Executions/constants';
-import { CacheStatus } from 'components/Executions/NodeExecutionCacheStatus';
+import { CacheStatus } from 'components/Executions/CacheStatus';
import { useCommonStyles } from '../styles';
interface StyleProps {
diff --git a/packages/zapp/console/src/components/common/constants.ts b/packages/zapp/console/src/components/common/constants.ts
index 64f2f041e..b3475fbf9 100644
--- a/packages/zapp/console/src/components/common/constants.ts
+++ b/packages/zapp/console/src/components/common/constants.ts
@@ -1,4 +1,5 @@
import { env } from 'common/env';
+import { graphStatusColors } from 'components/Theme/constants';
import { InterpreterOptions } from 'xstate';
export const detailsPanelWidth = 432;
@@ -16,5 +17,5 @@ export const defaultStateMachineConfig: Partial = {
export const barChartColors = {
default: '#e5e5e5',
success: '#78dfb1',
- failure: '#f2a4ad',
+ failure: graphStatusColors.FAILING,
};
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx
index e88abe852..0f901d1a9 100644
--- a/packages/zapp/console/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/NodeStatusLegend.tsx
@@ -1,9 +1,17 @@
import * as React from 'react';
import { useState, CSSProperties } from 'react';
import { Button } from '@material-ui/core';
-import { nodePhaseColorMapping } from './utils';
+import { nodeExecutionPhaseConstants } from 'components/Executions/constants';
+import {
+ graphButtonContainer,
+ graphButtonStyle,
+ popupContainerStyle,
+ rightPositionStyle,
+} from './commonStyles';
+import t from './strings';
+import { graphNodePhasesList } from './utils';
-export const LegendItem = ({ color, text }) => {
+export const LegendItem = ({ nodeColor, text }) => {
/**
* @TODO temporary check for nested graph until
* nested functionality is deployed
@@ -19,14 +27,14 @@ export const LegendItem = ({ color, text }) => {
const colorStyle: CSSProperties = {
width: '28px',
height: '22px',
- background: isNested ? color : 'none',
- border: `3px solid ${color}`,
+ background: isNested ? nodeColor : 'none',
+ border: `3px solid ${nodeColor}`,
borderRadius: '4px',
paddingRight: '10px',
marginRight: '1rem',
};
return (
-
+
@@ -37,73 +45,41 @@ interface LegendProps {
initialIsVisible?: boolean;
}
-export const Legend: React.FC
= (props) => {
- const { initialIsVisible = false } = props;
-
+export const Legend: React.FC = ({ initialIsVisible = false }) => {
const [isVisible, setIsVisible] = useState(initialIsVisible);
- const positionStyle: CSSProperties = {
- bottom: '1rem',
- right: '1rem',
- zIndex: 10,
- position: 'absolute',
- width: '150px',
- };
-
- const buttonContainer: CSSProperties = {
- width: '100%',
- display: 'flex',
- justifyContent: 'center',
- };
-
- const buttonStyle: CSSProperties = {
- color: '#555',
- width: '100%',
- };
-
const toggleVisibility = () => {
setIsVisible(!isVisible);
};
- const renderLegend = () => {
- const legendContainerStyle: CSSProperties = {
- width: '100%',
- padding: '1rem',
- background: 'rgba(255,255,255,1)',
- border: `1px solid #ddd`,
- borderRadius: '4px',
- boxShadow: '2px 4px 10px rgba(50,50,50,.2)',
- marginBottom: '1rem',
- };
-
- return (
-
- {Object.keys(nodePhaseColorMapping).map((phase) => {
- return (
-
- );
- })}
-
-
- );
- };
+ const renderLegend = () => (
+
+ {graphNodePhasesList.map((phase) => {
+ return (
+
+ );
+ })}
+
+
+ );
return (
-
+
{isVisible ? renderLegend() : null}
-
+
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx
new file mode 100644
index 000000000..244d771f4
--- /dev/null
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/PausedTasksComponent.tsx
@@ -0,0 +1,90 @@
+import * as React from 'react';
+import { useState } from 'react';
+import { Badge, Button, withStyles } from '@material-ui/core';
+import { TaskNames } from 'components/Executions/ExecutionDetails/Timeline/TaskNames';
+import { dNode } from 'models/Graph/types';
+import { isExpanded } from 'components/WorkflowGraph/utils';
+import { NodeExecutionPhase } from 'models/Execution/enums';
+import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum';
+import { nodeExecutionPhaseConstants } from 'components/Executions/constants';
+import {
+ graphButtonContainer,
+ graphButtonStyle,
+ leftPositionStyle,
+ popupContainerStyle,
+} from './commonStyles';
+import t from './strings';
+
+interface PausedTasksComponentProps {
+ pausedNodes: dNode[];
+ initialIsVisible?: boolean;
+}
+
+const CustomBadge = withStyles({
+ badge: {
+ backgroundColor: nodeExecutionPhaseConstants[NodeExecutionPhase.PAUSED].nodeColor,
+ color: COLOR_SPECTRUM.white.color,
+ },
+})(Badge);
+
+export const PausedTasksComponent: React.FC
= ({
+ pausedNodes,
+ initialIsVisible = false,
+}) => {
+ const [isVisible, setIsVisible] = useState(initialIsVisible);
+
+ const toggleVisibility = () => {
+ setIsVisible(!isVisible);
+ };
+
+ const toggleNode = (id: string, scopeId: string, level: number) => {
+ const searchNode = (nodes: dNode[], nodeLevel: number) => {
+ if (!nodes || nodes.length === 0) {
+ return;
+ }
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.id === id && node.scopedId === scopeId && nodeLevel === level) {
+ nodes[i].expanded = !nodes[i].expanded;
+ return;
+ }
+ if (node.nodes.length > 0 && isExpanded(node)) {
+ searchNode(node.nodes, nodeLevel + 1);
+ }
+ }
+ };
+ searchNode(pausedNodes, 0);
+ };
+
+ const resumeAction = () => {
+ // TODO https://github.com/flyteorg/flyteconsole/issues/587 Launch form for node id
+ };
+
+ const renderPausedTasksBlock = () => (
+
+
+
+ );
+
+ return (
+
+
+ {isVisible ? renderPausedTasksBlock() : null}
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx
index b84d2f8bc..c0c6ff222 100644
--- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowGraphComponent.tsx
@@ -2,10 +2,15 @@ import * as React from 'react';
import { useState, useEffect, useContext } from 'react';
import { ConvertFlyteDagToReactFlows } from 'components/flytegraph/ReactFlow/transformDAGToReactFlowV2';
import { NodeExecutionsByIdContext } from 'components/Executions/contexts';
+import { useNodeExecutionContext } from 'components/Executions/contextProvider/NodeExecutionDetails';
+import { NodeExecutionPhase } from 'models/Execution/enums';
+import { isNodeGateNode } from 'components/Executions/utils';
+import { dNode } from 'models/Graph/types';
import { RFWrapperProps, RFGraphTypes, ConvertDagProps } from './types';
import { getRFBackground } from './utils';
import { ReactFlowWrapper } from './ReactFlowWrapper';
import { Legend } from './NodeStatusLegend';
+import { PausedTasksComponent } from './PausedTasksComponent';
const nodeExecutionStatusChanged = (previous, nodeExecutionsById) => {
for (const exe in nodeExecutionsById) {
@@ -44,16 +49,20 @@ const graphNodeCountChanged = (previous, data) => {
}
};
-const ReactFlowGraphComponent = (props) => {
- const {
- data,
- onNodeSelectionChanged,
- onPhaseSelectionChanged,
- selectedPhase,
- isDetailsTabClosed,
- dynamicWorkflows,
- } = props;
+const ReactFlowGraphComponent = ({
+ data,
+ onNodeSelectionChanged,
+ onPhaseSelectionChanged,
+ selectedPhase,
+ isDetailsTabClosed,
+ dynamicWorkflows,
+ initialNodes,
+}) => {
const nodeExecutionsById = useContext(NodeExecutionsByIdContext);
+ const { compiledWorkflowClosure } = useNodeExecutionContext();
+
+ const [pausedNodes, setPausedNodes] = useState([]);
+
const [state, setState] = useState({
data,
dynamicWorkflows,
@@ -141,6 +150,30 @@ const ReactFlowGraphComponent = (props) => {
const backgroundStyle = getRFBackground().nested;
+ useEffect(() => {
+ const pausedNodes: dNode[] = initialNodes.filter((node) => {
+ const nodeExecution = nodeExecutionsById[node.id];
+ if (nodeExecution) {
+ const phase = nodeExecution?.closure.phase;
+ const isGateNode = isNodeGateNode(
+ compiledWorkflowClosure?.primary.template.nodes ?? [],
+ nodeExecution.id,
+ );
+ return isGateNode && phase === NodeExecutionPhase.RUNNING;
+ }
+ return false;
+ });
+ const nodesWithExecutions = pausedNodes.map((node) => {
+ const execution = nodeExecutionsById[node.scopedId];
+ return {
+ ...node,
+ startedAt: execution?.closure.startedAt,
+ execution,
+ };
+ });
+ setPausedNodes(nodesWithExecutions);
+ }, [initialNodes]);
+
const containerStyle: React.CSSProperties = {
display: 'flex',
flex: `1 1 100%`,
@@ -160,6 +193,9 @@ const ReactFlowGraphComponent = (props) => {
};
return (
+ {pausedNodes && pausedNodes.length > 0 && (
+
+ )}
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx
index b2a806eff..d86b5ca54 100644
--- a/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/ReactFlowWrapper.tsx
@@ -11,6 +11,7 @@ import {
ReactFlowCustomMaxNested,
ReactFlowStaticNested,
ReactFlowStaticNode,
+ ReactFlowGateNode,
} from './customNodeComponents';
import { getPositionedNodes, ReactFlowIdHash } from './utils';
@@ -27,6 +28,7 @@ const CustomNodeTypes = {
FlyteNode_nestedMaxDepth: ReactFlowCustomMaxNested,
FlyteNode_staticNode: ReactFlowStaticNode,
FlyteNode_staticNestedNode: ReactFlowStaticNested,
+ FlyteNode_gateNode: ReactFlowGateNode,
};
export const ReactFlowWrapper: React.FC = ({
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/commonStyles.ts b/packages/zapp/console/src/components/flytegraph/ReactFlow/commonStyles.ts
new file mode 100644
index 000000000..4db1f4ee9
--- /dev/null
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/commonStyles.ts
@@ -0,0 +1,39 @@
+import { CSSProperties } from 'react';
+
+const positionStyle: CSSProperties = {
+ bottom: '1rem',
+ zIndex: 10,
+ position: 'absolute',
+ maxHeight: '520px',
+};
+
+export const leftPositionStyle: CSSProperties = {
+ ...positionStyle,
+ left: '1rem',
+ width: '336px',
+};
+
+export const rightPositionStyle: CSSProperties = {
+ ...positionStyle,
+ right: '1rem',
+ width: '150px',
+};
+
+export const graphButtonContainer: CSSProperties = {
+ width: '100%',
+};
+
+export const graphButtonStyle: CSSProperties = {
+ color: '#555',
+ width: '100%',
+};
+
+export const popupContainerStyle: CSSProperties = {
+ width: '100%',
+ padding: '1rem',
+ background: 'rgba(255,255,255,1)',
+ border: `1px solid #ddd`,
+ borderRadius: '4px',
+ boxShadow: '2px 4px 10px rgba(50,50,50,.2)',
+ marginBottom: '1rem',
+};
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx
index 8a74f925f..bc3533c12 100644
--- a/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/customNodeComponents.tsx
@@ -2,21 +2,64 @@ import * as React from 'react';
import { useState, useEffect } from 'react';
import { Handle, Position } from 'react-flow-renderer';
import { dTypes } from 'models/Graph/types';
-import CachedOutlined from '@material-ui/icons/CachedOutlined';
-import { CatalogCacheStatus, TaskExecutionPhase } from 'models/Execution/enums';
-import { PublishedWithChangesOutlined } from 'components/common/PublishedWithChanges';
+import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums';
import { RENDER_ORDER } from 'components/Executions/TaskExecutionsList/constants';
import { whiteColor } from 'components/Theme/constants';
-import { CacheStatus } from 'components/Executions/NodeExecutionCacheStatus';
+import PlayCircleOutlineIcon from '@material-ui/icons/PlayCircleOutline';
+import { Tooltip } from '@material-ui/core';
+import { COLOR_SPECTRUM } from 'components/Theme/colorSpectrum';
+import { getNodeFrontendPhase } from 'components/Executions/utils';
+import { CacheStatus } from 'components/Executions/CacheStatus';
import {
- COLOR_TASK_TYPE,
COLOR_GRAPH_BACKGROUND,
getGraphHandleStyle,
getGraphNodeStyle,
getNestedContainerStyle,
getStatusColor,
} from './utils';
-import { RFHandleProps } from './types';
+import { RFHandleProps, RFNode } from './types';
+import t from './strings';
+
+const taskContainerStyle: React.CSSProperties = {
+ position: 'absolute',
+ top: '-.55rem',
+ zIndex: 0,
+ right: '.15rem',
+};
+
+const taskTypeStyle: React.CSSProperties = {
+ backgroundColor: COLOR_GRAPH_BACKGROUND,
+ color: 'white',
+ padding: '.1rem .2rem',
+ fontSize: '.3rem',
+};
+
+const renderTaskType = (taskType: dTypes | undefined) => {
+ if (!taskType) {
+ return null;
+ }
+ return (
+
+ );
+};
+
+const renderBasicNode = (
+ taskType: dTypes | undefined,
+ text: string,
+ scopedId: string,
+ styles: React.CSSProperties,
+ onClick?: () => void,
+) => {
+ return (
+
+ {renderTaskType(taskType)}
+
{text}
+ {renderDefaultHandles(scopedId, getGraphHandleStyle('source'), getGraphHandleStyle('target'))}
+
+ );
+};
export const renderDefaultHandles = (id: string, sourceStyle: any, targetStyle: any) => {
const leftHandleProps: RFHandleProps = {
@@ -40,8 +83,8 @@ export const renderDefaultHandles = (id: string, sourceStyle: any, targetStyle:
);
};
-export const renderStardEndHandles = (data: any) => {
- const isStart = data.nodeType == dTypes.nestedStart || data.nodeType == dTypes.start;
+export const renderStardEndHandles = (nodeType: dTypes, scopedId: string) => {
+ const isStart = nodeType === dTypes.nestedStart || nodeType === dTypes.start;
const idPrefix = isStart ? 'start' : 'end';
const position = isStart ? Position.Right : Position.Left;
const type = isStart ? 'source' : 'target';
@@ -52,12 +95,12 @@ export const renderStardEndHandles = (data: any) => {
* For now we force nestedMaxDepth for any nested types
*/
const style =
- data.nodeType == dTypes.nestedStart || data.nodeType == dTypes.nestedEnd
+ nodeType === dTypes.nestedStart || nodeType === dTypes.nestedEnd
? getGraphHandleStyle(type, dTypes.nestedMaxDepth)
: getGraphHandleStyle(type);
const handleProps: RFHandleProps = {
- id: `rf-handle-${idPrefix}-${data.scopedId}`,
+ id: `rf-handle-${idPrefix}-${scopedId}`,
type: type,
position: position,
style: style,
@@ -70,12 +113,13 @@ export const renderStardEndHandles = (data: any) => {
* Styles start/end nodes as a point; used for nested workflows
* @param props.data data property of ReactFlowGraphNodeData
*/
-export const ReactFlowCustomNestedPoint = ({ data }: any) => {
- const containerStyle = getGraphNodeStyle(data.nodeType);
+export const ReactFlowCustomNestedPoint = ({ data }: RFNode) => {
+ const { nodeType, scopedId } = data;
+ const containerStyle = getGraphNodeStyle(nodeType);
return (
<>
- {renderStardEndHandles(data)}
+ {renderStardEndHandles(nodeType, scopedId)}
>
);
};
@@ -88,119 +132,27 @@ export const ReactFlowCustomNestedPoint = ({ data }: any) => {
* @param props.data data property of ReactFlowGraphNodeData
*/
-export const ReactFlowCustomMaxNested = ({ data }: any) => {
+export const ReactFlowCustomMaxNested = ({ data }: RFNode) => {
+ const { text, taskType, scopedId, onAddNestedView } = data;
const styles = getGraphNodeStyle(dTypes.nestedMaxDepth);
- const containerStyle = {};
- const taskContainerStyle: React.CSSProperties = {
- position: 'absolute',
- top: '-.55rem',
- zIndex: 0,
- right: '.15rem',
- };
- const taskTypeStyle: React.CSSProperties = {
- backgroundColor: COLOR_GRAPH_BACKGROUND,
- color: 'white',
- padding: '.1rem .2rem',
- fontSize: '.3rem',
- };
-
- const renderTaskType = () => {
- return (
-
- );
- };
const onClick = () => {
- data.onAddNestedView();
+ onAddNestedView();
};
- return (
-
- {data.taskType ? renderTaskType() : null}
-
{data.text}
- {renderDefaultHandles(
- data.scopedId,
- getGraphHandleStyle('source'),
- getGraphHandleStyle('target'),
- )}
-
- );
+ return renderBasicNode(taskType, text, scopedId, styles, onClick);
};
-export const ReactFlowStaticNested = ({ data }: any) => {
+export const ReactFlowStaticNested = ({ data }: RFNode) => {
+ const { text, taskType, scopedId } = data;
const styles = getGraphNodeStyle(dTypes.staticNestedNode);
- const containerStyle = {};
- const taskContainerStyle: React.CSSProperties = {
- position: 'absolute',
- top: '-.55rem',
- zIndex: 0,
- right: '.15rem',
- };
- const taskTypeStyle: React.CSSProperties = {
- backgroundColor: COLOR_GRAPH_BACKGROUND,
- color: 'white',
- padding: '.1rem .2rem',
- fontSize: '.3rem',
- };
-
- const renderTaskType = () => {
- return (
-
- );
- };
-
- return (
-
- {data.taskType ? renderTaskType() : null}
-
{data.text}
- {renderDefaultHandles(
- data.scopedId,
- getGraphHandleStyle('source'),
- getGraphHandleStyle('target'),
- )}
-
- );
+ return renderBasicNode(taskType, text, scopedId, styles);
};
-export const ReactFlowStaticNode = ({ data }: any) => {
+export const ReactFlowStaticNode = ({ data }: RFNode) => {
+ const { text, taskType, scopedId } = data;
const styles = getGraphNodeStyle(dTypes.staticNode);
- const containerStyle = {};
- const taskContainerStyle: React.CSSProperties = {
- position: 'absolute',
- top: '-.55rem',
- zIndex: 0,
- right: '.15rem',
- };
- const taskTypeStyle: React.CSSProperties = {
- backgroundColor: COLOR_GRAPH_BACKGROUND,
- color: 'white',
- padding: '.1rem .2rem',
- fontSize: '.3rem',
- };
-
- const renderTaskType = () => {
- return (
-
- );
- };
-
- return (
-
- {data.taskType ? renderTaskType() : null}
-
{data.text}
- {renderDefaultHandles(
- data.scopedId,
- getGraphHandleStyle('source'),
- getGraphHandleStyle('target'),
- )}
-
- );
+ return renderBasicNode(taskType, text, scopedId, styles);
};
/**
@@ -258,14 +210,66 @@ const TaskPhaseItem = ({
* @param props.data data property of ReactFlowGraphNodeData
*/
-export const ReactFlowCustomTaskNode = ({ data }: any) => {
- const styles = getGraphNodeStyle(data.nodeType, data.nodeExecutionStatus);
- const onNodeSelectionChanged = data.onNodeSelectionChanged;
- const onPhaseSelectionChanged = data.onPhaseSelectionChanged;
- const [selectedNode, setSelectedNode] = useState(false);
- const [selectedPhase, setSelectedPhase] = useState(
- data.selectedPhase,
+export const ReactFlowGateNode = ({ data }: RFNode) => {
+ const { nodeType, nodeExecutionStatus, text, scopedId, onNodeSelectionChanged } = data;
+ const phase = getNodeFrontendPhase(nodeExecutionStatus, true);
+ const styles = getGraphNodeStyle(nodeType, phase);
+
+ const iconStyles: React.CSSProperties = {
+ width: '10px',
+ height: '10px',
+ marginLeft: '4px',
+ marginTop: '1px',
+ color: COLOR_GRAPH_BACKGROUND,
+ cursor: 'pointer',
+ };
+
+ const handleNodeClick = () => {
+ onNodeSelectionChanged(true);
+ };
+
+ const handleActionClick = (e) => {
+ // TODO https://github.com/flyteorg/flyteconsole/issues/587 Launch form for node id
+ e.stopPropagation();
+ };
+
+ return (
+
+
+ {text}
+ {phase === NodeExecutionPhase.PAUSED && (
+
+
+
+ )}
+
+ {renderDefaultHandles(scopedId, getGraphHandleStyle('source'), getGraphHandleStyle('target'))}
+
);
+};
+
+/**
+ * Custom component used by ReactFlow. Renders a label (text)
+ * and any edge handles.
+ * @param props.data data property of ReactFlowGraphNodeData
+ */
+
+export const ReactFlowCustomTaskNode = ({ data }: RFNode) => {
+ const {
+ nodeType,
+ nodeExecutionStatus,
+ selectedPhase: initialPhase,
+ taskType,
+ text,
+ nodeLogsByPhase,
+ cacheStatus,
+ scopedId,
+ onNodeSelectionChanged,
+ onPhaseSelectionChanged,
+ } = data;
+ const styles = getGraphNodeStyle(nodeType, nodeExecutionStatus);
+ const [selectedNode, setSelectedNode] = useState(false);
+ const [selectedPhase, setSelectedPhase] = useState(initialPhase);
useEffect(() => {
if (selectedNode === true) {
@@ -276,18 +280,6 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => {
}
}, [selectedNode, onNodeSelectionChanged, selectedPhase, onPhaseSelectionChanged]);
- const taskContainerStyle: React.CSSProperties = {
- position: 'absolute',
- top: '-.55rem',
- zIndex: 0,
- right: '.15rem',
- };
- const taskTypeStyle: React.CSSProperties = {
- backgroundColor: COLOR_TASK_TYPE,
- color: 'white',
- padding: '.1rem .2rem',
- fontSize: '.3rem',
- };
const mapTaskContainerStyle: React.CSSProperties = {
position: 'absolute',
top: '-.82rem',
@@ -295,7 +287,7 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => {
right: '.15rem',
};
const taskNameStyle: React.CSSProperties = {
- backgroundColor: getStatusColor(data.nodeExecutionStatus),
+ backgroundColor: getStatusColor(nodeExecutionStatus),
color: 'white',
padding: '.1rem .2rem',
fontSize: '.4rem',
@@ -318,18 +310,10 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => {
setSelectedPhase(undefined);
};
- const renderTaskType = () => {
- return (
-
- );
- };
-
const renderTaskName = () => {
return (
);
};
@@ -362,20 +346,12 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => {
return (
- {data.nodeLogsByPhase ? renderTaskName() : data.taskType ? renderTaskType() : null}
+ {nodeLogsByPhase ? renderTaskName() : renderTaskType(taskType)}
- {data.nodeLogsByPhase ? renderTaskPhases(data.nodeLogsByPhase) : data.text}
-
+ {nodeLogsByPhase ? renderTaskPhases(nodeLogsByPhase) : text}
+
- {renderDefaultHandles(
- data.scopedId,
- getGraphHandleStyle('source'),
- getGraphHandleStyle('target'),
- )}
+ {renderDefaultHandles(scopedId, getGraphHandleStyle('source'), getGraphHandleStyle('target'))}
);
};
@@ -385,22 +361,23 @@ export const ReactFlowCustomTaskNode = ({ data }: any) => {
* and any edge handles.
* @param props.data data property of ReactFlowGraphNodeData
*/
-export const ReactFlowSubWorkflowContainer = ({ data }: any) => {
+export const ReactFlowSubWorkflowContainer = ({ data }: RFNode) => {
+ const { nodeExecutionStatus, text, scopedId, currentNestedView, onRemoveNestedView } = data;
const BREAD_FONT_SIZE = '9px';
- const BREAD_COLOR_ACTIVE = '#8B37FF';
- const BREAD_COLOR_INACTIVE = '#000';
- const borderStyle = getNestedContainerStyle(data.nodeExecutionStatus);
+ const BREAD_COLOR_ACTIVE = COLOR_SPECTRUM.purple60.color;
+ const BREAD_COLOR_INACTIVE = COLOR_SPECTRUM.black.color;
+ const borderStyle = getNestedContainerStyle(nodeExecutionStatus);
const handleNestedViewClick = (e) => {
const index = e.target.id.substr(e.target.id.indexOf('_') + 1, e.target.id.length);
- data.onRemoveNestedView(data.scopedId, index);
+ onRemoveNestedView(scopedId, index);
};
const handleRootClick = () => {
- data.onRemoveNestedView(data.scopedId, -1);
+ onRemoveNestedView(scopedId, -1);
};
- const currentNestedDepth = data.currentNestedView?.length || 0;
+ const currentNestedDepth = currentNestedView?.length || 0;
const BreadElement = ({ nestedView, index }) => {
const liStyles: React.CSSProperties = {
@@ -423,7 +400,7 @@ export const ReactFlowSubWorkflowContainer = ({ data }: any) => {
{index === 0 ? {'>'} : null}
{nestedView}
@@ -470,10 +447,10 @@ export const ReactFlowSubWorkflowContainer = ({ data }: any) => {
return (
- {data.currentNestedView?.map((nestedView, i) => {
+ {currentNestedView?.map((nestedView, i) => {
return ;
})}
@@ -486,7 +463,7 @@ export const ReactFlowSubWorkflowContainer = ({ data }: any) => {
{renderBreadCrumb()}
{renderDefaultHandles(
- data.scopedId,
+ scopedId,
getGraphHandleStyle('source'),
getGraphHandleStyle('target'),
)}
@@ -499,12 +476,13 @@ export const ReactFlowSubWorkflowContainer = ({ data }: any) => {
* Custom component renders start node
* @param props.data data property of ReactFlowGraphNodeData
*/
-export const ReactFlowCustomStartNode = ({ data }: any) => {
- const styles = getGraphNodeStyle(data.nodeType);
+export const ReactFlowCustomStartNode = ({ data }: RFNode) => {
+ const { text, nodeType, scopedId } = data;
+ const styles = getGraphNodeStyle(nodeType);
return (
<>
- {data.text}
- {renderStardEndHandles(data)}
+ {text}
+ {renderStardEndHandles(nodeType, scopedId)}
>
);
};
@@ -513,12 +491,13 @@ export const ReactFlowCustomStartNode = ({ data }: any) => {
* Custom component renders start node
* @param props.data data property of ReactFlowGraphNodeData
*/
-export const ReactFlowCustomEndNode = ({ data }: any) => {
- const styles = getGraphNodeStyle(data.nodeType);
+export const ReactFlowCustomEndNode = ({ data }: RFNode) => {
+ const { text, nodeType, scopedId } = data;
+ const styles = getGraphNodeStyle(nodeType);
return (
<>
- {data.text}
- {renderStardEndHandles(data)}
+ {text}
+ {renderStardEndHandles(nodeType, scopedId)}
>
);
};
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/strings.ts b/packages/zapp/console/src/components/flytegraph/ReactFlow/strings.ts
new file mode 100644
index 000000000..68e2940fe
--- /dev/null
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/strings.ts
@@ -0,0 +1,10 @@
+import { createLocalizedString } from '@flyteconsole/locale';
+
+const str = {
+ pausedTasksButton: 'Paused Tasks',
+ legendButton: (isVisible: boolean) => `${isVisible ? 'Hide' : 'Show'} Legend`,
+ resumeTooltip: 'Resume',
+};
+
+export { patternKey } from '@flyteconsole/locale';
+export default createLocalizedString(str);
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/test/NodeStatusLegend.test.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/test/NodeStatusLegend.test.tsx
new file mode 100644
index 000000000..bd350de3f
--- /dev/null
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/test/NodeStatusLegend.test.tsx
@@ -0,0 +1,51 @@
+import * as React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import { Legend } from '../NodeStatusLegend';
+import { graphNodePhasesList } from '../utils';
+
+describe('flytegraph > ReactFlow > NodeStatusLegend', () => {
+ const renderComponent = (props) => render();
+
+ it('should render just the Legend button, if initialIsVisible was not passed', () => {
+ const { queryByTitle, queryByTestId } = renderComponent({});
+ expect(queryByTitle('Show Legend')).toBeInTheDocument();
+ expect(queryByTestId('legend-table')).not.toBeInTheDocument();
+ });
+
+ it('should render just the Legend button, if initialIsVisible is false', () => {
+ const { queryByTitle, queryByTestId } = renderComponent({ initialIsVisible: false });
+ expect(queryByTitle('Show Legend')).toBeInTheDocument();
+ expect(queryByTestId('legend-table')).not.toBeInTheDocument();
+ });
+
+ it('should render Legend table, if initialIsVisible is true', () => {
+ const { queryByTitle, queryByTestId, queryAllByTestId } = renderComponent({
+ initialIsVisible: true,
+ });
+ expect(queryByTitle('Show Legend')).not.toBeInTheDocument();
+ expect(queryByTitle('Hide Legend')).toBeInTheDocument();
+ expect(queryByTestId('legend-table')).toBeInTheDocument();
+ // the number of items should match the graphNodePhasesList const plus one extra for nested nodes
+ expect(queryAllByTestId('legend-item').length).toEqual(graphNodePhasesList.length + 1);
+ });
+
+ it('should render Legend table on button click, and hide it, when clicked again', async () => {
+ const { getByRole, queryByTitle, queryByTestId } = renderComponent({});
+ expect(queryByTitle('Show Legend')).toBeInTheDocument();
+ expect(queryByTitle('Hide Legend')).not.toBeInTheDocument();
+ expect(queryByTestId('legend-table')).not.toBeInTheDocument();
+
+ const button = getByRole('button');
+ await fireEvent.click(button);
+
+ expect(queryByTitle('Show Legend')).not.toBeInTheDocument();
+ expect(queryByTitle('Hide Legend')).toBeInTheDocument();
+ expect(queryByTestId('legend-table')).toBeInTheDocument();
+
+ await fireEvent.click(button);
+
+ expect(queryByTitle('Show Legend')).toBeInTheDocument();
+ expect(queryByTitle('Hide Legend')).not.toBeInTheDocument();
+ expect(queryByTestId('legend-table')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx
new file mode 100644
index 000000000..3320f7852
--- /dev/null
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/test/PausedTasksComponent.test.tsx
@@ -0,0 +1,67 @@
+import * as React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import { dTypes } from 'models/Graph/types';
+import { PausedTasksComponent } from '../PausedTasksComponent';
+
+const pausedNodes = [
+ {
+ id: 'n1',
+ scopedId: 'n1',
+ type: dTypes.gateNode,
+ name: 'node1',
+ nodes: [],
+ edges: [],
+ },
+ {
+ id: 'n2',
+ scopedId: 'n2',
+ type: dTypes.gateNode,
+ name: 'node2',
+ nodes: [],
+ edges: [],
+ },
+];
+
+describe('flytegraph > ReactFlow > PausedTasksComponent', () => {
+ const renderComponent = (props) => render();
+
+ it('should render just the Paused Tasks button, if initialIsVisible was not passed', () => {
+ const { queryByTitle, queryByTestId } = renderComponent({ pausedNodes });
+ expect(queryByTitle('Paused Tasks')).toBeInTheDocument();
+ expect(queryByTestId('paused-tasks-table')).not.toBeInTheDocument();
+ });
+
+ it('should render just the Paused Tasks button, if initialIsVisible is false', () => {
+ const { queryByTitle, queryByTestId } = renderComponent({
+ pausedNodes,
+ initialIsVisible: false,
+ });
+ expect(queryByTitle('Paused Tasks')).toBeInTheDocument();
+ expect(queryByTestId('paused-tasks-table')).not.toBeInTheDocument();
+ });
+
+ it('should render Paused Tasks table, if initialIsVisible is true', () => {
+ const { queryByTitle, queryByTestId, queryAllByTestId } = renderComponent({
+ pausedNodes,
+ initialIsVisible: true,
+ });
+ expect(queryByTitle('Paused Tasks')).toBeInTheDocument();
+ expect(queryByTestId('paused-tasks-table')).toBeInTheDocument();
+ expect(queryAllByTestId('task-name-item').length).toEqual(pausedNodes.length);
+ });
+
+ it('should render Paused Tasks table on button click, and hide it, when clicked again', async () => {
+ const { getByRole, queryByTitle, queryByTestId } = renderComponent({ pausedNodes });
+ expect(queryByTitle('Paused Tasks')).toBeInTheDocument();
+ expect(queryByTestId('paused-tasks-table')).not.toBeInTheDocument();
+
+ const button = getByRole('button');
+ await fireEvent.click(button);
+
+ expect(queryByTestId('paused-tasks-table')).toBeInTheDocument();
+
+ await fireEvent.click(button);
+
+ expect(queryByTestId('paused-tasks-table')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/test/utils.test.ts b/packages/zapp/console/src/components/flytegraph/ReactFlow/test/utils.test.ts
new file mode 100644
index 000000000..33a53de57
--- /dev/null
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/test/utils.test.ts
@@ -0,0 +1,23 @@
+import { nodeExecutionPhaseConstants } from 'components/Executions/constants';
+import { NodeExecutionPhase } from 'models/Execution/enums';
+import { COLOR_NOT_EXECUTED, getStatusColor } from '../utils';
+
+describe('getStatusColor', () => {
+ describe.each`
+ nodeExecutionStatus | expected
+ ${undefined} | ${COLOR_NOT_EXECUTED}
+ ${NodeExecutionPhase.FAILED} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.FAILED].nodeColor}
+ ${NodeExecutionPhase.FAILING} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.FAILING].nodeColor}
+ ${NodeExecutionPhase.SUCCEEDED} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.SUCCEEDED].nodeColor}
+ ${NodeExecutionPhase.ABORTED} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.ABORTED].nodeColor}
+ ${NodeExecutionPhase.RUNNING} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.RUNNING].nodeColor}
+ ${NodeExecutionPhase.QUEUED} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.QUEUED].nodeColor}
+ ${NodeExecutionPhase.PAUSED} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.PAUSED].nodeColor}
+ ${NodeExecutionPhase.UNDEFINED} | ${nodeExecutionPhaseConstants[NodeExecutionPhase.UNDEFINED].nodeColor}
+ `('for each case', ({ nodeExecutionStatus, expected }) => {
+ it(`should return ${expected} when called with nodeExecutionStatus = ${nodeExecutionStatus}`, () => {
+ const result = getStatusColor(nodeExecutionStatus);
+ expect(result).toEqual(expected);
+ });
+ });
+});
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/types.ts b/packages/zapp/console/src/components/flytegraph/ReactFlow/types.ts
index 328c89a8b..c57e4b2cc 100644
--- a/packages/zapp/console/src/components/flytegraph/ReactFlow/types.ts
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/types.ts
@@ -1,5 +1,5 @@
-import { CatalogCacheStatus } from 'models/Execution/enums';
-import { NodeExecutionsById } from 'models/Execution/types';
+import { CatalogCacheStatus, NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums';
+import { LogsByPhase } from 'models/Execution/types';
import { dNode, dTypes } from 'models/Graph/types';
import { HandleProps } from 'react-flow-renderer';
@@ -65,16 +65,24 @@ export interface DagToReactFlowProps extends ConvertDagProps {
parents: any;
}
-export interface RFCustomData {
- nodeExecutionStatus: NodeExecutionsById;
+interface RFCustomData {
+ nodeExecutionStatus: NodeExecutionPhase;
text: string;
handles: [];
nodeType: dTypes;
scopedId: string;
dag: any;
- taskType?: dTypes;
- cacheStatus?: CatalogCacheStatus;
- onNodeSelectionChanged?: any;
- onAddNestedView: any;
- onRemoveNestedView: any;
+ taskType: dTypes;
+ cacheStatus: CatalogCacheStatus;
+ nodeLogsByPhase: LogsByPhase;
+ selectedPhase: TaskExecutionPhase;
+ currentNestedView: string[];
+ onNodeSelectionChanged: (n: boolean) => void;
+ onPhaseSelectionChanged: (p?: TaskExecutionPhase) => void;
+ onAddNestedView: () => void;
+ onRemoveNestedView: (scopedId: string, index: number) => void;
+}
+
+export interface RFNode {
+ data: RFCustomData;
}
diff --git a/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx b/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx
index 99b7f80a5..3d18db427 100644
--- a/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx
+++ b/packages/zapp/console/src/components/flytegraph/ReactFlow/utils.tsx
@@ -2,13 +2,14 @@ import * as React from 'react';
import { NodeExecutionPhase, TaskExecutionPhase } from 'models/Execution/enums';
import { dTypes } from 'models/Graph/types';
import { CSSProperties } from 'react';
+import { graphStatusColors } from 'components/Theme/constants';
+import { nodeExecutionPhaseConstants } from 'components/Executions/constants';
import { RFBackgroundProps } from './types';
const dagre = require('dagre');
-export const COLOR_EXECUTED = '#2892f4';
+export const COLOR_EXECUTED = graphStatusColors.RUNNING;
export const COLOR_NOT_EXECUTED = '#c6c6c6';
-export const COLOR_TASK_TYPE = '#666666';
export const COLOR_GRAPH_BACKGROUND = '#666666';
export const GRAPH_PADDING_FACTOR = 50;
@@ -88,15 +89,16 @@ export const getGraphHandleStyle = (handleType: string, type?: dTypes): CSSPrope
}
};
-export const nodePhaseColorMapping = {
- [NodeExecutionPhase.FAILED]: { color: '#e90000', text: 'Failed' },
- [NodeExecutionPhase.FAILING]: { color: '#f2a4ad', text: 'Failing' },
- [NodeExecutionPhase.SUCCEEDED]: { color: '#37b789', text: 'Succeded' },
- [NodeExecutionPhase.ABORTED]: { color: '#be25d7', text: 'Aborted' },
- [NodeExecutionPhase.RUNNING]: { color: '#2892f4', text: 'Running' },
- [NodeExecutionPhase.QUEUED]: { color: '#dfd71b', text: 'Queued' },
- [NodeExecutionPhase.UNDEFINED]: { color: '#4a2839', text: 'Undefined' },
-};
+export const graphNodePhasesList = [
+ NodeExecutionPhase.FAILED,
+ NodeExecutionPhase.FAILING,
+ NodeExecutionPhase.SUCCEEDED,
+ NodeExecutionPhase.ABORTED,
+ NodeExecutionPhase.RUNNING,
+ NodeExecutionPhase.QUEUED,
+ NodeExecutionPhase.PAUSED,
+ NodeExecutionPhase.UNDEFINED,
+];
/**
* Maps node execution phases to UX colors
@@ -106,11 +108,12 @@ export const nodePhaseColorMapping = {
export const getStatusColor = (
nodeExecutionStatus?: NodeExecutionPhase | TaskExecutionPhase,
): string => {
- if (nodeExecutionStatus && nodePhaseColorMapping[nodeExecutionStatus]) {
- return nodePhaseColorMapping[nodeExecutionStatus].color;
+ // should explicitly check for undefined, as one of the phases is '0' and fails the presence check
+ if (nodeExecutionStatus !== undefined && nodeExecutionPhaseConstants[nodeExecutionStatus]) {
+ return nodeExecutionPhaseConstants[nodeExecutionStatus].nodeColor;
} else {
/** @TODO decide what we want default color to be */
- return '#c6c6c6';
+ return COLOR_NOT_EXECUTED;
}
};
diff --git a/packages/zapp/console/src/models/Execution/enums.ts b/packages/zapp/console/src/models/Execution/enums.ts
index da80201b4..5d45d3a5f 100644
--- a/packages/zapp/console/src/models/Execution/enums.ts
+++ b/packages/zapp/console/src/models/Execution/enums.ts
@@ -14,7 +14,7 @@ export const ExecutionMode = Admin.ExecutionMetadata.ExecutionMode;
export type WorkflowExecutionPhase = Core.WorkflowExecution.Phase;
export const WorkflowExecutionPhase = Core.WorkflowExecution.Phase;
export type NodeExecutionPhase = Core.NodeExecution.Phase;
-export const NodeExecutionPhase = Core.NodeExecution.Phase;
+export const NodeExecutionPhase = { ...Core.NodeExecution.Phase, PAUSED: 100 };
export type TaskExecutionPhase = Core.TaskExecution.Phase;
export const TaskExecutionPhase = Core.TaskExecution.Phase;
enum MapCacheStatus {
diff --git a/packages/zapp/console/src/models/Graph/types.ts b/packages/zapp/console/src/models/Graph/types.ts
index 0ad26e65e..b979e1cfb 100644
--- a/packages/zapp/console/src/models/Graph/types.ts
+++ b/packages/zapp/console/src/models/Graph/types.ts
@@ -26,6 +26,7 @@ export enum dTypes {
nestedMaxDepth,
staticNode,
staticNestedNode,
+ gateNode,
}
/**
diff --git a/packages/zapp/console/src/models/Node/__mocks__/mockNodeData.ts b/packages/zapp/console/src/models/Node/__mocks__/mockNodeData.ts
index 186f29808..28a377849 100644
--- a/packages/zapp/console/src/models/Node/__mocks__/mockNodeData.ts
+++ b/packages/zapp/console/src/models/Node/__mocks__/mockNodeData.ts
@@ -5,3 +5,8 @@ export const mockNodes: CompiledNode[] = mockTasks.map(({ template
const { id } = template;
return { id: id.name, taskNode: { referenceId: id } };
});
+
+export const mockNodesWithGateNode: CompiledNode[] = [
+ ...mockNodes,
+ { id: 'GateNode', gateNode: {} },
+];
diff --git a/packages/zapp/console/src/models/Node/types.ts b/packages/zapp/console/src/models/Node/types.ts
index c723d4f90..cb578b6fc 100644
--- a/packages/zapp/console/src/models/Node/types.ts
+++ b/packages/zapp/console/src/models/Node/types.ts
@@ -33,6 +33,7 @@ export interface CompiledNode extends Core.INode {
taskNode?: TaskNode;
upstreamNodeIds?: string[];
workflowNode?: WorkflowNode;
+ gateNode?: Core.IGateNode;
}
/** Holds all connections/edges for a given `CompiledNode` */