Visualize workflow run step output (#10730)

- Displays the output of the selected step in the `Output` tab
- Access to the `Output` tab is prevented when the selected node is
currently executed or was skipped
- Display the status of the workflow run instead of the status of the
workflow version at the top left corner of the workflow run visualizer
- Fixed the icon's color for disabled tabs
- Use text/primary color for the step's name even when the input is
disabled

## Demo: Successful execution


https://github.com/user-attachments/assets/02e492f3-1589-48e9-926e-7edb031d9210

## Demo: Failed execution


https://github.com/user-attachments/assets/73e5ec86-5f38-4306-aa9a-46b2e73950da

Closes https://github.com/twentyhq/core-team-issues/issues/434
This commit is contained in:
Baptiste Devessier
2025-03-07 17:35:39 +01:00
committed by GitHub
parent 0e1d742f3d
commit b49ec864b1
19 changed files with 219 additions and 107 deletions

View File

@ -83,7 +83,9 @@ export const Tab = ({
const theme = useTheme(); const theme = useTheme();
const iconColor = active const iconColor = active
? theme.font.color.primary ? theme.font.color.primary
: theme.font.color.secondary; : disabled
? theme.font.color.light
: theme.font.color.secondary;
return ( return (
<StyledTab <StyledTab

View File

@ -1,5 +1,5 @@
import { WorkflowRunVisualizerContent } from '@/workflow/components/WorkflowRunVisualizerContent';
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
@ -19,7 +19,7 @@ export const WorkflowRunVisualizer = ({
return ( return (
<StyledSourceCodeContainer> <StyledSourceCodeContainer>
<WorkflowRunVisualizerContent workflowRun={workflowRun} /> <WorkflowRunDiagramCanvas workflowRunStatus={workflowRun.status} />
</StyledSourceCodeContainer> </StyledSourceCodeContainer>
); );
}; };

View File

@ -1,17 +0,0 @@
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
import { WorkflowRun } from '@/workflow/types/Workflow';
import { WorkflowRunDiagramCanvas } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvas';
import { isDefined } from 'twenty-shared';
export const WorkflowRunVisualizerContent = ({
workflowRun,
}: {
workflowRun: WorkflowRun;
}) => {
const workflowVersion = useWorkflowVersion(workflowRun.workflowVersionId);
if (!isDefined(workflowVersion)) {
return null;
}
return <WorkflowRunDiagramCanvas versionStatus={workflowVersion.status} />;
};

View File

@ -15,6 +15,7 @@ import {
workflowRunOutputSchema, workflowRunOutputSchema,
workflowRunOutputStepsOutputSchema, workflowRunOutputStepsOutputSchema,
workflowRunSchema, workflowRunSchema,
workflowRunStatusSchema,
workflowSendEmailActionSchema, workflowSendEmailActionSchema,
workflowSendEmailActionSettingsSchema, workflowSendEmailActionSettingsSchema,
workflowTriggerSchema, workflowTriggerSchema,
@ -107,6 +108,8 @@ export type WorkflowRunContext = z.infer<typeof workflowRunContextSchema>;
export type WorkflowRunFlow = WorkflowRunOutput['flow']; export type WorkflowRunFlow = WorkflowRunOutput['flow'];
export type WorkflowRunStatus = z.infer<typeof workflowRunStatusSchema>;
export type WorkflowRun = z.infer<typeof workflowRunSchema>; export type WorkflowRun = z.infer<typeof workflowRunSchema>;
export type Workflow = { export type Workflow = {

View File

@ -197,6 +197,13 @@ export const workflowRunOutputSchema = z.object({
export const workflowRunContextSchema = z.record(z.any()); export const workflowRunContextSchema = z.record(z.any());
export const workflowRunStatusSchema = z.enum([
'NOT_STARTED',
'RUNNING',
'COMPLETED',
'FAILED',
]);
export const workflowRunSchema = z export const workflowRunSchema = z
.object({ .object({
__typename: z.literal('WorkflowRun'), __typename: z.literal('WorkflowRun'),
@ -204,6 +211,7 @@ export const workflowRunSchema = z
workflowVersionId: z.string(), workflowVersionId: z.string(),
output: workflowRunOutputSchema.nullable(), output: workflowRunOutputSchema.nullable(),
context: workflowRunContextSchema.nullable(), context: workflowRunContextSchema.nullable(),
status: workflowRunStatusSchema,
createdAt: z.string(), createdAt: z.string(),
deletedAt: z.string().nullable(), deletedAt: z.string().nullable(),
endedAt: z.string().nullable(), endedAt: z.string().nullable(),

View File

@ -1,8 +1,6 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose'; import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose';
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers'; import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/components/WorkflowDiagramCustomMarkers';
import { WorkflowVersionStatusTag } from '@/workflow/workflow-diagram/components/WorkflowVersionStatusTag';
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState'; import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState'; import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState'; import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState';
@ -16,6 +14,8 @@ import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrgani
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import {
applyEdgeChanges,
applyNodeChanges,
Background, Background,
EdgeChange, EdgeChange,
EdgeProps, EdgeProps,
@ -23,15 +23,13 @@ import {
NodeChange, NodeChange,
NodeProps, NodeProps,
ReactFlow, ReactFlow,
applyEdgeChanges,
applyNodeChanges,
useReactFlow, useReactFlow,
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import React, { useEffect, useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { THEME_COMMON } from 'twenty-ui'; import { Tag, TagColor, THEME_COMMON } from 'twenty-ui';
const StyledResetReactflowStyles = styled.div` const StyledResetReactflowStyles = styled.div`
height: 100%; height: 100%;
@ -83,12 +81,13 @@ const defaultFitViewOptions = {
} satisfies FitViewOptions; } satisfies FitViewOptions;
export const WorkflowDiagramCanvasBase = ({ export const WorkflowDiagramCanvasBase = ({
status,
nodeTypes, nodeTypes,
edgeTypes, edgeTypes,
children, children,
tagContainerTestId,
tagColor,
tagText,
}: { }: {
status: WorkflowVersionStatus;
nodeTypes: Partial< nodeTypes: Partial<
Record< Record<
WorkflowDiagramNodeType, WorkflowDiagramNodeType,
@ -112,6 +111,9 @@ export const WorkflowDiagramCanvasBase = ({
> >
>; >;
children?: React.ReactNode; children?: React.ReactNode;
tagContainerTestId: string;
tagColor: TagColor;
tagText: string;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@ -258,8 +260,8 @@ export const WorkflowDiagramCanvasBase = ({
{children} {children}
</ReactFlow> </ReactFlow>
<StyledStatusTagContainer data-testid="workflow-visualizer-status"> <StyledStatusTagContainer data-testid={tagContainerTestId}>
<WorkflowVersionStatusTag versionStatus={status} /> <Tag color={tagColor} text={tagText} />
</StyledStatusTagContainer> </StyledStatusTagContainer>
</StyledResetReactflowStyles> </StyledResetReactflowStyles>
); );

View File

@ -5,6 +5,7 @@ import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/compo
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge'; import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable'; import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeEditable';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { ReactFlowProvider } from '@xyflow/react'; import { ReactFlowProvider } from '@xyflow/react';
export const WorkflowDiagramCanvasEditable = ({ export const WorkflowDiagramCanvasEditable = ({
@ -12,10 +13,13 @@ export const WorkflowDiagramCanvasEditable = ({
}: { }: {
versionStatus: WorkflowVersionStatus; versionStatus: WorkflowVersionStatus;
}) => { }) => {
const tagProps = getWorkflowVersionStatusTagProps({
workflowVersionStatus: versionStatus,
});
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<WorkflowDiagramCanvasBase <WorkflowDiagramCanvasBase
status={versionStatus}
nodeTypes={{ nodeTypes={{
default: WorkflowDiagramStepNodeEditable, default: WorkflowDiagramStepNodeEditable,
'create-step': WorkflowDiagramCreateStepNode, 'create-step': WorkflowDiagramCreateStepNode,
@ -24,7 +28,11 @@ export const WorkflowDiagramCanvasEditable = ({
edgeTypes={{ edgeTypes={{
default: WorkflowDiagramDefaultEdge, default: WorkflowDiagramDefaultEdge,
}} }}
tagContainerTestId="workflow-visualizer-status"
tagColor={tagProps.color}
tagText={tagProps.text}
/> />
<WorkflowDiagramCanvasEditableEffect /> <WorkflowDiagramCanvasEditableEffect />
</ReactFlowProvider> </ReactFlowProvider>
); );

View File

@ -5,6 +5,7 @@ import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/componen
import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger'; import { WorkflowDiagramEmptyTrigger } from '@/workflow/workflow-diagram/components/WorkflowDiagramEmptyTrigger';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge'; import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
import { ReactFlowProvider } from '@xyflow/react'; import { ReactFlowProvider } from '@xyflow/react';
export const WorkflowDiagramCanvasReadonly = ({ export const WorkflowDiagramCanvasReadonly = ({
@ -12,10 +13,13 @@ export const WorkflowDiagramCanvasReadonly = ({
}: { }: {
versionStatus: WorkflowVersionStatus; versionStatus: WorkflowVersionStatus;
}) => { }) => {
const tagProps = getWorkflowVersionStatusTagProps({
workflowVersionStatus: versionStatus,
});
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<WorkflowDiagramCanvasBase <WorkflowDiagramCanvasBase
status={versionStatus}
nodeTypes={{ nodeTypes={{
default: WorkflowDiagramStepNodeReadonly, default: WorkflowDiagramStepNodeReadonly,
'empty-trigger': WorkflowDiagramEmptyTrigger, 'empty-trigger': WorkflowDiagramEmptyTrigger,
@ -24,7 +28,11 @@ export const WorkflowDiagramCanvasReadonly = ({
default: WorkflowDiagramDefaultEdge, default: WorkflowDiagramDefaultEdge,
success: WorkflowDiagramSuccessEdge, success: WorkflowDiagramSuccessEdge,
}} }}
tagContainerTestId="workflow-visualizer-status"
tagColor={tagProps.color}
tagText={tagProps.text}
/> />
<WorkflowDiagramCanvasReadonlyEffect /> <WorkflowDiagramCanvasReadonlyEffect />
</ReactFlowProvider> </ReactFlowProvider>
); );

View File

@ -1,20 +1,24 @@
import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; import { WorkflowRunStatus } from '@/workflow/types/Workflow';
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase'; import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge'; import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly'; import { WorkflowDiagramStepNodeReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeReadonly';
import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge'; import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramSuccessEdge';
import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect'; import { WorkflowRunDiagramCanvasEffect } from '@/workflow/workflow-diagram/components/WorkflowRunDiagramCanvasEffect';
import { getWorkflowRunStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowRunStatusTagProps';
import { ReactFlowProvider } from '@xyflow/react'; import { ReactFlowProvider } from '@xyflow/react';
export const WorkflowRunDiagramCanvas = ({ export const WorkflowRunDiagramCanvas = ({
versionStatus, workflowRunStatus,
}: { }: {
versionStatus: WorkflowVersionStatus; workflowRunStatus: WorkflowRunStatus;
}) => { }) => {
const tagProps = getWorkflowRunStatusTagProps({
workflowRunStatus,
});
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<WorkflowDiagramCanvasBase <WorkflowDiagramCanvasBase
status={versionStatus}
nodeTypes={{ nodeTypes={{
default: WorkflowDiagramStepNodeReadonly, default: WorkflowDiagramStepNodeReadonly,
}} }}
@ -22,6 +26,9 @@ export const WorkflowRunDiagramCanvas = ({
default: WorkflowDiagramDefaultEdge, default: WorkflowDiagramDefaultEdge,
success: WorkflowDiagramSuccessEdge, success: WorkflowDiagramSuccessEdge,
}} }}
tagContainerTestId="workflow-run-status"
tagColor={tagProps.color}
tagText={tagProps.text}
/> />
<WorkflowRunDiagramCanvasEffect /> <WorkflowRunDiagramCanvasEffect />

View File

@ -40,7 +40,10 @@ export const WorkflowRunDiagramCanvasEffect = () => {
workflowRunRightDrawerListActiveTabIdState, workflowRunRightDrawerListActiveTabIdState,
) as WorkflowRunTabId | null; ) as WorkflowRunTabId | null;
if (activeWorkflowRunRightDrawerTab === 'input') { if (
activeWorkflowRunRightDrawerTab === 'input' ||
activeWorkflowRunRightDrawerTab === 'output'
) {
set(workflowRunRightDrawerListActiveTabIdState, 'node'); set(workflowRunRightDrawerListActiveTabIdState, 'node');
} }
}, },

View File

@ -1,22 +0,0 @@
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { Tag } from 'twenty-ui';
export const WorkflowVersionStatusTag = ({
versionStatus,
}: {
versionStatus: WorkflowVersionStatus;
}) => {
if (versionStatus === 'ACTIVE') {
return <Tag color="green" text="Active" />;
}
if (versionStatus === 'DRAFT') {
return <Tag color="yellow" text="Draft" />;
}
if (versionStatus === 'ARCHIVED') {
return <Tag color="gray" text="Archived" />;
}
return <Tag color="gray" text="Deactivated" />;
};

View File

@ -41,7 +41,6 @@ type Story = StoryObj<typeof WorkflowDiagramCanvasBase>;
export const DefaultEdge: Story = { export const DefaultEdge: Story = {
args: { args: {
status: 'DRAFT',
nodeTypes: { nodeTypes: {
default: WorkflowDiagramStepNodeReadonly, default: WorkflowDiagramStepNodeReadonly,
'create-step': WorkflowDiagramCreateStepNode, 'create-step': WorkflowDiagramCreateStepNode,
@ -116,7 +115,6 @@ export const DefaultEdge: Story = {
export const SuccessEdge: Story = { export const SuccessEdge: Story = {
args: { args: {
status: 'DRAFT',
nodeTypes: { nodeTypes: {
default: WorkflowDiagramStepNodeReadonly, default: WorkflowDiagramStepNodeReadonly,
'create-step': WorkflowDiagramCreateStepNode, 'create-step': WorkflowDiagramCreateStepNode,

View File

@ -1,43 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { CatalogDecorator, CatalogStory, ComponentDecorator } from 'twenty-ui';
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { WorkflowVersionStatusTag } from '../WorkflowVersionStatusTag';
const meta: Meta<typeof WorkflowVersionStatusTag> = {
title: 'Modules/Workflow/WorkflowVersionStatusTag',
component: WorkflowVersionStatusTag,
};
export default meta;
type Story = StoryObj<typeof WorkflowVersionStatusTag>;
export const Default: Story = {
args: {
versionStatus: 'DRAFT',
},
decorators: [ComponentDecorator],
};
export const Catalog: CatalogStory<Story, typeof WorkflowVersionStatusTag> = {
argTypes: {
versionStatus: { table: { disable: true } },
},
parameters: {
catalog: {
dimensions: [
{
name: 'version status',
values: [
'DRAFT',
'ACTIVE',
'DEACTIVATED',
'ARCHIVED',
] satisfies WorkflowVersionStatus[],
props: (versionStatus: WorkflowVersionStatus) => ({ versionStatus }),
},
],
},
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,34 @@
import { WorkflowRunStatus } from '@/workflow/types/Workflow';
import { TagColor } from 'twenty-ui';
export const getWorkflowRunStatusTagProps = ({
workflowRunStatus,
}: {
workflowRunStatus: WorkflowRunStatus;
}): { color: TagColor; text: string } => {
if (workflowRunStatus === 'NOT_STARTED') {
return {
color: 'gray',
text: 'Not started',
};
}
if (workflowRunStatus === 'RUNNING') {
return {
color: 'yellow',
text: 'Running',
};
}
if (workflowRunStatus === 'COMPLETED') {
return {
color: 'green',
text: 'Completed',
};
}
return {
color: 'red',
text: 'Failed',
};
};

View File

@ -0,0 +1,34 @@
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
import { TagColor } from 'twenty-ui';
export const getWorkflowVersionStatusTagProps = ({
workflowVersionStatus,
}: {
workflowVersionStatus: WorkflowVersionStatus;
}): { color: TagColor; text: string } => {
if (workflowVersionStatus === 'ARCHIVED') {
return {
color: 'gray',
text: 'Archived',
};
}
if (workflowVersionStatus === 'DRAFT') {
return {
color: 'yellow',
text: 'Draft',
};
}
if (workflowVersionStatus === 'ACTIVE') {
return {
color: 'green',
text: 'Active',
};
}
return {
color: 'gray',
text: 'Deactivated',
};
};

View File

@ -6,6 +6,7 @@ import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow'; import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow'; import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail'; import { WorkflowRunStepInputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepInputDetail';
import { WorkflowRunStepOutputDetail } from '@/workflow/workflow-steps/components/WorkflowRunStepOutputDetail';
import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail'; import { WorkflowStepDetail } from '@/workflow/workflow-steps/components/WorkflowStepDetail';
import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId'; import { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus'; import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
@ -38,7 +39,7 @@ export const RightDrawerWorkflowRunViewStep = () => {
}) })
: undefined; : undefined;
const isInputTabDisabled = const areInputAndOutputTabsDisabled =
workflowSelectedNode === TRIGGER_STEP_ID || workflowSelectedNode === TRIGGER_STEP_ID ||
stepExecutionStatus === 'running' || stepExecutionStatus === 'running' ||
stepExecutionStatus === 'not-executed'; stepExecutionStatus === 'not-executed';
@ -49,9 +50,14 @@ export const RightDrawerWorkflowRunViewStep = () => {
id: 'input', id: 'input',
title: 'Input', title: 'Input',
Icon: IconLogin2, Icon: IconLogin2,
disabled: isInputTabDisabled, disabled: areInputAndOutputTabsDisabled,
},
{
id: 'output',
title: 'Output',
Icon: IconLogout,
disabled: areInputAndOutputTabsDisabled,
}, },
{ id: 'output', title: 'Output', Icon: IconLogout },
]; ];
if (!isDefined(workflowRun)) { if (!isDefined(workflowRun)) {
@ -80,6 +86,10 @@ export const RightDrawerWorkflowRunViewStep = () => {
{activeTabId === 'input' ? ( {activeTabId === 'input' ? (
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} /> <WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
) : null} ) : null}
{activeTabId === 'output' ? (
<WorkflowRunStepOutputDetail stepId={workflowSelectedNode} />
) : null}
</> </>
); );
}; };

View File

@ -0,0 +1,27 @@
import { JsonTree } from '@/workflow/components/json-visualizer/components/JsonTree';
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared';
const StyledContainer = styled.div`
padding-block: ${({ theme }) => theme.spacing(4)};
padding-inline: ${({ theme }) => theme.spacing(3)};
`;
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
const workflowRunId = useWorkflowRunIdOrThrow();
const workflowRun = useWorkflowRun({ workflowRunId });
if (!isDefined(workflowRun?.output?.stepsOutput)) {
return null;
}
const stepOutput = workflowRun.output.stepsOutput[stepId];
return (
<StyledContainer>
<JsonTree value={stepOutput} />
</StyledContainer>
);
};

View File

@ -26,6 +26,10 @@ const StyledHeaderTitle = styled.div`
font-size: ${({ theme }) => theme.font.size.xl}; font-size: ${({ theme }) => theme.font.size.xl};
width: 420px; width: 420px;
overflow: hidden; overflow: hidden;
& > input:disabled {
color: ${({ theme }) => theme.font.color.primary};
}
`; `;
const StyledHeaderType = styled.div` const StyledHeaderType = styled.div`

View File

@ -117,7 +117,6 @@ export const InputTabNotExecutedStep: Story = {
return <Story />; return <Story />;
}, },
], ],
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
@ -138,5 +137,52 @@ export const OutputTab: Story = {
await waitFor(() => { await waitFor(() => {
expect(canvas.queryByText('Create Record')).not.toBeInTheDocument(); expect(canvas.queryByText('Create Record')).not.toBeInTheDocument();
}); });
expect(await canvas.findByText('result')).toBeVisible();
},
};
export const OutputTabDisabledForTrigger: Story = {
decorators: [
(Story) => {
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
setWorkflowSelectedNode(TRIGGER_STEP_ID);
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const outputTab = await canvas.findByRole('button', { name: 'Output' });
expect(outputTab).toBeDisabled();
},
};
export const OutputTabNotExecutedStep: Story = {
decorators: [
(Story) => {
const setWorkflowSelectedNode = useSetRecoilState(
workflowSelectedNodeState,
);
setWorkflowSelectedNode(
oneFailedWorkflowRunQueryResult.workflowRun.output.flow.steps.at(-1)!
.id,
);
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const outputTab = await canvas.findByRole('button', { name: 'Output' });
expect(outputTab).toBeDisabled();
}, },
}; };