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

@ -6,6 +6,7 @@ import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { useWorkflowSelectedNodeOrThrow } from '@/workflow/workflow-diagram/hooks/useWorkflowSelectedNodeOrThrow';
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 { WORKFLOW_RUN_STEP_SIDE_PANEL_TAB_LIST_COMPONENT_ID } from '@/workflow/workflow-steps/constants/WorkflowRunStepSidePanelTabListComponentId';
import { getWorkflowRunStepExecutionStatus } from '@/workflow/workflow-steps/utils/getWorkflowRunStepExecutionStatus';
@ -38,7 +39,7 @@ export const RightDrawerWorkflowRunViewStep = () => {
})
: undefined;
const isInputTabDisabled =
const areInputAndOutputTabsDisabled =
workflowSelectedNode === TRIGGER_STEP_ID ||
stepExecutionStatus === 'running' ||
stepExecutionStatus === 'not-executed';
@ -49,9 +50,14 @@ export const RightDrawerWorkflowRunViewStep = () => {
id: 'input',
title: 'Input',
Icon: IconLogin2,
disabled: isInputTabDisabled,
disabled: areInputAndOutputTabsDisabled,
},
{
id: 'output',
title: 'Output',
Icon: IconLogout,
disabled: areInputAndOutputTabsDisabled,
},
{ id: 'output', title: 'Output', Icon: IconLogout },
];
if (!isDefined(workflowRun)) {
@ -80,6 +86,10 @@ export const RightDrawerWorkflowRunViewStep = () => {
{activeTabId === 'input' ? (
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
) : 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};
width: 420px;
overflow: hidden;
& > input:disabled {
color: ${({ theme }) => theme.font.color.primary};
}
`;
const StyledHeaderType = styled.div`

View File

@ -117,7 +117,6 @@ export const InputTabNotExecutedStep: Story = {
return <Story />;
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@ -138,5 +137,52 @@ export const OutputTab: Story = {
await waitFor(() => {
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();
},
};