Add workflow run visualizer (#10146)
- Remove the tabs from the workflowRun show page; now, we only show the visualizer with the nodes highlighted based on the run's output - Create the `generateWorkflowRunDiagram` function to go other each step and assign a `runStatus` to it based on the workflow run's output Remaining to do: - Show the output of each step in the right drawer when selecting one - The labels (e.g. "1 item") are not set on the edges; we might implement that later https://github.com/user-attachments/assets/bcf22f4c-db8c-4b02-9a1a-62d688b4c28e Closes https://github.com/twentyhq/core-team-issues/issues/338 Closes https://github.com/twentyhq/core-team-issues/issues/336
This commit is contained in:
committed by
GitHub
parent
1863ef7d10
commit
81b2d5bc89
@ -8,8 +8,7 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE
|
|||||||
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
|
import { FieldsCard } from '@/object-record/record-show/components/FieldsCard';
|
||||||
import { CardType } from '@/object-record/record-show/types/CardType';
|
import { CardType } from '@/object-record/record-show/types/CardType';
|
||||||
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
||||||
import { WorkflowRunOutputVisualizer } from '@/workflow/components/WorkflowRunOutputVisualizer';
|
import { WorkflowRunVisualizer } from '@/workflow/components/WorkflowRunVisualizer';
|
||||||
import { WorkflowRunVersionVisualizer } from '@/workflow/components/WorkflowRunVersionVisualizer';
|
|
||||||
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
||||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
||||||
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
|
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
|
||||||
@ -94,10 +93,6 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
|||||||
),
|
),
|
||||||
|
|
||||||
[CardType.WorkflowRunCard]: ({ targetableObject }) => (
|
[CardType.WorkflowRunCard]: ({ targetableObject }) => (
|
||||||
<WorkflowRunVersionVisualizer workflowRunId={targetableObject.id} />
|
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
||||||
),
|
|
||||||
|
|
||||||
[CardType.WorkflowRunOutputCard]: ({ targetableObject }) => (
|
|
||||||
<WorkflowRunOutputVisualizer workflowRunId={targetableObject.id} />
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import {
|
|||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
IconMail,
|
IconMail,
|
||||||
IconNotes,
|
IconNotes,
|
||||||
IconPrinter,
|
|
||||||
IconSettings,
|
IconSettings,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
@ -180,20 +179,6 @@ export const useRecordShowContainerTabs = (
|
|||||||
},
|
},
|
||||||
[CoreObjectNameSingular.WorkflowRun]: {
|
[CoreObjectNameSingular.WorkflowRun]: {
|
||||||
tabs: {
|
tabs: {
|
||||||
workflowRunOutput: {
|
|
||||||
title: 'Output',
|
|
||||||
position: 0,
|
|
||||||
Icon: IconPrinter,
|
|
||||||
cards: [{ type: CardType.WorkflowRunOutputCard }],
|
|
||||||
hide: {
|
|
||||||
ifMobile: false,
|
|
||||||
ifDesktop: false,
|
|
||||||
ifInRightDrawer: false,
|
|
||||||
ifFeaturesDisabled: [FeatureFlagKey.IsWorkflowEnabled],
|
|
||||||
ifRequiredObjectsInactive: [],
|
|
||||||
ifRelationsMissing: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workflowRunFlow: {
|
workflowRunFlow: {
|
||||||
title: 'Flow',
|
title: 'Flow',
|
||||||
position: 0,
|
position: 0,
|
||||||
|
|||||||
@ -9,6 +9,5 @@ export enum CardType {
|
|||||||
WorkflowCard = 'WorkflowCard',
|
WorkflowCard = 'WorkflowCard',
|
||||||
WorkflowVersionCard = 'WorkflowVersionCard',
|
WorkflowVersionCard = 'WorkflowVersionCard',
|
||||||
WorkflowRunCard = 'WorkflowRunCard',
|
WorkflowRunCard = 'WorkflowRunCard',
|
||||||
WorkflowRunOutputCard = 'WorkflowRunOutputCard',
|
|
||||||
RichTextCard = 'RichTextCard',
|
RichTextCard = 'RichTextCard',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
|
||||||
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
|
||||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
|
||||||
|
|
||||||
export const WorkflowRunVersionVisualizer = ({
|
|
||||||
workflowRunId,
|
|
||||||
}: {
|
|
||||||
workflowRunId: string;
|
|
||||||
}) => {
|
|
||||||
const workflowRun = useWorkflowRun({
|
|
||||||
workflowRunId,
|
|
||||||
});
|
|
||||||
if (!isDefined(workflowRun)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<WorkflowVersionVisualizerEffect
|
|
||||||
workflowVersionId={workflowRun.workflowVersionId}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<WorkflowVersionVisualizer
|
|
||||||
workflowVersionId={workflowRun.workflowVersionId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
|
import { WorkflowRunVisualizerContent } from '@/workflow/components/WorkflowRunVisualizerContent';
|
||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { CodeEditor } from 'twenty-ui';
|
|
||||||
|
|
||||||
const StyledSourceCodeContainer = styled.div`
|
const StyledSourceCodeContainer = styled.div`
|
||||||
margin: ${({ theme }) => theme.spacing(4)};
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const WorkflowRunOutputVisualizer = ({
|
export const WorkflowRunVisualizer = ({
|
||||||
workflowRunId,
|
workflowRunId,
|
||||||
}: {
|
}: {
|
||||||
workflowRunId: string;
|
workflowRunId: string;
|
||||||
@ -19,11 +19,7 @@ export const WorkflowRunOutputVisualizer = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSourceCodeContainer>
|
<StyledSourceCodeContainer>
|
||||||
<CodeEditor
|
<WorkflowRunVisualizerContent workflowRun={workflowRun} />
|
||||||
value={JSON.stringify(workflowRun.output, null, 2)}
|
|
||||||
language="json"
|
|
||||||
options={{ readOnly: true, domReadOnly: true }}
|
|
||||||
/>
|
|
||||||
</StyledSourceCodeContainer>
|
</StyledSourceCodeContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||||
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
|
import { WorkflowDiagramCanvasReadonly } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonly';
|
||||||
|
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const WorkflowRunVisualizerContent = ({
|
||||||
|
workflowRun,
|
||||||
|
}: {
|
||||||
|
workflowRun: WorkflowRun;
|
||||||
|
}) => {
|
||||||
|
const workflowVersion = useWorkflowVersion(workflowRun.workflowVersionId);
|
||||||
|
if (!isDefined(workflowVersion)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WorkflowRunVisualizerEffect
|
||||||
|
workflowRun={workflowRun}
|
||||||
|
workflowVersionId={workflowVersion.id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WorkflowDiagramCanvasReadonly versionStatus={workflowVersion.status} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -206,7 +206,7 @@ export type WorkflowRun = {
|
|||||||
__typename: 'WorkflowRun';
|
__typename: 'WorkflowRun';
|
||||||
id: string;
|
id: string;
|
||||||
workflowVersionId: string;
|
workflowVersionId: string;
|
||||||
output: WorkflowRunOutput;
|
output: WorkflowRunOutput | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Workflow = {
|
export type Workflow = {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||||
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
|
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
|
||||||
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
|
import { WorkflowDiagramCreateStepNode } from '@/workflow/workflow-diagram/components/WorkflowDiagramCreateStepNode';
|
||||||
@ -8,14 +8,14 @@ import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/com
|
|||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
|
|
||||||
export const WorkflowDiagramCanvasEditable = ({
|
export const WorkflowDiagramCanvasEditable = ({
|
||||||
workflowWithCurrentVersion,
|
versionStatus,
|
||||||
}: {
|
}: {
|
||||||
workflowWithCurrentVersion: WorkflowWithCurrentVersion;
|
versionStatus: WorkflowVersionStatus;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<WorkflowDiagramCanvasBase
|
<WorkflowDiagramCanvasBase
|
||||||
status={workflowWithCurrentVersion.currentVersion.status}
|
status={versionStatus}
|
||||||
nodeTypes={{
|
nodeTypes={{
|
||||||
default: WorkflowDiagramStepNodeEditable,
|
default: WorkflowDiagramStepNodeEditable,
|
||||||
'create-step': WorkflowDiagramCreateStepNode,
|
'create-step': WorkflowDiagramCreateStepNode,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
import { WorkflowVersionStatus } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||||
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect';
|
import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasReadonlyEffect';
|
||||||
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
import { WorkflowDiagramDefaultEdge } from '@/workflow/workflow-diagram/components/WorkflowDiagramDefaultEdge';
|
||||||
@ -8,14 +8,14 @@ import { WorkflowDiagramSuccessEdge } from '@/workflow/workflow-diagram/componen
|
|||||||
import { ReactFlowProvider } from '@xyflow/react';
|
import { ReactFlowProvider } from '@xyflow/react';
|
||||||
|
|
||||||
export const WorkflowDiagramCanvasReadonly = ({
|
export const WorkflowDiagramCanvasReadonly = ({
|
||||||
workflowVersion,
|
versionStatus,
|
||||||
}: {
|
}: {
|
||||||
workflowVersion: WorkflowVersion;
|
versionStatus: WorkflowVersionStatus;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<WorkflowDiagramCanvasBase
|
<WorkflowDiagramCanvasBase
|
||||||
status={workflowVersion.status}
|
status={versionStatus}
|
||||||
nodeTypes={{
|
nodeTypes={{
|
||||||
default: WorkflowDiagramStepNodeReadonly,
|
default: WorkflowDiagramStepNodeReadonly,
|
||||||
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
'empty-trigger': WorkflowDiagramEmptyTrigger,
|
||||||
|
|||||||
@ -1,6 +1,26 @@
|
|||||||
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
|
import { WorkflowDiagramStepNodeBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeBase';
|
||||||
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
|
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon';
|
||||||
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import {
|
||||||
|
WorkflowDiagramRunStatus,
|
||||||
|
WorkflowDiagramStepNodeData,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
|
||||||
|
const getNodeVariantFromRunStatus = (
|
||||||
|
runStatus: WorkflowDiagramRunStatus | undefined,
|
||||||
|
) => {
|
||||||
|
switch (runStatus) {
|
||||||
|
case 'success':
|
||||||
|
return 'success';
|
||||||
|
case 'failure':
|
||||||
|
return 'failure';
|
||||||
|
case 'running':
|
||||||
|
return 'default';
|
||||||
|
case 'not-executed':
|
||||||
|
return 'not-executed';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const WorkflowDiagramStepNodeReadonly = ({
|
export const WorkflowDiagramStepNodeReadonly = ({
|
||||||
data,
|
data,
|
||||||
@ -10,7 +30,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
|
|||||||
return (
|
return (
|
||||||
<WorkflowDiagramStepNodeBase
|
<WorkflowDiagramStepNodeBase
|
||||||
name={data.name}
|
name={data.name}
|
||||||
variant="default"
|
variant={getNodeVariantFromRunStatus(data.runStatus)}
|
||||||
nodeType={data.nodeType}
|
nodeType={data.nodeType}
|
||||||
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
Icon={<WorkflowDiagramStepNodeIcon data={data} />}
|
||||||
isLeafNode={data.isLeafNode}
|
isLeafNode={data.isLeafNode}
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||||
|
import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState';
|
||||||
|
import { WorkflowRun } from '@/workflow/types/Workflow';
|
||||||
|
import { workflowDiagramState } from '@/workflow/workflow-diagram/states/workflowDiagramState';
|
||||||
|
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const WorkflowRunVisualizerEffect = ({
|
||||||
|
workflowVersionId,
|
||||||
|
workflowRun,
|
||||||
|
}: {
|
||||||
|
workflowVersionId: string;
|
||||||
|
workflowRun: WorkflowRun;
|
||||||
|
}) => {
|
||||||
|
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||||
|
|
||||||
|
const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState);
|
||||||
|
const setWorkflowDiagram = useSetRecoilState(workflowDiagramState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setWorkflowVersionId(workflowVersionId);
|
||||||
|
}, [setWorkflowVersionId, workflowVersionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
isDefined(workflowVersion) &&
|
||||||
|
isDefined(workflowVersion.trigger) &&
|
||||||
|
isDefined(workflowVersion.steps)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setWorkflowDiagram(undefined);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextWorkflowDiagram = generateWorkflowRunDiagram({
|
||||||
|
trigger: workflowVersion.trigger,
|
||||||
|
steps: workflowVersion.steps,
|
||||||
|
output: workflowRun.output,
|
||||||
|
});
|
||||||
|
|
||||||
|
setWorkflowDiagram(nextWorkflowDiagram);
|
||||||
|
}, [setWorkflowDiagram, workflowRun.output, workflowVersion]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@ -11,6 +11,6 @@ export const WorkflowVersionVisualizer = ({
|
|||||||
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
const workflowVersion = useWorkflowVersion(workflowVersionId);
|
||||||
|
|
||||||
return isDefined(workflowVersion) ? (
|
return isDefined(workflowVersion) ? (
|
||||||
<WorkflowDiagramCanvasReadonly workflowVersion={workflowVersion} />
|
<WorkflowDiagramCanvasReadonly versionStatus={workflowVersion.status} />
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const WorkflowVisualizer = ({ workflowId }: { workflowId: string }) => {
|
|||||||
|
|
||||||
{isDefined(workflowWithCurrentVersion) ? (
|
{isDefined(workflowWithCurrentVersion) ? (
|
||||||
<WorkflowDiagramCanvasEditable
|
<WorkflowDiagramCanvasEditable
|
||||||
workflowWithCurrentVersion={workflowWithCurrentVersion}
|
versionStatus={workflowWithCurrentVersion.currentVersion.status}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
export const FIRST_NODE_POSITION = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const VERTICAL_DISTANCE_BETWEEN_TWO_NODES = 150;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { WorkflowDiagramEmptyTriggerNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
import { Node } from '@xyflow/react';
|
||||||
|
|
||||||
|
export const WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION = {
|
||||||
|
id: TRIGGER_STEP_ID,
|
||||||
|
type: 'empty-trigger',
|
||||||
|
data: {
|
||||||
|
nodeType: 'empty-trigger',
|
||||||
|
isLeafNode: false,
|
||||||
|
} satisfies WorkflowDiagramEmptyTriggerNodeData,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
} satisfies Node<WorkflowDiagramEmptyTriggerNodeData>;
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
|
||||||
|
export const WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE =
|
||||||
|
'success' satisfies WorkflowDiagramEdgeType;
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { EDGE_GREEN_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId';
|
import { EDGE_GREEN_CIRCLE_MARKED_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenCircleMarkedId';
|
||||||
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId';
|
import { EDGE_GREEN_ROUNDED_ARROW_MARKER_ID } from '@/workflow/workflow-diagram/constants/EdgeGreenRoundedArrowMarkerId';
|
||||||
|
import { WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE } from '@/workflow/workflow-diagram/constants/WorkflowDiagramSuccessEdgeType';
|
||||||
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
import { WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
|
||||||
export const WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION = {
|
export const WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION = {
|
||||||
|
type: WORKFLOW_DIAGRAM_SUCCESS_EDGE_TYPE,
|
||||||
markerStart: EDGE_GREEN_CIRCLE_MARKED_ID,
|
markerStart: EDGE_GREEN_CIRCLE_MARKED_ID,
|
||||||
markerEnd: EDGE_GREEN_ROUNDED_ARROW_MARKER_ID,
|
markerEnd: EDGE_GREEN_ROUNDED_ARROW_MARKER_ID,
|
||||||
deletable: false,
|
deletable: false,
|
||||||
|
|||||||
@ -7,23 +7,39 @@ import { Edge, Node } from '@xyflow/react';
|
|||||||
export type WorkflowDiagramNode = Node<WorkflowDiagramNodeData>;
|
export type WorkflowDiagramNode = Node<WorkflowDiagramNodeData>;
|
||||||
export type WorkflowDiagramEdge = Edge;
|
export type WorkflowDiagramEdge = Edge;
|
||||||
|
|
||||||
|
export type WorkflowRunDiagramNode = Node<WorkflowRunDiagramNodeData>;
|
||||||
|
export type WorkflowRunDiagramEdge = Edge;
|
||||||
|
|
||||||
|
export type WorkflowRunDiagram = {
|
||||||
|
nodes: Array<WorkflowRunDiagramNode>;
|
||||||
|
edges: Array<WorkflowRunDiagramEdge>;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowDiagram = {
|
export type WorkflowDiagram = {
|
||||||
nodes: Array<WorkflowDiagramNode>;
|
nodes: Array<WorkflowDiagramNode>;
|
||||||
edges: Array<WorkflowDiagramEdge>;
|
edges: Array<WorkflowDiagramEdge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowDiagramRunStatus =
|
||||||
|
| 'running'
|
||||||
|
| 'success'
|
||||||
|
| 'failure'
|
||||||
|
| 'not-executed';
|
||||||
|
|
||||||
export type WorkflowDiagramStepNodeData =
|
export type WorkflowDiagramStepNodeData =
|
||||||
| {
|
| {
|
||||||
nodeType: 'trigger';
|
nodeType: 'trigger';
|
||||||
triggerType: WorkflowTriggerType;
|
triggerType: WorkflowTriggerType;
|
||||||
name: string;
|
name: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
runStatus?: WorkflowDiagramRunStatus;
|
||||||
isLeafNode: boolean;
|
isLeafNode: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
nodeType: 'action';
|
nodeType: 'action';
|
||||||
actionType: WorkflowActionType;
|
actionType: WorkflowActionType;
|
||||||
name: string;
|
name: string;
|
||||||
|
runStatus?: WorkflowDiagramRunStatus;
|
||||||
isLeafNode: boolean;
|
isLeafNode: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,6 +59,11 @@ export type WorkflowDiagramNodeData =
|
|||||||
| WorkflowDiagramCreateStepNodeData
|
| WorkflowDiagramCreateStepNodeData
|
||||||
| WorkflowDiagramEmptyTriggerNodeData;
|
| WorkflowDiagramEmptyTriggerNodeData;
|
||||||
|
|
||||||
|
export type WorkflowRunDiagramNodeData = Exclude<
|
||||||
|
WorkflowDiagramStepNodeData,
|
||||||
|
'runStatus'
|
||||||
|
> & { runStatus: WorkflowDiagramRunStatus };
|
||||||
|
|
||||||
export type WorkflowDiagramNodeType =
|
export type WorkflowDiagramNodeType =
|
||||||
| 'default'
|
| 'default'
|
||||||
| 'empty-trigger'
|
| 'empty-trigger'
|
||||||
|
|||||||
@ -0,0 +1,955 @@
|
|||||||
|
import {
|
||||||
|
WorkflowRunOutput,
|
||||||
|
WorkflowStep,
|
||||||
|
WorkflowTrigger,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { getUuidV4Mock } from '~/testing/utils/getUuidV4Mock';
|
||||||
|
import { generateWorkflowRunDiagram } from '../generateWorkflowRunDiagram';
|
||||||
|
|
||||||
|
jest.mock('uuid', () => ({
|
||||||
|
v4: getUuidV4Mock(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('generateWorkflowRunDiagram', () => {
|
||||||
|
it('marks node as failed when not at least one attempt is in output', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
name: 'Company created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const steps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Step 2',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Step 3',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const output: WorkflowRunOutput = {
|
||||||
|
steps: {
|
||||||
|
step1: {
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
outputs: [],
|
||||||
|
type: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = generateWorkflowRunDiagram({ trigger, steps, output });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-0",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "trigger",
|
||||||
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-1",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-2",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "failure",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 3",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks node as failed when the last attempt failed', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
name: 'Company created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const steps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Step 2',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Step 3',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const output: WorkflowRunOutput = {
|
||||||
|
steps: {
|
||||||
|
step1: {
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
attemptCount: 1,
|
||||||
|
result: undefined,
|
||||||
|
error: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = generateWorkflowRunDiagram({ trigger, steps, output });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-3",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "trigger",
|
||||||
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-4",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-5",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "failure",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 3",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks all nodes as successful when each node has an output', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
name: 'Company created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const steps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Step 2',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Step 3',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const output: WorkflowRunOutput = {
|
||||||
|
steps: {
|
||||||
|
step1: {
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
attemptCount: 1,
|
||||||
|
result: {},
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'CODE',
|
||||||
|
},
|
||||||
|
step2: {
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Step 2',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
attemptCount: 1,
|
||||||
|
result: {},
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'CODE',
|
||||||
|
},
|
||||||
|
step3: {
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Step 3',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
attemptCount: 1,
|
||||||
|
result: {},
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = generateWorkflowRunDiagram({ trigger, steps, output });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-6",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "trigger",
|
||||||
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-7",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-8",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "success",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "success",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 3",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "success",
|
||||||
|
},
|
||||||
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks node as running and all other ones as not-executed when no output is available at all', () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
name: 'Company created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const steps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Step 2',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Step 3',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const output = null;
|
||||||
|
|
||||||
|
const result = generateWorkflowRunDiagram({ trigger, steps, output });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-9",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "trigger",
|
||||||
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-10",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-11",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "running",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 3",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks node as running and all other ones as not-executed when a node doesn't have an attached output", () => {
|
||||||
|
const trigger: WorkflowTrigger = {
|
||||||
|
name: 'Company created',
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
settings: {
|
||||||
|
eventName: 'company.created',
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const steps: WorkflowStep[] = [
|
||||||
|
{
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: 'Step 2',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'Step 3',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step4',
|
||||||
|
name: 'Step 4',
|
||||||
|
type: 'CODE',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: true },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
|
||||||
|
serverlessFunctionVersion: '1',
|
||||||
|
serverlessFunctionInput: {},
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const output: WorkflowRunOutput = {
|
||||||
|
steps: {
|
||||||
|
step1: {
|
||||||
|
id: 'step1',
|
||||||
|
name: 'Step 1',
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
attemptCount: 1,
|
||||||
|
result: {},
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'CODE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = generateWorkflowRunDiagram({ trigger, steps, output });
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-12",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "trigger",
|
||||||
|
"target": "step1",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-13",
|
||||||
|
"markerEnd": "workflow-edge-green-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-green-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step1",
|
||||||
|
"target": "step2",
|
||||||
|
"type": "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-14",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step2",
|
||||||
|
"target": "step3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deletable": false,
|
||||||
|
"id": "8f3b2121-f194-4ba4-9fbf-15",
|
||||||
|
"markerEnd": "workflow-edge-arrow-rounded",
|
||||||
|
"markerStart": "workflow-edge-gray-circle",
|
||||||
|
"selectable": false,
|
||||||
|
"source": "step3",
|
||||||
|
"target": "step4",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"icon": "IconPlaylistAdd",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Company created",
|
||||||
|
"nodeType": "trigger",
|
||||||
|
"runStatus": "success",
|
||||||
|
"triggerType": "DATABASE_EVENT",
|
||||||
|
},
|
||||||
|
"id": "trigger",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 1",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "success",
|
||||||
|
},
|
||||||
|
"id": "step1",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 2",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "running",
|
||||||
|
},
|
||||||
|
"id": "step2",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 3",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step3",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"actionType": "CODE",
|
||||||
|
"isLeafNode": false,
|
||||||
|
"name": "Step 4",
|
||||||
|
"nodeType": "action",
|
||||||
|
"runStatus": "not-executed",
|
||||||
|
},
|
||||||
|
"id": "step4",
|
||||||
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 450,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -164,8 +164,8 @@ describe('getWorkflowVersionDiagram', () => {
|
|||||||
},
|
},
|
||||||
"id": "step-1",
|
"id": "step-1",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 150,
|
"x": 0,
|
||||||
"y": 100,
|
"y": 150,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
import { WorkflowStep, WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
|
||||||
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
|
||||||
|
import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
|
||||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||||
import {
|
import {
|
||||||
WorkflowDiagram,
|
WorkflowDiagram,
|
||||||
WorkflowDiagramEdge,
|
WorkflowDiagramEdge,
|
||||||
WorkflowDiagramEmptyTriggerNodeData,
|
|
||||||
WorkflowDiagramNode,
|
WorkflowDiagramNode,
|
||||||
WorkflowDiagramStepNodeData,
|
WorkflowDiagramStepNodeData,
|
||||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||||
|
|
||||||
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -26,12 +25,28 @@ export const generateWorkflowDiagram = ({
|
|||||||
const nodes: Array<WorkflowDiagramNode> = [];
|
const nodes: Array<WorkflowDiagramNode> = [];
|
||||||
const edges: Array<WorkflowDiagramEdge> = [];
|
const edges: Array<WorkflowDiagramEdge> = [];
|
||||||
|
|
||||||
const processNode = (
|
if (isDefined(trigger)) {
|
||||||
step: WorkflowStep,
|
nodes.push(getWorkflowDiagramTriggerNode({ trigger }));
|
||||||
parentNodeId: string,
|
} else {
|
||||||
xPos: number,
|
nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION);
|
||||||
yPos: number,
|
}
|
||||||
) => {
|
|
||||||
|
const processNode = ({
|
||||||
|
stepIndex,
|
||||||
|
parentNodeId,
|
||||||
|
xPos,
|
||||||
|
yPos,
|
||||||
|
}: {
|
||||||
|
stepIndex: number;
|
||||||
|
parentNodeId: string;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}) => {
|
||||||
|
const step = steps.at(stepIndex);
|
||||||
|
if (!isDefined(step)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nodeId = step.id;
|
const nodeId = step.id;
|
||||||
|
|
||||||
nodes.push({
|
nodes.push({
|
||||||
@ -44,7 +59,7 @@ export const generateWorkflowDiagram = ({
|
|||||||
} satisfies WorkflowDiagramStepNodeData,
|
} satisfies WorkflowDiagramStepNodeData,
|
||||||
position: {
|
position: {
|
||||||
x: xPos,
|
x: xPos,
|
||||||
y: yPos,
|
y: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -55,91 +70,20 @@ export const generateWorkflowDiagram = ({
|
|||||||
target: nodeId,
|
target: nodeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return nodeId;
|
processNode({
|
||||||
|
stepIndex: stepIndex + 1,
|
||||||
|
parentNodeId: nodeId,
|
||||||
|
xPos,
|
||||||
|
yPos: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerNodeId = TRIGGER_STEP_ID;
|
processNode({
|
||||||
|
stepIndex: 0,
|
||||||
if (isDefined(trigger)) {
|
parentNodeId: TRIGGER_STEP_ID,
|
||||||
let triggerDefaultLabel: string;
|
xPos: FIRST_NODE_POSITION.x,
|
||||||
let triggerIcon: string | undefined;
|
yPos: FIRST_NODE_POSITION.y,
|
||||||
|
});
|
||||||
switch (trigger.type) {
|
|
||||||
case 'MANUAL': {
|
|
||||||
triggerDefaultLabel = 'Manual Trigger';
|
|
||||||
triggerIcon = getTriggerIcon({
|
|
||||||
type: 'MANUAL',
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'CRON': {
|
|
||||||
triggerDefaultLabel = 'On a Schedule';
|
|
||||||
triggerIcon = getTriggerIcon({
|
|
||||||
type: 'CRON',
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'DATABASE_EVENT': {
|
|
||||||
const triggerEvent = splitWorkflowTriggerEventName(
|
|
||||||
trigger.settings.eventName,
|
|
||||||
);
|
|
||||||
|
|
||||||
triggerDefaultLabel =
|
|
||||||
DATABASE_TRIGGER_TYPES.find(
|
|
||||||
(item) => item.event === triggerEvent.event,
|
|
||||||
)?.defaultLabel ?? '';
|
|
||||||
|
|
||||||
triggerIcon = getTriggerIcon({
|
|
||||||
type: 'DATABASE_EVENT',
|
|
||||||
eventName: triggerEvent.event,
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return assertUnreachable(
|
|
||||||
trigger,
|
|
||||||
`Expected the trigger "${JSON.stringify(trigger)}" to be supported.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.push({
|
|
||||||
id: triggerNodeId,
|
|
||||||
data: {
|
|
||||||
nodeType: 'trigger',
|
|
||||||
triggerType: trigger.type,
|
|
||||||
name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel,
|
|
||||||
icon: triggerIcon,
|
|
||||||
isLeafNode: false,
|
|
||||||
} satisfies WorkflowDiagramStepNodeData,
|
|
||||||
position: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
nodes.push({
|
|
||||||
id: triggerNodeId,
|
|
||||||
type: 'empty-trigger',
|
|
||||||
data: {
|
|
||||||
nodeType: 'empty-trigger',
|
|
||||||
isLeafNode: false,
|
|
||||||
} satisfies WorkflowDiagramEmptyTriggerNodeData,
|
|
||||||
position: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastStepId = triggerNodeId;
|
|
||||||
|
|
||||||
for (const step of steps) {
|
|
||||||
lastStepId = processNode(step, lastStepId, 150, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes,
|
nodes,
|
||||||
|
|||||||
@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
WorkflowRunOutput,
|
||||||
|
WorkflowStep,
|
||||||
|
WorkflowTrigger,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
|
||||||
|
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
|
||||||
|
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||||
|
import { WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION } from '@/workflow/workflow-diagram/constants/WorkflowVisualizerEdgeSuccessConfiguration';
|
||||||
|
import {
|
||||||
|
WorkflowDiagramRunStatus,
|
||||||
|
WorkflowRunDiagram,
|
||||||
|
WorkflowRunDiagramEdge,
|
||||||
|
WorkflowRunDiagramNode,
|
||||||
|
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const generateWorkflowRunDiagram = ({
|
||||||
|
trigger,
|
||||||
|
steps,
|
||||||
|
output,
|
||||||
|
}: {
|
||||||
|
trigger: WorkflowTrigger;
|
||||||
|
steps: Array<WorkflowStep>;
|
||||||
|
output: WorkflowRunOutput | null;
|
||||||
|
}): WorkflowRunDiagram => {
|
||||||
|
const triggerBase = getWorkflowDiagramTriggerNode({ trigger });
|
||||||
|
|
||||||
|
const nodes: Array<WorkflowRunDiagramNode> = [
|
||||||
|
{
|
||||||
|
...triggerBase,
|
||||||
|
data: {
|
||||||
|
...triggerBase.data,
|
||||||
|
runStatus: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const edges: Array<WorkflowRunDiagramEdge> = [];
|
||||||
|
|
||||||
|
const processNode = ({
|
||||||
|
stepIndex,
|
||||||
|
parentNodeId,
|
||||||
|
parentRunStatus,
|
||||||
|
xPos,
|
||||||
|
yPos,
|
||||||
|
skippedExecution,
|
||||||
|
}: {
|
||||||
|
stepIndex: number;
|
||||||
|
parentNodeId: string;
|
||||||
|
parentRunStatus: WorkflowDiagramRunStatus;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
skippedExecution: boolean;
|
||||||
|
}) => {
|
||||||
|
const step = steps.at(stepIndex);
|
||||||
|
if (!isDefined(step)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeId = step.id;
|
||||||
|
|
||||||
|
if (parentRunStatus === 'success') {
|
||||||
|
edges.push({
|
||||||
|
...WORKFLOW_VISUALIZER_EDGE_SUCCESS_CONFIGURATION,
|
||||||
|
id: v4(),
|
||||||
|
source: parentNodeId,
|
||||||
|
target: nodeId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
edges.push({
|
||||||
|
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||||
|
id: v4(),
|
||||||
|
source: parentNodeId,
|
||||||
|
target: nodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const runResult = output?.steps[nodeId];
|
||||||
|
|
||||||
|
let runStatus: WorkflowDiagramRunStatus;
|
||||||
|
if (skippedExecution) {
|
||||||
|
runStatus = 'not-executed';
|
||||||
|
} else if (!isDefined(runResult)) {
|
||||||
|
runStatus = 'running';
|
||||||
|
} else {
|
||||||
|
const lastAttempt = runResult.outputs.at(-1);
|
||||||
|
|
||||||
|
if (!isDefined(lastAttempt)) {
|
||||||
|
// Should never happen. Should we throw instead?
|
||||||
|
runStatus = 'failure';
|
||||||
|
} else if (isDefined(lastAttempt.error)) {
|
||||||
|
runStatus = 'failure';
|
||||||
|
} else {
|
||||||
|
runStatus = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
data: {
|
||||||
|
nodeType: 'action',
|
||||||
|
actionType: step.type,
|
||||||
|
name: step.name,
|
||||||
|
isLeafNode: false,
|
||||||
|
runStatus,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: xPos,
|
||||||
|
y: yPos,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
processNode({
|
||||||
|
stepIndex: stepIndex + 1,
|
||||||
|
parentNodeId: nodeId,
|
||||||
|
parentRunStatus: runStatus,
|
||||||
|
xPos,
|
||||||
|
yPos: yPos + VERTICAL_DISTANCE_BETWEEN_TWO_NODES,
|
||||||
|
skippedExecution: skippedExecution
|
||||||
|
? true
|
||||||
|
: runStatus === 'failure' || runStatus === 'running',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
processNode({
|
||||||
|
stepIndex: 0,
|
||||||
|
parentNodeId: TRIGGER_STEP_ID,
|
||||||
|
parentRunStatus: 'success',
|
||||||
|
xPos: FIRST_NODE_POSITION.x,
|
||||||
|
yPos: FIRST_NODE_POSITION.y,
|
||||||
|
skippedExecution: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { WorkflowTrigger } from '@/workflow/types/Workflow';
|
||||||
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
|
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||||
|
import { WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||||
|
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||||
|
import { Node } from '@xyflow/react';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const getWorkflowDiagramTriggerNode = ({
|
||||||
|
trigger,
|
||||||
|
}: {
|
||||||
|
trigger: WorkflowTrigger;
|
||||||
|
}): Node<WorkflowDiagramStepNodeData> => {
|
||||||
|
let triggerDefaultLabel: string;
|
||||||
|
let triggerIcon: string | undefined;
|
||||||
|
|
||||||
|
switch (trigger.type) {
|
||||||
|
case 'MANUAL': {
|
||||||
|
triggerDefaultLabel = 'Manual Trigger';
|
||||||
|
triggerIcon = getTriggerIcon({
|
||||||
|
type: 'MANUAL',
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'CRON': {
|
||||||
|
triggerDefaultLabel = 'On a Schedule';
|
||||||
|
triggerIcon = getTriggerIcon({
|
||||||
|
type: 'CRON',
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'DATABASE_EVENT': {
|
||||||
|
const triggerEvent = splitWorkflowTriggerEventName(
|
||||||
|
trigger.settings.eventName,
|
||||||
|
);
|
||||||
|
|
||||||
|
triggerDefaultLabel =
|
||||||
|
DATABASE_TRIGGER_TYPES.find((item) => item.event === triggerEvent.event)
|
||||||
|
?.defaultLabel ?? '';
|
||||||
|
|
||||||
|
triggerIcon = getTriggerIcon({
|
||||||
|
type: 'DATABASE_EVENT',
|
||||||
|
eventName: triggerEvent.event,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return assertUnreachable(
|
||||||
|
trigger,
|
||||||
|
`Expected the trigger "${JSON.stringify(trigger)}" to be supported.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: TRIGGER_STEP_ID,
|
||||||
|
data: {
|
||||||
|
nodeType: 'trigger',
|
||||||
|
triggerType: trigger.type,
|
||||||
|
name: isDefined(trigger.name) ? trigger.name : triggerDefaultLabel,
|
||||||
|
icon: triggerIcon,
|
||||||
|
isLeafNode: false,
|
||||||
|
} satisfies WorkflowDiagramStepNodeData,
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user