Make the frontend resilient to old workflow run output formats (#10522)

- Create zod schemas for everything related to a workflow run
- Update the types to be inferred from the zod schemas
- Improper workflow run outputs will render a blank screen; we could
show an error in the future



https://github.com/user-attachments/assets/8e666c3e-82b0-4ab5-8804-2f70130ea257
This commit is contained in:
Baptiste Devessier
2025-02-27 10:36:19 +01:00
committed by GitHub
parent 5a39903d42
commit 8bd9bc9d31
7 changed files with 324 additions and 411 deletions

View File

@ -1,4 +1,4 @@
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; import { useWorkflowRunUnsafe } from '@/workflow/hooks/useWorkflowRunUnsafe';
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'; import { CodeEditor } from 'twenty-ui';
@ -12,7 +12,8 @@ export const WorkflowRunOutputVisualizer = ({
}: { }: {
workflowRunId: string; workflowRunId: string;
}) => { }) => {
const workflowRun = useWorkflowRun({ workflowRunId }); const workflowRun = useWorkflowRunUnsafe({ workflowRunId });
if (!isDefined(workflowRun)) { if (!isDefined(workflowRun)) {
return null; return null;
} }

View File

@ -1,16 +1,23 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { WorkflowRun } from '@/workflow/types/Workflow'; import { WorkflowRun } from '@/workflow/types/Workflow';
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
export const useWorkflowRun = ({ export const useWorkflowRun = ({
workflowRunId, workflowRunId,
}: { }: {
workflowRunId: string; workflowRunId: string;
}) => { }): WorkflowRun | undefined => {
const { record } = useFindOneRecord<WorkflowRun>({ const { record: rawRecord } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkflowRun, objectNameSingular: CoreObjectNameSingular.WorkflowRun,
objectRecordId: workflowRunId, objectRecordId: workflowRunId,
}); });
const { success, data: record } = workflowRunSchema.safeParse(rawRecord);
if (!success) {
return undefined;
}
return record; return record;
}; };

View File

@ -0,0 +1,16 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { WorkflowRun } from '@/workflow/types/Workflow';
export const useWorkflowRunUnsafe = ({
workflowRunId,
}: {
workflowRunId: string;
}) => {
const { record } = useFindOneRecord<WorkflowRun>({
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
objectRecordId: workflowRunId,
});
return record;
};

View File

@ -1,179 +1,89 @@
type BaseWorkflowActionSettings = { import {
input: object; workflowActionSchema,
outputSchema: object; workflowCodeActionSchema,
errorHandlingOptions: { workflowCodeActionSettingsSchema,
retryOnFailure: { workflowCreateRecordActionSchema,
value: boolean; workflowCreateRecordActionSettingsSchema,
}; workflowCronTriggerSchema,
continueOnFailure: { workflowDatabaseEventTriggerSchema,
value: boolean; workflowDeleteRecordActionSchema,
}; workflowDeleteRecordActionSettingsSchema,
}; workflowFindRecordsActionSchema,
}; workflowFindRecordsActionSettingsSchema,
workflowManualTriggerSchema,
workflowRunOutputSchema,
workflowRunSchema,
workflowSendEmailActionSchema,
workflowSendEmailActionSettingsSchema,
workflowTriggerSchema,
workflowUpdateRecordActionSchema,
workflowUpdateRecordActionSettingsSchema,
} from '@/workflow/validation-schemas/workflowSchema';
import { z } from 'zod';
export type WorkflowCodeActionSettings = BaseWorkflowActionSettings & { export type WorkflowCodeActionSettings = z.infer<
input: { typeof workflowCodeActionSettingsSchema
serverlessFunctionId: string; >;
serverlessFunctionVersion: string; export type WorkflowSendEmailActionSettings = z.infer<
serverlessFunctionInput: { typeof workflowSendEmailActionSettingsSchema
[key: string]: any; >;
}; export type WorkflowCreateRecordActionSettings = z.infer<
}; typeof workflowCreateRecordActionSettingsSchema
}; >;
export type WorkflowUpdateRecordActionSettings = z.infer<
typeof workflowUpdateRecordActionSettingsSchema
>;
export type WorkflowDeleteRecordActionSettings = z.infer<
typeof workflowDeleteRecordActionSettingsSchema
>;
export type WorkflowFindRecordsActionSettings = z.infer<
typeof workflowFindRecordsActionSettingsSchema
>;
export type WorkflowSendEmailActionSettings = BaseWorkflowActionSettings & { export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;
input: { export type WorkflowSendEmailAction = z.infer<
connectedAccountId: string; typeof workflowSendEmailActionSchema
email: string; >;
subject?: string; export type WorkflowCreateRecordAction = z.infer<
body?: string; typeof workflowCreateRecordActionSchema
}; >;
}; export type WorkflowUpdateRecordAction = z.infer<
typeof workflowUpdateRecordActionSchema
type ObjectRecord = Record<string, any>; >;
export type WorkflowDeleteRecordAction = z.infer<
export type WorkflowCreateRecordActionSettings = BaseWorkflowActionSettings & { typeof workflowDeleteRecordActionSchema
input: { >;
objectName: string; export type WorkflowFindRecordsAction = z.infer<
objectRecord: ObjectRecord; typeof workflowFindRecordsActionSchema
}; >;
};
export type WorkflowUpdateRecordActionSettings = BaseWorkflowActionSettings & {
input: {
objectName: string;
objectRecord: ObjectRecord;
objectRecordId: string;
fieldsToUpdate: string[];
};
};
export type WorkflowDeleteRecordActionSettings = BaseWorkflowActionSettings & {
input: {
objectName: string;
objectRecordId: string;
};
};
export type WorkflowFindRecordsActionSettings = BaseWorkflowActionSettings & {
input: {
objectName: string;
limit?: number;
};
};
type BaseWorkflowAction = {
id: string;
name: string;
valid: boolean;
};
export type WorkflowCodeAction = BaseWorkflowAction & {
type: 'CODE';
settings: WorkflowCodeActionSettings;
};
export type WorkflowSendEmailAction = BaseWorkflowAction & {
type: 'SEND_EMAIL';
settings: WorkflowSendEmailActionSettings;
};
export type WorkflowCreateRecordAction = BaseWorkflowAction & {
type: 'CREATE_RECORD';
settings: WorkflowCreateRecordActionSettings;
};
export type WorkflowUpdateRecordAction = BaseWorkflowAction & {
type: 'UPDATE_RECORD';
settings: WorkflowUpdateRecordActionSettings;
};
export type WorkflowDeleteRecordAction = BaseWorkflowAction & {
type: 'DELETE_RECORD';
settings: WorkflowDeleteRecordActionSettings;
};
export type WorkflowFindRecordsAction = BaseWorkflowAction & {
type: 'FIND_RECORDS';
settings: WorkflowFindRecordsActionSettings;
};
export type WorkflowAction =
| WorkflowCodeAction
| WorkflowSendEmailAction
| WorkflowCreateRecordAction
| WorkflowUpdateRecordAction
| WorkflowDeleteRecordAction
| WorkflowFindRecordsAction;
export type WorkflowAction = z.infer<typeof workflowActionSchema>;
export type WorkflowActionType = WorkflowAction['type']; export type WorkflowActionType = WorkflowAction['type'];
export type WorkflowStep = WorkflowAction; export type WorkflowStep = WorkflowAction;
export type WorkflowStepType = WorkflowStep['type']; export type WorkflowStepType = WorkflowStep['type'];
type BaseTrigger = { export type WorkflowDatabaseEventTrigger = z.infer<
name?: string; typeof workflowDatabaseEventTriggerSchema
type: string; >;
}; export type WorkflowManualTrigger = z.infer<typeof workflowManualTriggerSchema>;
export type WorkflowCronTrigger = z.infer<typeof workflowCronTriggerSchema>;
export type WorkflowDatabaseEventTrigger = BaseTrigger & {
type: 'DATABASE_EVENT';
settings: {
eventName: string;
input?: object;
outputSchema: object;
objectType?: string;
};
};
export type WorkflowManualTrigger = BaseTrigger & {
type: 'MANUAL';
settings: {
objectType?: string;
outputSchema: object;
};
};
export type WorkflowCronTrigger = BaseTrigger & {
type: 'CRON';
settings: (
| {
type: 'HOURS';
schedule: { hour: number; minute: number };
}
| {
type: 'MINUTES';
schedule: { minute: number };
}
| {
type: 'CUSTOM';
pattern: string;
}
) & { outputSchema: object };
};
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings']; export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
export type WorkflowManualTriggerAvailability = export type WorkflowManualTriggerAvailability =
| 'EVERYWHERE' | 'EVERYWHERE'
| 'WHEN_RECORD_SELECTED'; | 'WHEN_RECORD_SELECTED';
export type WorkflowTrigger = export type WorkflowTrigger = z.infer<typeof workflowTriggerSchema>;
| WorkflowDatabaseEventTrigger
| WorkflowManualTrigger
| WorkflowCronTrigger;
export type WorkflowTriggerType = WorkflowTrigger['type']; export type WorkflowTriggerType = WorkflowTrigger['type'];
export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED'; export type WorkflowStatus = 'DRAFT' | 'ACTIVE' | 'DEACTIVATED';
export type WorkflowVersionStatus = export type WorkflowVersionStatus =
| 'DRAFT' | 'DRAFT'
| 'ACTIVE' | 'ACTIVE'
| 'DEACTIVATED' | 'DEACTIVATED'
| 'ARCHIVED'; | 'ARCHIVED';
// Keep existing types that are not covered by schemas
export type WorkflowVersion = { export type WorkflowVersion = {
id: string; id: string;
name: string; name: string;
@ -186,32 +96,10 @@ export type WorkflowVersion = {
__typename: 'WorkflowVersion'; __typename: 'WorkflowVersion';
}; };
type StepRunOutput = { export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;
id: string; export type WorkflowRunOutputStepsOutput = WorkflowRunOutput['stepsOutput'];
outputs: {
attemptCount: number;
result: object | undefined;
error: string | undefined;
}[];
};
export type WorkflowRunOutputStepsOutput = Record<string, StepRunOutput>; export type WorkflowRun = z.infer<typeof workflowRunSchema>;
export type WorkflowRunOutput = {
flow: {
trigger: WorkflowTrigger;
steps: WorkflowAction[];
};
stepsOutput?: WorkflowRunOutputStepsOutput;
error?: string;
};
export type WorkflowRun = {
__typename: 'WorkflowRun';
id: string;
workflowVersionId: string;
output: WorkflowRunOutput | null;
};
export type Workflow = { export type Workflow = {
__typename: 'Workflow'; __typename: 'Workflow';

View File

@ -0,0 +1,205 @@
import { z } from 'zod';
// Base schemas
export const objectRecordSchema = z.record(z.any());
export const baseWorkflowActionSettingsSchema = z.object({
input: z.object({}).passthrough(),
outputSchema: z.object({}).passthrough(),
errorHandlingOptions: z.object({
retryOnFailure: z.object({
value: z.boolean(),
}),
continueOnFailure: z.object({
value: z.boolean(),
}),
}),
});
export const baseWorkflowActionSchema = z.object({
id: z.string(),
name: z.string(),
valid: z.boolean(),
});
export const baseTriggerSchema = z.object({
name: z.string().optional(),
type: z.string(),
});
// Action settings schemas
export const workflowCodeActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
serverlessFunctionId: z.string(),
serverlessFunctionVersion: z.string(),
serverlessFunctionInput: z.record(z.any()),
}),
});
export const workflowSendEmailActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
connectedAccountId: z.string(),
email: z.string(),
subject: z.string().optional(),
body: z.string().optional(),
}),
});
export const workflowCreateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
}),
});
export const workflowUpdateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
objectRecordId: z.string(),
fieldsToUpdate: z.array(z.string()),
}),
});
export const workflowDeleteRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecordId: z.string(),
}),
});
export const workflowFindRecordsActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
limit: z.number().optional(),
}),
});
// Action schemas
export const workflowCodeActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CODE'),
settings: workflowCodeActionSettingsSchema,
});
export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('SEND_EMAIL'),
settings: workflowSendEmailActionSettingsSchema,
});
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
},
);
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
},
);
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
},
);
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FIND_RECORDS'),
settings: workflowFindRecordsActionSettingsSchema,
});
// Combined action schema
export const workflowActionSchema = z.discriminatedUnion('type', [
workflowCodeActionSchema,
workflowSendEmailActionSchema,
workflowCreateRecordActionSchema,
workflowUpdateRecordActionSchema,
workflowDeleteRecordActionSchema,
workflowFindRecordsActionSchema,
]);
// Trigger schemas
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
type: z.literal('DATABASE_EVENT'),
settings: z.object({
eventName: z.string(),
input: z.object({}).passthrough().optional(),
outputSchema: z.object({}).passthrough(),
objectType: z.string().optional(),
}),
});
export const workflowManualTriggerSchema = baseTriggerSchema.extend({
type: z.literal('MANUAL'),
settings: z.object({
objectType: z.string().optional(),
outputSchema: z.object({}).passthrough(),
}),
});
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
type: z.literal('CRON'),
settings: z.discriminatedUnion('type', [
z.object({
type: z.literal('HOURS'),
schedule: z.object({ hour: z.number(), minute: z.number() }),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('MINUTES'),
schedule: z.object({ minute: z.number() }),
outputSchema: z.object({}).passthrough(),
}),
z.object({
type: z.literal('CUSTOM'),
pattern: z.string(),
outputSchema: z.object({}).passthrough(),
}),
]),
});
// Combined trigger schema
export const workflowTriggerSchema = z.discriminatedUnion('type', [
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
workflowCronTriggerSchema,
]);
// Step output schemas
const workflowExecutorOutputSchema = z.object({
result: z.any().optional(),
error: z.string().optional(),
});
const workflowRunOutputStepsOutputSchema = z.record(
workflowExecutorOutputSchema,
);
// Final workflow run output schema
export const workflowRunOutputSchema = z.object({
flow: z.object({
trigger: workflowTriggerSchema,
steps: z.array(workflowActionSchema),
}),
stepsOutput: workflowRunOutputStepsOutputSchema.optional(),
error: z.string().optional(),
});
export const workflowRunSchema = z.object({
__typename: z.literal('WorkflowRun'),
id: z.string(),
workflowVersionId: z.string(),
output: workflowRunOutputSchema.nullable(),
});
export type WorkflowRunOutput = z.infer<typeof workflowRunOutputSchema>;

View File

@ -11,175 +11,6 @@ jest.mock('uuid', () => ({
})); }));
describe('generateWorkflowRunDiagram', () => { 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 stepsOutput: WorkflowRunOutputStepsOutput = {
step1: {
id: 'step1',
outputs: [],
},
};
const result = generateWorkflowRunDiagram({ trigger, steps, stepsOutput });
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', () => { it('marks node as failed when the last attempt failed', () => {
const trigger: WorkflowTrigger = { const trigger: WorkflowTrigger = {
name: 'Company created', name: 'Company created',
@ -247,14 +78,8 @@ describe('generateWorkflowRunDiagram', () => {
]; ];
const stepsOutput: WorkflowRunOutputStepsOutput = { const stepsOutput: WorkflowRunOutputStepsOutput = {
step1: { step1: {
id: 'step1', result: undefined,
outputs: [ error: '',
{
attemptCount: 1,
result: undefined,
error: '',
},
],
}, },
}; };
@ -265,7 +90,7 @@ describe('generateWorkflowRunDiagram', () => {
"edges": [ "edges": [
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-3", "id": "8f3b2121-f194-4ba4-9fbf-0",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -275,7 +100,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-4", "id": "8f3b2121-f194-4ba4-9fbf-1",
"markerEnd": "workflow-edge-arrow-rounded", "markerEnd": "workflow-edge-arrow-rounded",
"markerStart": "workflow-edge-gray-circle", "markerStart": "workflow-edge-gray-circle",
"selectable": false, "selectable": false,
@ -284,7 +109,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-5", "id": "8f3b2121-f194-4ba4-9fbf-2",
"markerEnd": "workflow-edge-arrow-rounded", "markerEnd": "workflow-edge-arrow-rounded",
"markerStart": "workflow-edge-gray-circle", "markerStart": "workflow-edge-gray-circle",
"selectable": false, "selectable": false,
@ -422,34 +247,16 @@ describe('generateWorkflowRunDiagram', () => {
]; ];
const stepsOutput: WorkflowRunOutputStepsOutput = { const stepsOutput: WorkflowRunOutputStepsOutput = {
step1: { step1: {
id: 'step1', result: {},
outputs: [ error: undefined,
{
attemptCount: 1,
result: {},
error: undefined,
},
],
}, },
step2: { step2: {
id: 'step2', result: {},
outputs: [ error: undefined,
{
attemptCount: 1,
result: {},
error: undefined,
},
],
}, },
step3: { step3: {
id: 'step3', result: {},
outputs: [ error: undefined,
{
attemptCount: 1,
result: {},
error: undefined,
},
],
}, },
}; };
@ -460,7 +267,7 @@ describe('generateWorkflowRunDiagram', () => {
"edges": [ "edges": [
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-6", "id": "8f3b2121-f194-4ba4-9fbf-3",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -470,7 +277,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-7", "id": "8f3b2121-f194-4ba4-9fbf-4",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -480,7 +287,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-8", "id": "8f3b2121-f194-4ba4-9fbf-5",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -626,7 +433,7 @@ describe('generateWorkflowRunDiagram', () => {
"edges": [ "edges": [
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-9", "id": "8f3b2121-f194-4ba4-9fbf-6",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -636,7 +443,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-10", "id": "8f3b2121-f194-4ba4-9fbf-7",
"markerEnd": "workflow-edge-arrow-rounded", "markerEnd": "workflow-edge-arrow-rounded",
"markerStart": "workflow-edge-gray-circle", "markerStart": "workflow-edge-gray-circle",
"selectable": false, "selectable": false,
@ -645,7 +452,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-11", "id": "8f3b2121-f194-4ba4-9fbf-8",
"markerEnd": "workflow-edge-arrow-rounded", "markerEnd": "workflow-edge-arrow-rounded",
"markerStart": "workflow-edge-gray-circle", "markerStart": "workflow-edge-gray-circle",
"selectable": false, "selectable": false,
@ -801,14 +608,8 @@ describe('generateWorkflowRunDiagram', () => {
]; ];
const stepsOutput: WorkflowRunOutputStepsOutput = { const stepsOutput: WorkflowRunOutputStepsOutput = {
step1: { step1: {
id: 'step1', result: {},
outputs: [ error: undefined,
{
attemptCount: 1,
result: {},
error: undefined,
},
],
}, },
}; };
@ -819,7 +620,7 @@ describe('generateWorkflowRunDiagram', () => {
"edges": [ "edges": [
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-12", "id": "8f3b2121-f194-4ba4-9fbf-9",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -829,7 +630,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-13", "id": "8f3b2121-f194-4ba4-9fbf-10",
"markerEnd": "workflow-edge-green-arrow-rounded", "markerEnd": "workflow-edge-green-arrow-rounded",
"markerStart": "workflow-edge-green-circle", "markerStart": "workflow-edge-green-circle",
"selectable": false, "selectable": false,
@ -839,7 +640,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-14", "id": "8f3b2121-f194-4ba4-9fbf-11",
"markerEnd": "workflow-edge-arrow-rounded", "markerEnd": "workflow-edge-arrow-rounded",
"markerStart": "workflow-edge-gray-circle", "markerStart": "workflow-edge-gray-circle",
"selectable": false, "selectable": false,
@ -848,7 +649,7 @@ describe('generateWorkflowRunDiagram', () => {
}, },
{ {
"deletable": false, "deletable": false,
"id": "8f3b2121-f194-4ba4-9fbf-15", "id": "8f3b2121-f194-4ba4-9fbf-12",
"markerEnd": "workflow-edge-arrow-rounded", "markerEnd": "workflow-edge-arrow-rounded",
"markerStart": "workflow-edge-gray-circle", "markerStart": "workflow-edge-gray-circle",
"selectable": false, "selectable": false,

View File

@ -86,12 +86,7 @@ export const generateWorkflowRunDiagram = ({
} else if (!isDefined(runResult)) { } else if (!isDefined(runResult)) {
runStatus = 'running'; runStatus = 'running';
} else { } else {
const lastAttempt = runResult.outputs.at(-1); if (isDefined(runResult.error)) {
if (!isDefined(lastAttempt)) {
// Should never happen. Should we throw instead?
runStatus = 'failure';
} else if (isDefined(lastAttempt.error)) {
runStatus = 'failure'; runStatus = 'failure';
} else { } else {
runStatus = 'success'; runStatus = 'success';