From 0763a896becbd9d266669c530d2a452beb2b38e1 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 10 Feb 2025 10:14:27 +0100 Subject: [PATCH] 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 --- src/components/timeline/timeline.tsx | 9 +- src/components/timeline/timeline.types.ts | 2 + .../workflow-history-timeline-chart.test.tsx | 2 + ...nvert-event-group-to-timeline-item.test.ts | 30 ++- .../get-class-name-for-event-group.test.ts | 130 ++++++++++--- .../__tests__/is-valid-class-name-key.test.ts | 37 ++++ .../convert-event-group-to-timeline-item.ts | 18 +- .../helpers/get-class-name-for-event-group.ts | 45 ++++- .../helpers/is-valid-class-name-key.ts | 10 + .../workflow-history-timeline-chart.styles.ts | 175 +++++++++++++----- .../workflow-history-timeline-chart.tsx | 31 ++-- .../workflow-history-timeline-chart.types.ts | 2 + .../workflow-history/workflow-history.tsx | 22 +++ 13 files changed, 409 insertions(+), 104 deletions(-) create mode 100644 src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/is-valid-class-name-key.test.ts create mode 100644 src/views/workflow-history/workflow-history-timeline-chart/helpers/is-valid-class-name-key.ts diff --git a/src/components/timeline/timeline.tsx b/src/components/timeline/timeline.tsx index 197805b6f..1b0e6f835 100644 --- a/src/components/timeline/timeline.tsx +++ b/src/components/timeline/timeline.tsx @@ -3,7 +3,11 @@ import VisJSTimeline from 'react-visjs-timeline'; import type { Props } from './timeline.types'; -export default function Timeline({ items, height = '400px' }: Props) { +export default function Timeline({ + items, + height = '400px', + onClickItem, +}: Props) { return ( { + if (item !== null) onClickItem(item); + }} /> ); } diff --git a/src/components/timeline/timeline.types.ts b/src/components/timeline/timeline.types.ts index 8fa5f8b39..10f6b67e0 100644 --- a/src/components/timeline/timeline.types.ts +++ b/src/components/timeline/timeline.types.ts @@ -1,4 +1,5 @@ export type TimelineItem = { + id: number; start: Date; end?: Date; content: string; @@ -10,4 +11,5 @@ export type TimelineItem = { export type Props = { items: Array; height?: string; + onClickItem: (itemId: number) => void; }; diff --git a/src/views/workflow-history/workflow-history-timeline-chart/__tests__/workflow-history-timeline-chart.test.tsx b/src/views/workflow-history/workflow-history-timeline-chart/__tests__/workflow-history-timeline-chart.test.tsx index 8a085455d..bfaf48489 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/__tests__/workflow-history-timeline-chart.test.tsx +++ b/src/views/workflow-history/workflow-history-timeline-chart/__tests__/workflow-history-timeline-chart.test.tsx @@ -64,6 +64,8 @@ function setup({ hasMoreEvents={hasMoreEvents} fetchMoreEvents={mockFetchMoreEvents} isFetchingMoreEvents={isFetchingMoreEvents} + selectedEventId={mockActivityEventGroup.events[0].eventId} + onClickEventGroup={jest.fn()} /> ); diff --git a/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/convert-event-group-to-timeline-item.test.ts b/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/convert-event-group-to-timeline-item.test.ts index 3c3041ab6..b4c71892b 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/convert-event-group-to-timeline-item.test.ts +++ b/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/convert-event-group-to-timeline-item.test.ts @@ -14,8 +14,14 @@ jest.useFakeTimers().setSystemTime(new Date('2024-09-10')); describe(convertEventGroupToTimelineItem.name, () => { it('converts an event group to timeline chart item correctly', () => { expect( - convertEventGroupToTimelineItem(mockActivityEventGroup, {} as any) + convertEventGroupToTimelineItem({ + group: mockActivityEventGroup, + index: 1, + classes: {} as any, + isSelected: false, + }) ).toEqual({ + id: 1, className: 'mock-class-name', content: 'Mock event', end: new Date('2024-09-07T22:16:20.000Z'), @@ -27,11 +33,14 @@ describe(convertEventGroupToTimelineItem.name, () => { it('returns end time as present when the event is ongoing or waiting', () => { expect( - convertEventGroupToTimelineItem( - { ...mockActivityEventGroup, timeMs: null, status: 'ONGOING' }, - {} as any - ) + convertEventGroupToTimelineItem({ + group: { ...mockActivityEventGroup, timeMs: null, status: 'ONGOING' }, + index: 1, + classes: {} as any, + isSelected: false, + }) ).toEqual({ + id: 1, className: 'mock-class-name', content: 'Mock event', end: new Date('2024-09-10T00:00:00.000Z'), @@ -43,15 +52,18 @@ describe(convertEventGroupToTimelineItem.name, () => { it('returns end time as timer end time when the event is an ongoing timer', () => { expect( - convertEventGroupToTimelineItem( - { + convertEventGroupToTimelineItem({ + group: { ...mockTimerEventGroup, timeMs: null, status: 'ONGOING', }, - {} as any - ) + index: 1, + classes: {} as any, + isSelected: false, + }) ).toEqual({ + id: 1, className: 'mock-class-name', content: 'Mock event', end: new Date('2024-09-07T22:32:55.632Z'), diff --git a/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/get-class-name-for-event-group.test.ts b/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/get-class-name-for-event-group.test.ts index 5f44946f0..c8ced80d1 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/get-class-name-for-event-group.test.ts +++ b/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/get-class-name-for-event-group.test.ts @@ -11,82 +11,165 @@ import { type cssStyles } from '../../workflow-history-timeline-chart.styles'; import getClassNameForEventGroup from '../get-class-name-for-event-group'; const MOCK_CSS_CLASS_NAMES: ClsObjectFor = { - timer: 'mockTimer', - timerCompleted: 'mockTimerCompleted', - timerNegative: 'mockTimerNegative', - completed: 'mockCompleted', - ongoing: 'mockOngoing', - negative: 'mockNegative', - waiting: 'mockWaiting', - singleCompleted: 'mockSingleCompleted', - singleNegative: 'mockSingleNegative', + timerWaiting: 'timerWaiting', + timerCompleted: 'timerCompleted', + timerNegative: 'timerNegative', + regularCompleted: 'regularCompleted', + regularOngoing: 'regularOngoing', + regularNegative: 'regularNegative', + regularWaiting: 'regularWaiting', + singleCompleted: 'singleCompleted', + singleNegative: 'singleNegative', + timerWaitingSelected: 'timerWaitingSelected', + timerCompletedSelected: 'timerCompletedSelected', + timerNegativeSelected: 'timerNegativeSelected', + regularCompletedSelected: 'regularCompletedSelected', + regularOngoingSelected: 'regularOngoingSelected', + regularNegativeSelected: 'regularNegativeSelected', + regularWaitingSelected: 'regularWaitingSelected', + singleCompletedSelected: 'singleCompletedSelected', + singleNegativeSelected: 'singleNegativeSelected', }; describe(getClassNameForEventGroup.name, () => { const tests: Array<{ groupStatus: WorkflowEventStatus; kind?: 'event' | 'timer'; + isSelected?: boolean; expectedClass: string; }> = [ { groupStatus: 'ONGOING', - expectedClass: 'mockOngoing', + expectedClass: 'regularOngoing', }, { groupStatus: 'CANCELED', - expectedClass: 'mockNegative', + expectedClass: 'regularNegative', }, { groupStatus: 'COMPLETED', - expectedClass: 'mockCompleted', + expectedClass: 'regularCompleted', }, { groupStatus: 'FAILED', - expectedClass: 'mockNegative', + expectedClass: 'regularNegative', }, { groupStatus: 'WAITING', - expectedClass: 'mockWaiting', + expectedClass: 'regularWaiting', }, { groupStatus: 'ONGOING', kind: 'timer', - expectedClass: 'mockTimer', + expectedClass: 'timerWaiting', }, { groupStatus: 'CANCELED', kind: 'timer', - expectedClass: 'mockTimerNegative', + expectedClass: 'timerNegative', }, { groupStatus: 'COMPLETED', kind: 'timer', - expectedClass: 'mockTimerCompleted', + expectedClass: 'timerCompleted', }, { groupStatus: 'FAILED', kind: 'timer', - expectedClass: 'mockTimerNegative', + expectedClass: 'timerNegative', }, { groupStatus: 'WAITING', kind: 'timer', - expectedClass: 'mockTimer', + expectedClass: 'timerWaiting', }, { groupStatus: 'COMPLETED', kind: 'event', - expectedClass: 'mockSingleCompleted', + expectedClass: 'singleCompleted', }, { groupStatus: 'CANCELED', kind: 'event', - expectedClass: 'mockSingleNegative', + expectedClass: 'singleNegative', }, { groupStatus: 'FAILED', kind: 'event', - expectedClass: 'mockSingleNegative', + expectedClass: 'singleNegative', + }, + { + groupStatus: 'ONGOING', + isSelected: true, + expectedClass: 'regularOngoingSelected', + }, + { + groupStatus: 'CANCELED', + isSelected: true, + expectedClass: 'regularNegativeSelected', + }, + { + groupStatus: 'COMPLETED', + isSelected: true, + expectedClass: 'regularCompletedSelected', + }, + { + groupStatus: 'FAILED', + isSelected: true, + expectedClass: 'regularNegativeSelected', + }, + { + groupStatus: 'WAITING', + isSelected: true, + expectedClass: 'regularWaitingSelected', + }, + { + groupStatus: 'ONGOING', + kind: 'timer', + isSelected: true, + expectedClass: 'timerWaitingSelected', + }, + { + groupStatus: 'CANCELED', + kind: 'timer', + isSelected: true, + expectedClass: 'timerNegativeSelected', + }, + { + groupStatus: 'COMPLETED', + kind: 'timer', + isSelected: true, + expectedClass: 'timerCompletedSelected', + }, + { + groupStatus: 'FAILED', + kind: 'timer', + isSelected: true, + expectedClass: 'timerNegativeSelected', + }, + { + groupStatus: 'WAITING', + kind: 'timer', + isSelected: true, + expectedClass: 'timerWaitingSelected', + }, + { + groupStatus: 'COMPLETED', + kind: 'event', + isSelected: true, + expectedClass: 'singleCompletedSelected', + }, + { + groupStatus: 'CANCELED', + kind: 'event', + isSelected: true, + expectedClass: 'singleNegativeSelected', + }, + { + groupStatus: 'FAILED', + kind: 'event', + isSelected: true, + expectedClass: 'singleNegativeSelected', }, ]; @@ -105,7 +188,8 @@ describe(getClassNameForEventGroup.name, () => { ...group, status: test.groupStatus, }, - MOCK_CSS_CLASS_NAMES + MOCK_CSS_CLASS_NAMES, + Boolean(test.isSelected) ) ).toEqual(test.expectedClass); }); diff --git a/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/is-valid-class-name-key.test.ts b/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/is-valid-class-name-key.test.ts new file mode 100644 index 000000000..b011539bc --- /dev/null +++ b/src/views/workflow-history/workflow-history-timeline-chart/helpers/__tests__/is-valid-class-name-key.test.ts @@ -0,0 +1,37 @@ +import { type ClsObjectFor } from '@/hooks/use-styletron-classes'; + +import { type cssStyles } from '../../workflow-history-timeline-chart.styles'; +import isValidClassNameKey from '../is-valid-class-name-key'; + +const MOCK_CSS_CLASS_NAMES: ClsObjectFor = { + timerWaiting: 'timerWaiting', + timerCompleted: 'timerCompleted', + timerNegative: 'timerNegative', + regularCompleted: 'regularCompleted', + regularOngoing: 'regularOngoing', + regularNegative: 'regularNegative', + regularWaiting: 'regularWaiting', + singleCompleted: 'singleCompleted', + singleNegative: 'singleNegative', + timerWaitingSelected: 'timerWaitingSelected', + timerCompletedSelected: 'timerCompletedSelected', + timerNegativeSelected: 'timerNegativeSelected', + regularCompletedSelected: 'regularCompletedSelected', + regularOngoingSelected: 'regularOngoingSelected', + regularNegativeSelected: 'regularNegativeSelected', + regularWaitingSelected: 'regularWaitingSelected', + singleCompletedSelected: 'singleCompletedSelected', + singleNegativeSelected: 'singleNegativeSelected', +}; + +describe(isValidClassNameKey.name, () => { + it('returns true for a valid class name', () => { + expect(isValidClassNameKey(MOCK_CSS_CLASS_NAMES, 'timerWaiting')).toEqual( + true + ); + }); + + it('returns false for an ivalid class name', () => { + expect(isValidClassNameKey(MOCK_CSS_CLASS_NAMES, 'invalid')).toEqual(false); + }); +}); diff --git a/src/views/workflow-history/workflow-history-timeline-chart/helpers/convert-event-group-to-timeline-item.ts b/src/views/workflow-history/workflow-history-timeline-chart/helpers/convert-event-group-to-timeline-item.ts index c63379f30..07ff530c3 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/helpers/convert-event-group-to-timeline-item.ts +++ b/src/views/workflow-history/workflow-history-timeline-chart/helpers/convert-event-group-to-timeline-item.ts @@ -8,10 +8,17 @@ import { type cssStyles } from '../workflow-history-timeline-chart.styles'; import getClassNameForEventGroup from './get-class-name-for-event-group'; -export default function convertEventGroupToTimelineItem( - group: HistoryEventsGroup, - classes: ClsObjectFor -): TimelineItem | undefined { +export default function convertEventGroupToTimelineItem({ + group, + index, + classes, + isSelected, +}: { + group: HistoryEventsGroup; + index: number; + classes: ClsObjectFor; + isSelected: boolean; +}): TimelineItem | undefined { if (group.events.length === 0) { return undefined; } @@ -43,11 +50,12 @@ export default function convertEventGroupToTimelineItem( } return { + id: index, start: groupStartDayjs.toDate(), end: groupEndDayjs.toDate(), content: group.label, title: `${group.label}: ${group.timeLabel}`, type: group.groupType === 'Event' ? 'point' : 'range', - className: getClassNameForEventGroup(group, classes), + className: getClassNameForEventGroup(group, classes, isSelected), }; } diff --git a/src/views/workflow-history/workflow-history-timeline-chart/helpers/get-class-name-for-event-group.ts b/src/views/workflow-history/workflow-history-timeline-chart/helpers/get-class-name-for-event-group.ts index b256d3ea9..c5be53da6 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/helpers/get-class-name-for-event-group.ts +++ b/src/views/workflow-history/workflow-history-timeline-chart/helpers/get-class-name-for-event-group.ts @@ -3,39 +3,64 @@ import { type ClsObjectFor } from '@/hooks/use-styletron-classes'; import { type HistoryEventsGroup } from '../../workflow-history.types'; import { type cssStyles } from '../workflow-history-timeline-chart.styles'; +import isValidClassNameKey from './is-valid-class-name-key'; + export default function getClassNameForEventGroup( group: HistoryEventsGroup, - classes: ClsObjectFor + classes: ClsObjectFor, + isSelected: boolean ): string { + let kind: string, color: string; + if (group.groupType === 'Timer') { + kind = 'timer'; switch (group.status) { case 'CANCELED': case 'FAILED': - return classes.timerNegative; + color = 'Negative'; + break; case 'COMPLETED': - return classes.timerCompleted; + color = 'Completed'; + break; default: - return classes.timer; + color = 'Waiting'; + break; } } else if (group.groupType === 'Event') { + kind = 'single'; switch (group.status) { case 'CANCELED': case 'FAILED': - return classes.singleNegative; + color = 'Negative'; + break; default: - return classes.singleCompleted; + color = 'Completed'; + break; } } else { + kind = 'regular'; switch (group.status) { case 'CANCELED': case 'FAILED': - return classes.negative; + color = 'Negative'; + break; case 'COMPLETED': - return classes.completed; + color = 'Completed'; + break; case 'WAITING': - return classes.waiting; + color = 'Waiting'; + break; default: - return classes.ongoing; + color = 'Ongoing'; + break; } } + + const classNameKey = `${kind}${color}${isSelected ? 'Selected' : ''}`; + + if (isValidClassNameKey(classes, classNameKey)) { + return classes[classNameKey]; + } + + return classes.regularWaiting; } diff --git a/src/views/workflow-history/workflow-history-timeline-chart/helpers/is-valid-class-name-key.ts b/src/views/workflow-history/workflow-history-timeline-chart/helpers/is-valid-class-name-key.ts new file mode 100644 index 000000000..97fb7eea7 --- /dev/null +++ b/src/views/workflow-history/workflow-history-timeline-chart/helpers/is-valid-class-name-key.ts @@ -0,0 +1,10 @@ +import { type ClsObjectFor } from '@/hooks/use-styletron-classes'; + +import { type cssStyles } from '../workflow-history-timeline-chart.styles'; + +export default function isValidClassNameKey( + classes: ClsObjectFor, + key: string +): key is keyof ClsObjectFor { + return Object.hasOwn(classes, key); +} diff --git a/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.styles.ts b/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.styles.ts index 5b7429f0f..df5ab816f 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.styles.ts +++ b/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.styles.ts @@ -8,59 +8,146 @@ import type { StyletronCSSObjectOf, } from '@/hooks/use-styletron-classes'; +// Colours +const waiting = (theme: Theme) => ({ + backgroundColor: `${theme.colors.backgroundSecondary} !important`, + borderColor: `${theme.colors.contentPrimary} !important`, +}); + +const completed = (_: Theme) => ({ + backgroundColor: '#D3EFDA !important', + borderColor: '#009A51 !important', +}); + +const negative = (theme: Theme) => ({ + backgroundColor: '#FFE1DE !important', + borderColor: `${theme.colors.negative} !important`, +}); + +const ongoing = (theme: Theme) => ({ + backgroundColor: '#DEE9FE !important', + borderColor: `${theme.colors.accent} !important`, +}); + +// Items +const timer = (_: Theme) => ({ + paddingInline: '2px', + borderWidth: '0 2px !important', + borderRadius: '0 !important', +}); + +const regular = (theme: Theme) => ({ + color: `${theme.colors.contentPrimary} !important`, + paddingInline: '2px', + borderRadius: `${theme.sizing.scale300} !important`, +}); + +const single = (theme: Theme) => ({ + color: `${theme.colors.contentPrimary} !important`, + paddingInline: '2px', + borderRadius: theme.sizing.scale300, +}); + +const selected = ( + theme: Theme, + color: (theme: Theme) => { backgroundColor: string; borderColor: string }, + kind: 'regular' | 'single' | 'timer' = 'regular' +) => { + const unselectedStyle = color(theme); + + let borderColor: string; + switch (kind) { + case 'single': + borderColor = `${theme.colors.contentInversePrimary} !important`; + break; + case 'timer': + case 'regular': + default: + borderColor = unselectedStyle.borderColor; + break; + } + + return { + color: `${theme.colors.contentInversePrimary} !important`, + borderColor, + backgroundColor: unselectedStyle.borderColor, + }; +}; + const cssStylesObj = { - timer: (theme) => ({ - paddingInline: '2px', - borderWidth: '0 2px !important', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: `${theme.colors.backgroundSecondary} !important`, - borderColor: `${theme.colors.contentPrimary} !important`, + timerWaiting: (theme) => ({ + ...timer(theme), + ...waiting(theme), }), timerCompleted: (theme) => ({ - paddingInline: '2px', - borderWidth: '0 2px !important', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: '#D3EFDA !important', - borderColor: '#009A51 !important', + ...timer(theme), + ...completed(theme), }), timerNegative: (theme) => ({ - paddingInline: '2px', - borderWidth: '0 2px !important', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: '#FFE1DE !important', - borderColor: `${theme.colors.negative} !important`, - }), - completed: (theme) => ({ - paddingInline: '2px', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: '#D3EFDA !important', - borderColor: '#009A51 !important', - }), - ongoing: (theme) => ({ - paddingInline: '2px', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: '#DEE9FE !important', - borderColor: `${theme.colors.accent} !important`, - }), - negative: (theme) => ({ - paddingInline: '2px', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: '#FFE1DE !important', - borderColor: `${theme.colors.negative} !important`, - }), - waiting: (theme) => ({ - paddingInline: '2px', - color: `${theme.colors.contentPrimary} !important`, - backgroundColor: `${theme.colors.backgroundSecondary} !important`, - borderColor: `${theme.colors.contentPrimary} !important`, + ...timer(theme), + ...negative(theme), + }), + regularCompleted: (theme) => ({ + ...regular(theme), + ...completed(theme), + }), + regularOngoing: (theme) => ({ + ...regular(theme), + ...ongoing(theme), + }), + regularNegative: (theme) => ({ + ...regular(theme), + ...negative(theme), + }), + regularWaiting: (theme) => ({ + ...regular(theme), + ...waiting(theme), }), singleCompleted: (theme) => ({ - color: `${theme.colors.contentPrimary} !important`, - borderColor: '#009A51 !important', + ...single(theme), + ...completed(theme), + backgroundColor: 'unset', }), singleNegative: (theme) => ({ - color: `${theme.colors.contentPrimary} !important`, - borderColor: `${theme.colors.negative} !important`, + ...single(theme), + ...negative(theme), + backgroundColor: 'unset', + }), + timerWaitingSelected: (theme) => ({ + ...timer(theme), + ...selected(theme, waiting, 'timer'), + }), + timerCompletedSelected: (theme) => ({ + ...timer(theme), + ...selected(theme, completed, 'timer'), + }), + timerNegativeSelected: (theme) => ({ + ...timer(theme), + ...selected(theme, negative, 'timer'), + }), + regularCompletedSelected: (theme) => ({ + ...regular(theme), + ...selected(theme, completed), + }), + regularOngoingSelected: (theme) => ({ + ...regular(theme), + ...selected(theme, ongoing), + }), + regularNegativeSelected: (theme) => ({ + ...regular(theme), + ...selected(theme, negative), + }), + regularWaitingSelected: (theme) => ({ + ...regular(theme), + ...selected(theme, waiting), + }), + singleCompletedSelected: (theme) => ({ + ...single(theme), + ...selected(theme, completed, 'single'), + }), + singleNegativeSelected: (theme) => ({ + ...single(theme), + ...selected(theme, negative, 'single'), }), } satisfies StyletronCSSObject; diff --git a/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.tsx b/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.tsx index 03d8ae5b0..3cd80afba 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.tsx +++ b/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.tsx @@ -21,10 +21,12 @@ const Timeline = dynamic(() => import('@/components/timeline/timeline'), { export default function WorkflowHistoryTimelineChart({ eventGroupsEntries, + selectedEventId, isLoading, hasMoreEvents, fetchMoreEvents, isFetchingMoreEvents, + onClickEventGroup, }: Props) { const { cls } = useStyletronClasses(cssStyles); @@ -34,19 +36,24 @@ export default function WorkflowHistoryTimelineChart({ const timelineItems = useMemo( () => - eventGroupsEntries.reduce((items: Array, [_, group]) => { - const timelineChartItem = convertEventGroupToTimelineChartItem( - group, - cls - ); + eventGroupsEntries.reduce( + (items: Array, [_, group], index) => { + const timelineChartItem = convertEventGroupToTimelineChartItem({ + group, + index, + classes: cls, + isSelected: group.events[0].eventId === selectedEventId, + }); - if (timelineChartItem) { - items.push(timelineChartItem); - } + if (timelineChartItem) { + items.push(timelineChartItem); + } - return items; - }, []), - [eventGroupsEntries, cls] + return items; + }, + [] + ), + [eventGroupsEntries, cls, selectedEventId] ); return ( @@ -64,7 +71,7 @@ export default function WorkflowHistoryTimelineChart({ )} - + ); } diff --git a/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.types.ts b/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.types.ts index 6cd224221..cd7bd6a0e 100644 --- a/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.types.ts +++ b/src/views/workflow-history/workflow-history-timeline-chart/workflow-history-timeline-chart.types.ts @@ -2,8 +2,10 @@ import { type HistoryEventsGroup } from '../workflow-history.types'; export type Props = { eventGroupsEntries: Array<[string, HistoryEventsGroup]>; + selectedEventId: string | undefined; isLoading: boolean; hasMoreEvents: boolean; fetchMoreEvents: () => void; isFetchingMoreEvents: boolean; + onClickEventGroup: (eventGroupIndex: number) => void; }; diff --git a/src/views/workflow-history/workflow-history.tsx b/src/views/workflow-history/workflow-history.tsx index 857a72e89..b22d5a630 100644 --- a/src/views/workflow-history/workflow-history.tsx +++ b/src/views/workflow-history/workflow-history.tsx @@ -181,6 +181,7 @@ export default function WorkflowHistory({ params }: Props) { const [isTimelineChartShown, setIsTimelineChartShown] = useState(false); + const compactSectionListRef = useRef(null); const timelineSectionListRef = useRef(null); if (contentIsLoading) { @@ -224,6 +225,7 @@ export default function WorkflowHistory({ params }: Props) { {typeof window !== 'undefined' && isTimelineChartShown && ( { + setQueryParams({ + historySelectedEventId: + filteredEventGroupsEntries[eventGroupIndex][1].events[0] + .eventId, + }); + + compactSectionListRef.current?.scrollToIndex({ + index: eventGroupIndex, + align: 'start', + behavior: 'smooth', + }); + + timelineSectionListRef.current?.scrollToIndex({ + index: eventGroupIndex, + align: 'start', + behavior: 'smooth', + }); + }} /> )} {filteredEventGroupsEntries.length > 0 && ( @@ -240,6 +261,7 @@ export default function WorkflowHistory({ params }: Props) {