Skip to content

Commit 00ede49

Browse files
Show pending events in history timeline (#813)
* Show pending events in history timeline * rename variables * change function to arrow function * Allow selecting items in Workflow History Timeline (#812) Add onClickItem handler to Timeline that gets called when the VisJS Timeline registers a click on an item Add "id" number to TimelineItem Set selected event in query params and scroll to it when an item is selected in the timeline Refactor Workflow Timeline Styles to share common styles across different items and states Added rounding to non-timer items Removed rounding from timer items * fix selection type * fix ling * fix typos --------- Co-authored-by: Adhitya Mamallan <[email protected]>
1 parent 5620df7 commit 00ede49

File tree

45 files changed

+1081
-174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1081
-174
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { ZodError } from 'zod';
2+
3+
import logger from '@/utils/logger';
4+
import {
5+
pendingActivityTaskStartEvent,
6+
pendingDecisionTaskScheduleEvent,
7+
} from '@/views/workflow-history/__fixtures__/workflow-history-pending-events';
8+
9+
import formatPendingWorkflowHistoryEvent from '..';
10+
11+
jest.mock('@/utils/logger');
12+
13+
const pendingEvents = [
14+
pendingActivityTaskStartEvent,
15+
pendingDecisionTaskScheduleEvent,
16+
];
17+
describe('formatWorkflowHistoryEvent', () => {
18+
pendingEvents.forEach((event) => {
19+
it(`should format workflow ${event.attributes} to match snapshot`, () => {
20+
expect(formatPendingWorkflowHistoryEvent(event)).toMatchSnapshot();
21+
});
22+
});
23+
it(`should log error if parsing failed`, () => {
24+
expect(
25+
//@ts-expect-error pass event with missing fields
26+
formatPendingWorkflowHistoryEvent({
27+
attributes: 'pendingActivityTaskStartEventAttributes',
28+
})
29+
).toBe(null);
30+
expect(logger.warn).toHaveBeenCalledWith(
31+
{ cause: expect.any(ZodError) },
32+
'Failed to format workflow pending event'
33+
);
34+
});
35+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`formatWorkflowHistoryEvent should format workflow pendingActivityTaskStartEventAttributes to match snapshot 1`] = `
4+
{
5+
"activityId": "0",
6+
"activityType": {
7+
"name": "activity.cron.Start",
8+
},
9+
"attempt": 1,
10+
"eventId": null,
11+
"eventTime": 2024-09-07T22:16:10.599Z,
12+
"eventType": "PendingActivityTaskStart",
13+
"expirationTime": 1970-01-01T00:06:00.000Z,
14+
"heartbeatDetails": [
15+
"1725747370575409843",
16+
"gadence-canary-xdc",
17+
"workflow.sanity",
18+
],
19+
"lastFailure": {
20+
"details": "",
21+
"reason": "",
22+
},
23+
"lastFailureDetails": null,
24+
"lastFailureReason": "",
25+
"lastHeartbeatTime": null,
26+
"lastStartedTime": null,
27+
"lastWorkerIdentity": "",
28+
"maximumAttempts": 10,
29+
"scheduleId": 7,
30+
"scheduledTime": 1970-01-01T00:03:00.000Z,
31+
"startedWorkerIdentity": "",
32+
}
33+
`;
34+
35+
exports[`formatWorkflowHistoryEvent should format workflow pendingDecisionTaskScheduleEventAttributes to match snapshot 1`] = `
36+
{
37+
"attempt": 1,
38+
"eventId": 2,
39+
"eventTime": 2024-09-07T22:16:10.599Z,
40+
"eventType": "PendingDecisionTaskSchedule",
41+
"originalScheduledTime": null,
42+
"scheduleId": 7,
43+
"scheduledTime": 1970-01-01T00:03:00.000Z,
44+
"startedTime": null,
45+
}
46+
`;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import omit from 'lodash/omit';
2+
3+
import { type PendingActivityTaskStartEvent } from '@/views/workflow-history/workflow-history.types';
4+
5+
import formatFailureDetails from '../format-failure-details';
6+
import formatPayload from '../format-payload';
7+
import formatTimestampToDatetime from '../format-timestamp-to-datetime';
8+
9+
export default function formatPendingActivityTaskStartEvent({
10+
pendingActivityTaskStartEventAttributes: pendingInfo,
11+
eventTime,
12+
eventId,
13+
}: PendingActivityTaskStartEvent) {
14+
return {
15+
...omit(pendingInfo, 'state'),
16+
eventId,
17+
eventTime: formatTimestampToDatetime(eventTime),
18+
eventType: 'PendingActivityTaskStart',
19+
20+
scheduleId: parseInt(pendingInfo.scheduleId),
21+
lastHeartbeatTime: formatTimestampToDatetime(pendingInfo.lastHeartbeatTime),
22+
lastStartedTime: formatTimestampToDatetime(pendingInfo.lastStartedTime),
23+
scheduledTime: formatTimestampToDatetime(pendingInfo.scheduledTime),
24+
expirationTime: formatTimestampToDatetime(pendingInfo.expirationTime),
25+
heartbeatDetails: formatPayload(pendingInfo.heartbeatDetails),
26+
lastFailureDetails: formatFailureDetails(pendingInfo.lastFailure),
27+
lastFailureReason: pendingInfo.lastFailure?.reason,
28+
};
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import omit from 'lodash/omit';
2+
3+
import { type PendingDecisionTaskScheduleEvent } from '@/views/workflow-history/workflow-history.types';
4+
5+
import formatTimestampToDatetime from '../format-timestamp-to-datetime';
6+
7+
const formatPendingDecisionTaskScheduleEvent = ({
8+
pendingDecisionTaskScheduleEventAttributes: pendingInfo,
9+
eventTime,
10+
eventId,
11+
}: PendingDecisionTaskScheduleEvent) => {
12+
return {
13+
...omit(pendingInfo, 'state'),
14+
eventId: parseInt(eventId),
15+
eventType: 'PendingDecisionTaskSchedule',
16+
eventTime: formatTimestampToDatetime(eventTime),
17+
18+
scheduleId: parseInt(pendingInfo.scheduleId),
19+
scheduledTime: formatTimestampToDatetime(pendingInfo.scheduledTime),
20+
startedTime: formatTimestampToDatetime(pendingInfo.startedTime),
21+
originalScheduledTime: formatTimestampToDatetime(
22+
pendingInfo.originalScheduledTime
23+
),
24+
};
25+
};
26+
27+
export default formatPendingDecisionTaskScheduleEvent;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import logger from '@/utils/logger';
2+
import { type PendingHistoryEvent } from '@/views/workflow-history/workflow-history.types';
3+
4+
import {
5+
getFormatPendingEventSchema,
6+
type FormattedHistoryPendingEvent,
7+
} from '../schema/format-history-pending-event-schema';
8+
9+
export default function formatPendingWorkflowHistoryEvent(
10+
event: PendingHistoryEvent
11+
): FormattedHistoryPendingEvent | null {
12+
const schema = getFormatPendingEventSchema(event);
13+
if (schema) {
14+
const { data, error } = schema.safeParse(event);
15+
if (error) {
16+
logger.warn({ cause: error }, 'Failed to format workflow pending event');
17+
return null;
18+
}
19+
return data ?? null;
20+
}
21+
return null;
22+
}

src/utils/data-formatters/schema/format-history-event-schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ export const formatUpsertWorkflowSearchAttributesEventSchema =
272272
function unExistingEventType(_: never) {
273273
return null;
274274
}
275-
export const getFormatHistoryEventSchema = function (event: HistoryEvent) {
275+
export const getFormatHistoryEventSchema = (event: HistoryEvent) => {
276276
switch (event.attributes) {
277277
case 'workflowExecutionStartedEventAttributes':
278278
return formatWorkflowExecutionStartedEventSchema;
@@ -378,7 +378,7 @@ export const getFormatHistoryEventSchema = function (event: HistoryEvent) {
378378
return formatUpsertWorkflowSearchAttributesEventSchema;
379379

380380
default:
381-
return unExistingEventType(event.attributes); // should not be unreachable, used to show a type error if not all attributes cases are covered
381+
return unExistingEventType(event.attributes); // used to show a type error if any attributes cases are covered
382382
}
383383
};
384384

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { type z } from 'zod';
2+
3+
import { type PendingHistoryEvent } from '@/views/workflow-history/workflow-history.types';
4+
5+
import formatPendingActivityTaskStartEvent from '../format-pending-workflow-history-event/format-pending-activity-start-event';
6+
import formatPendingDecisionTaskScheduleEvent from '../format-pending-workflow-history-event/format-pending-decision-schedule-event';
7+
8+
import {
9+
pendingActivityTaskStartSchema,
10+
pendingDecisionTaskScheduleSchema,
11+
} from './pending-history-event-schema';
12+
13+
export const formatPendingActivityTaskStartEventSchema =
14+
pendingActivityTaskStartSchema.transform(formatPendingActivityTaskStartEvent);
15+
16+
export const formatPendingDecisionTaskScheduleEventSchema =
17+
pendingDecisionTaskScheduleSchema.transform(
18+
formatPendingDecisionTaskScheduleEvent
19+
);
20+
21+
function unExistingEventType(_: never) {
22+
return null;
23+
}
24+
export const getFormatPendingEventSchema = (event: PendingHistoryEvent) => {
25+
switch (event.attributes) {
26+
case 'pendingActivityTaskStartEventAttributes':
27+
return formatPendingActivityTaskStartEventSchema;
28+
case 'pendingDecisionTaskScheduleEventAttributes':
29+
return formatPendingDecisionTaskScheduleEventSchema;
30+
default:
31+
return unExistingEventType(event); // used to show a type error if any pending event attributes cases are not covered
32+
}
33+
};
34+
35+
export type FormattedPendingActivityTaskStartEvent = z.infer<
36+
typeof formatPendingActivityTaskStartEventSchema
37+
>;
38+
export type FormattedPendingDecisionTaskScheduleEvent = z.infer<
39+
typeof formatPendingDecisionTaskScheduleEventSchema
40+
>;
41+
42+
export type FormattedHistoryPendingEvent =
43+
| FormattedPendingActivityTaskStartEvent
44+
| FormattedPendingDecisionTaskScheduleEvent;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { z } from 'zod';
2+
3+
import { PendingActivityState } from '@/__generated__/proto-ts/uber/cadence/api/v1/PendingActivityState';
4+
import { PendingDecisionState } from '@/__generated__/proto-ts/uber/cadence/api/v1/PendingDecisionState';
5+
6+
const timestampSchema = z.object({
7+
seconds: z.string(),
8+
nanos: z.number(),
9+
});
10+
11+
const payloadSchema = z.object({
12+
data: z.string(),
13+
});
14+
15+
const activityTypeSchema = z.object({
16+
name: z.string(),
17+
});
18+
19+
const failureSchema = z.object({
20+
reason: z.string(),
21+
details: z.string(),
22+
});
23+
24+
export const pendingActivityTaskStartSchema = z.object({
25+
attributes: z.literal('pendingActivityTaskStartEventAttributes'),
26+
eventTime: timestampSchema.nullable(),
27+
eventId: z.null(),
28+
computedEventId: z.string(),
29+
pendingActivityTaskStartEventAttributes: z.object({
30+
activityId: z.string(),
31+
activityType: activityTypeSchema.nullable(),
32+
state: z.literal(PendingActivityState.PENDING_ACTIVITY_STATE_STARTED),
33+
heartbeatDetails: payloadSchema.nullable(),
34+
lastHeartbeatTime: timestampSchema.nullable(),
35+
lastStartedTime: timestampSchema.nullable(),
36+
scheduledTime: timestampSchema.nullable(),
37+
expirationTime: timestampSchema.nullable(),
38+
attempt: z.number(),
39+
maximumAttempts: z.number(),
40+
lastFailure: failureSchema.nullable(),
41+
lastWorkerIdentity: z.string(),
42+
startedWorkerIdentity: z.string(),
43+
scheduleId: z.string(),
44+
}),
45+
});
46+
47+
export const pendingDecisionTaskScheduleSchema = z.object({
48+
attributes: z.literal('pendingDecisionTaskScheduleEventAttributes'),
49+
eventTime: timestampSchema.nullable(),
50+
eventId: z.string(),
51+
pendingDecisionTaskScheduleEventAttributes: z.object({
52+
state: z.literal(PendingDecisionState.PENDING_DECISION_STATE_SCHEDULED),
53+
scheduledTime: timestampSchema.nullable(),
54+
startedTime: timestampSchema.nullable(),
55+
attempt: z.number(),
56+
originalScheduledTime: timestampSchema.nullable(),
57+
scheduleId: z.string(),
58+
}),
59+
});

src/views/workflow-history/__fixtures__/all-workflow-event-types-attributes.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
import { type HistoryEvent } from '@/__generated__/proto-ts/uber/cadence/api/v1/HistoryEvent';
22

3+
import { type ExtendedHistoryEvent } from '../workflow-history.types';
4+
35
// validates that each all attributes exists in the provided array
46
const arrayOfAllAttrs = <T extends HistoryEvent['attributes'][]>(
57
array: T &
68
([HistoryEvent['attributes']] extends [T[number]] ? unknown : 'Invalid')
79
) => array;
810

11+
// validates that each all attributes exists in the provided array
12+
const arrayOfAllAttrsExtended = <
13+
T extends ExtendedHistoryEvent['attributes'][],
14+
>(
15+
array: T &
16+
([ExtendedHistoryEvent['attributes']] extends [T[number]]
17+
? unknown
18+
: 'Invalid')
19+
) => array;
20+
921
const allAttrsArr = arrayOfAllAttrs([
1022
'activityTaskScheduledEventAttributes',
1123
'activityTaskStartedEventAttributes',
@@ -51,5 +63,16 @@ const allAttrsArr = arrayOfAllAttrs([
5163
'upsertWorkflowSearchAttributesEventAttributes',
5264
]);
5365

66+
const allAttrsArrExtended = arrayOfAllAttrsExtended([
67+
...allAttrsArr,
68+
'pendingActivityTaskStartEventAttributes',
69+
'pendingDecisionTaskScheduleEventAttributes',
70+
]);
71+
5472
export const allWorkflowEventTypesAttrs: Pick<HistoryEvent, 'attributes'>[] =
5573
allAttrsArr.map((v) => ({ attributes: v }));
74+
75+
export const allWorkflowEventTypesAttrsExtended: Pick<
76+
ExtendedHistoryEvent,
77+
'attributes'
78+
>[] = allAttrsArrExtended.map((v) => ({ attributes: v }));

src/views/workflow-history/__fixtures__/all-workflow-event-types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import {
2323
startDecisionTaskEvent,
2424
timeoutDecisionTaskEvent,
2525
} from './workflow-history-decision-events';
26+
import {
27+
pendingDecisionTaskScheduleEvent,
28+
pendingActivityTaskStartEvent,
29+
} from './workflow-history-pending-events';
2630
import {
2731
initiateRequestCancelExternalWorkflowEvent,
2832
requestCancelExternalWorkflowEvent,
@@ -99,3 +103,9 @@ export const allWorkflowEvents = [
99103
timeoutWorkflowExecutionEvent,
100104
upsertWorkflowSearchAttributesEvent,
101105
];
106+
107+
export const allWorkflowEventsExtended = [
108+
...allWorkflowEvents,
109+
pendingDecisionTaskScheduleEvent,
110+
pendingActivityTaskStartEvent,
111+
];

0 commit comments

Comments
 (0)