Skip to content

Commit 5620df7

Browse files
adhityamamallanAssem-Uber
authored andcommitted
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
1 parent f7a1db7 commit 5620df7

13 files changed

+409
-104
lines changed

src/components/timeline/timeline.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ import VisJSTimeline from 'react-visjs-timeline';
33

44
import type { Props } from './timeline.types';
55

6-
export default function Timeline({ items, height = '400px' }: Props) {
6+
export default function Timeline({
7+
items,
8+
height = '400px',
9+
onClickItem,
10+
}: Props) {
711
return (
812
<VisJSTimeline
913
options={{
1014
height,
1115
verticalScroll: true,
1216
}}
1317
items={items}
18+
clickHandler={({ item }: { item: number | null }) => {
19+
if (item !== null) onClickItem(item);
20+
}}
1421
/>
1522
);
1623
}

src/components/timeline/timeline.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type TimelineItem = {
2+
id: number;
23
start: Date;
34
end?: Date;
45
content: string;
@@ -10,4 +11,5 @@ export type TimelineItem = {
1011
export type Props = {
1112
items: Array<TimelineItem>;
1213
height?: string;
14+
onClickItem: (itemId: number) => void;
1315
};

src/views/workflow-history/workflow-history-timeline-chart/__tests__/workflow-history-timeline-chart.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ function setup({
6464
hasMoreEvents={hasMoreEvents}
6565
fetchMoreEvents={mockFetchMoreEvents}
6666
isFetchingMoreEvents={isFetchingMoreEvents}
67+
selectedEventId={mockActivityEventGroup.events[0].eventId}
68+
onClickEventGroup={jest.fn()}
6769
/>
6870
);
6971

src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/convert-event-group-to-timeline-item.test.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ jest.useFakeTimers().setSystemTime(new Date('2024-09-10'));
1414
describe(convertEventGroupToTimelineItem.name, () => {
1515
it('converts an event group to timeline chart item correctly', () => {
1616
expect(
17-
convertEventGroupToTimelineItem(mockActivityEventGroup, {} as any)
17+
convertEventGroupToTimelineItem({
18+
group: mockActivityEventGroup,
19+
index: 1,
20+
classes: {} as any,
21+
isSelected: false,
22+
})
1823
).toEqual({
24+
id: 1,
1925
className: 'mock-class-name',
2026
content: 'Mock event',
2127
end: new Date('2024-09-07T22:16:20.000Z'),
@@ -27,11 +33,14 @@ describe(convertEventGroupToTimelineItem.name, () => {
2733

2834
it('returns end time as present when the event is ongoing or waiting', () => {
2935
expect(
30-
convertEventGroupToTimelineItem(
31-
{ ...mockActivityEventGroup, timeMs: null, status: 'ONGOING' },
32-
{} as any
33-
)
36+
convertEventGroupToTimelineItem({
37+
group: { ...mockActivityEventGroup, timeMs: null, status: 'ONGOING' },
38+
index: 1,
39+
classes: {} as any,
40+
isSelected: false,
41+
})
3442
).toEqual({
43+
id: 1,
3544
className: 'mock-class-name',
3645
content: 'Mock event',
3746
end: new Date('2024-09-10T00:00:00.000Z'),
@@ -43,15 +52,18 @@ describe(convertEventGroupToTimelineItem.name, () => {
4352

4453
it('returns end time as timer end time when the event is an ongoing timer', () => {
4554
expect(
46-
convertEventGroupToTimelineItem(
47-
{
55+
convertEventGroupToTimelineItem({
56+
group: {
4857
...mockTimerEventGroup,
4958
timeMs: null,
5059
status: 'ONGOING',
5160
},
52-
{} as any
53-
)
61+
index: 1,
62+
classes: {} as any,
63+
isSelected: false,
64+
})
5465
).toEqual({
66+
id: 1,
5567
className: 'mock-class-name',
5668
content: 'Mock event',
5769
end: new Date('2024-09-07T22:32:55.632Z'),

src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/get-class-name-for-event-group.test.ts

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,82 +11,165 @@ import { type cssStyles } from '../../workflow-history-timeline-chart.styles';
1111
import getClassNameForEventGroup from '../get-class-name-for-event-group';
1212

1313
const MOCK_CSS_CLASS_NAMES: ClsObjectFor<typeof cssStyles> = {
14-
timer: 'mockTimer',
15-
timerCompleted: 'mockTimerCompleted',
16-
timerNegative: 'mockTimerNegative',
17-
completed: 'mockCompleted',
18-
ongoing: 'mockOngoing',
19-
negative: 'mockNegative',
20-
waiting: 'mockWaiting',
21-
singleCompleted: 'mockSingleCompleted',
22-
singleNegative: 'mockSingleNegative',
14+
timerWaiting: 'timerWaiting',
15+
timerCompleted: 'timerCompleted',
16+
timerNegative: 'timerNegative',
17+
regularCompleted: 'regularCompleted',
18+
regularOngoing: 'regularOngoing',
19+
regularNegative: 'regularNegative',
20+
regularWaiting: 'regularWaiting',
21+
singleCompleted: 'singleCompleted',
22+
singleNegative: 'singleNegative',
23+
timerWaitingSelected: 'timerWaitingSelected',
24+
timerCompletedSelected: 'timerCompletedSelected',
25+
timerNegativeSelected: 'timerNegativeSelected',
26+
regularCompletedSelected: 'regularCompletedSelected',
27+
regularOngoingSelected: 'regularOngoingSelected',
28+
regularNegativeSelected: 'regularNegativeSelected',
29+
regularWaitingSelected: 'regularWaitingSelected',
30+
singleCompletedSelected: 'singleCompletedSelected',
31+
singleNegativeSelected: 'singleNegativeSelected',
2332
};
2433

2534
describe(getClassNameForEventGroup.name, () => {
2635
const tests: Array<{
2736
groupStatus: WorkflowEventStatus;
2837
kind?: 'event' | 'timer';
38+
isSelected?: boolean;
2939
expectedClass: string;
3040
}> = [
3141
{
3242
groupStatus: 'ONGOING',
33-
expectedClass: 'mockOngoing',
43+
expectedClass: 'regularOngoing',
3444
},
3545
{
3646
groupStatus: 'CANCELED',
37-
expectedClass: 'mockNegative',
47+
expectedClass: 'regularNegative',
3848
},
3949
{
4050
groupStatus: 'COMPLETED',
41-
expectedClass: 'mockCompleted',
51+
expectedClass: 'regularCompleted',
4252
},
4353
{
4454
groupStatus: 'FAILED',
45-
expectedClass: 'mockNegative',
55+
expectedClass: 'regularNegative',
4656
},
4757
{
4858
groupStatus: 'WAITING',
49-
expectedClass: 'mockWaiting',
59+
expectedClass: 'regularWaiting',
5060
},
5161
{
5262
groupStatus: 'ONGOING',
5363
kind: 'timer',
54-
expectedClass: 'mockTimer',
64+
expectedClass: 'timerWaiting',
5565
},
5666
{
5767
groupStatus: 'CANCELED',
5868
kind: 'timer',
59-
expectedClass: 'mockTimerNegative',
69+
expectedClass: 'timerNegative',
6070
},
6171
{
6272
groupStatus: 'COMPLETED',
6373
kind: 'timer',
64-
expectedClass: 'mockTimerCompleted',
74+
expectedClass: 'timerCompleted',
6575
},
6676
{
6777
groupStatus: 'FAILED',
6878
kind: 'timer',
69-
expectedClass: 'mockTimerNegative',
79+
expectedClass: 'timerNegative',
7080
},
7181
{
7282
groupStatus: 'WAITING',
7383
kind: 'timer',
74-
expectedClass: 'mockTimer',
84+
expectedClass: 'timerWaiting',
7585
},
7686
{
7787
groupStatus: 'COMPLETED',
7888
kind: 'event',
79-
expectedClass: 'mockSingleCompleted',
89+
expectedClass: 'singleCompleted',
8090
},
8191
{
8292
groupStatus: 'CANCELED',
8393
kind: 'event',
84-
expectedClass: 'mockSingleNegative',
94+
expectedClass: 'singleNegative',
8595
},
8696
{
8797
groupStatus: 'FAILED',
8898
kind: 'event',
89-
expectedClass: 'mockSingleNegative',
99+
expectedClass: 'singleNegative',
100+
},
101+
{
102+
groupStatus: 'ONGOING',
103+
isSelected: true,
104+
expectedClass: 'regularOngoingSelected',
105+
},
106+
{
107+
groupStatus: 'CANCELED',
108+
isSelected: true,
109+
expectedClass: 'regularNegativeSelected',
110+
},
111+
{
112+
groupStatus: 'COMPLETED',
113+
isSelected: true,
114+
expectedClass: 'regularCompletedSelected',
115+
},
116+
{
117+
groupStatus: 'FAILED',
118+
isSelected: true,
119+
expectedClass: 'regularNegativeSelected',
120+
},
121+
{
122+
groupStatus: 'WAITING',
123+
isSelected: true,
124+
expectedClass: 'regularWaitingSelected',
125+
},
126+
{
127+
groupStatus: 'ONGOING',
128+
kind: 'timer',
129+
isSelected: true,
130+
expectedClass: 'timerWaitingSelected',
131+
},
132+
{
133+
groupStatus: 'CANCELED',
134+
kind: 'timer',
135+
isSelected: true,
136+
expectedClass: 'timerNegativeSelected',
137+
},
138+
{
139+
groupStatus: 'COMPLETED',
140+
kind: 'timer',
141+
isSelected: true,
142+
expectedClass: 'timerCompletedSelected',
143+
},
144+
{
145+
groupStatus: 'FAILED',
146+
kind: 'timer',
147+
isSelected: true,
148+
expectedClass: 'timerNegativeSelected',
149+
},
150+
{
151+
groupStatus: 'WAITING',
152+
kind: 'timer',
153+
isSelected: true,
154+
expectedClass: 'timerWaitingSelected',
155+
},
156+
{
157+
groupStatus: 'COMPLETED',
158+
kind: 'event',
159+
isSelected: true,
160+
expectedClass: 'singleCompletedSelected',
161+
},
162+
{
163+
groupStatus: 'CANCELED',
164+
kind: 'event',
165+
isSelected: true,
166+
expectedClass: 'singleNegativeSelected',
167+
},
168+
{
169+
groupStatus: 'FAILED',
170+
kind: 'event',
171+
isSelected: true,
172+
expectedClass: 'singleNegativeSelected',
90173
},
91174
];
92175

@@ -105,7 +188,8 @@ describe(getClassNameForEventGroup.name, () => {
105188
...group,
106189
status: test.groupStatus,
107190
},
108-
MOCK_CSS_CLASS_NAMES
191+
MOCK_CSS_CLASS_NAMES,
192+
Boolean(test.isSelected)
109193
)
110194
).toEqual(test.expectedClass);
111195
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { type ClsObjectFor } from '@/hooks/use-styletron-classes';
2+
3+
import { type cssStyles } from '../../workflow-history-timeline-chart.styles';
4+
import isValidClassNameKey from '../is-valid-class-name-key';
5+
6+
const MOCK_CSS_CLASS_NAMES: ClsObjectFor<typeof cssStyles> = {
7+
timerWaiting: 'timerWaiting',
8+
timerCompleted: 'timerCompleted',
9+
timerNegative: 'timerNegative',
10+
regularCompleted: 'regularCompleted',
11+
regularOngoing: 'regularOngoing',
12+
regularNegative: 'regularNegative',
13+
regularWaiting: 'regularWaiting',
14+
singleCompleted: 'singleCompleted',
15+
singleNegative: 'singleNegative',
16+
timerWaitingSelected: 'timerWaitingSelected',
17+
timerCompletedSelected: 'timerCompletedSelected',
18+
timerNegativeSelected: 'timerNegativeSelected',
19+
regularCompletedSelected: 'regularCompletedSelected',
20+
regularOngoingSelected: 'regularOngoingSelected',
21+
regularNegativeSelected: 'regularNegativeSelected',
22+
regularWaitingSelected: 'regularWaitingSelected',
23+
singleCompletedSelected: 'singleCompletedSelected',
24+
singleNegativeSelected: 'singleNegativeSelected',
25+
};
26+
27+
describe(isValidClassNameKey.name, () => {
28+
it('returns true for a valid class name', () => {
29+
expect(isValidClassNameKey(MOCK_CSS_CLASS_NAMES, 'timerWaiting')).toEqual(
30+
true
31+
);
32+
});
33+
34+
it('returns false for an ivalid class name', () => {
35+
expect(isValidClassNameKey(MOCK_CSS_CLASS_NAMES, 'invalid')).toEqual(false);
36+
});
37+
});

src/views/workflow-history/workflow-history-timeline-chart/helpers/convert-event-group-to-timeline-item.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ import { type cssStyles } from '../workflow-history-timeline-chart.styles';
88

99
import getClassNameForEventGroup from './get-class-name-for-event-group';
1010

11-
export default function convertEventGroupToTimelineItem(
12-
group: HistoryEventsGroup,
13-
classes: ClsObjectFor<typeof cssStyles>
14-
): TimelineItem | undefined {
11+
export default function convertEventGroupToTimelineItem({
12+
group,
13+
index,
14+
classes,
15+
isSelected,
16+
}: {
17+
group: HistoryEventsGroup;
18+
index: number;
19+
classes: ClsObjectFor<typeof cssStyles>;
20+
isSelected: boolean;
21+
}): TimelineItem | undefined {
1522
if (group.events.length === 0) {
1623
return undefined;
1724
}
@@ -43,11 +50,12 @@ export default function convertEventGroupToTimelineItem(
4350
}
4451

4552
return {
53+
id: index,
4654
start: groupStartDayjs.toDate(),
4755
end: groupEndDayjs.toDate(),
4856
content: group.label,
4957
title: `${group.label}: ${group.timeLabel}`,
5058
type: group.groupType === 'Event' ? 'point' : 'range',
51-
className: getClassNameForEventGroup(group, classes),
59+
className: getClassNameForEventGroup(group, classes, isSelected),
5260
};
5361
}

0 commit comments

Comments
 (0)