From d931f2ce3a965548ac8a01f6dcd2450f1adb98c4 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 2 Jan 2026 14:39:28 +0100 Subject: [PATCH 01/11] initial implementation of scroll to event Signed-off-by: Adhitya Mamallan --- .../get-navigation-bar-events-menu-items.ts | 28 +++++ ...story-navigation-bar-events-menu.styles.ts | 59 +++++++++ ...low-history-navigation-bar-events-menu.tsx | 77 ++++++++++++ .../workflow-history-navigation-bar.tsx | 46 +++++++ .../workflow-history-navigation-bar.types.ts | 12 ++ .../workflow-history-v2.tsx | 116 ++++++++++++++++-- .../workflow-history-v2.types.ts | 3 + 7 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts create mode 100644 src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx diff --git a/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts b/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts new file mode 100644 index 000000000..f7f47ec42 --- /dev/null +++ b/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts @@ -0,0 +1,28 @@ +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; + +import getEventGroupFilteringType from '../workflow-history-event-group/helpers/get-event-group-filtering-type'; +import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar/workflow-history-navigation-bar.types'; +import { type EventGroupEntry } from '../workflow-history-v2.types'; + +export default function getNavigationBarEventsMenuItems( + eventGroupsEntries: Array, + filterFn: (group: HistoryEventsGroup) => boolean +): Array { + return eventGroupsEntries.reduce>( + (acc, [_, group]) => { + const lastEventId = group.events.at(-1)?.eventId; + if (!lastEventId) return acc; + + if (!filterFn(group)) return acc; + + acc.push({ + eventId: lastEventId, + label: group.shortLabel ?? group.label, + type: getEventGroupFilteringType(group), + }); + + return acc; + }, + [] + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts new file mode 100644 index 000000000..057247b46 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts @@ -0,0 +1,59 @@ +import { styled as createStyled, type Theme } from 'baseui'; +import { type ButtonOverrides } from 'baseui/button'; +import { type PaginationOverrides } from 'baseui/pagination'; +import { type StyleObject } from 'styletron-react'; + +export const styled = { + MenuItemsContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + padding: $theme.sizing.scale200, + backgroundColor: $theme.colors.backgroundPrimary, + borderRadius: $theme.borders.radius400, + })), + MenuItemContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + gap: $theme.sizing.scale500, + alignItems: 'flex-start', + })), + PaginationContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + justifyContent: 'center', + marginTop: $theme.sizing.scale300, + paddingTop: $theme.sizing.scale300, + borderTop: `1px solid ${$theme.colors.borderOpaque}`, + })), +}; + +export const overrides = { + navActionButton: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + paddingTop: $theme.sizing.scale200, + paddingBottom: $theme.sizing.scale200, + paddingLeft: $theme.sizing.scale200, + paddingRight: $theme.sizing.scale200, + }), + }, + } satisfies ButtonOverrides, + pagination: { + Select: { + props: { + overrides: { + SingleValue: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.ParagraphXSmall, + }), + }, + }, + }, + }, + MaxLabel: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.ParagraphXSmall, + marginLeft: $theme.sizing.scale200, + }), + }, + } satisfies PaginationOverrides, +}; diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx new file mode 100644 index 000000000..ab09b0a47 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx @@ -0,0 +1,77 @@ +import { useState, useMemo } from 'react'; + +import { Button } from 'baseui/button'; +import { Pagination } from 'baseui/pagination'; +import { StatefulPopover } from 'baseui/popover'; + +import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar/workflow-history-navigation-bar.types'; + +import { + styled, + overrides, +} from './workflow-history-navigation-bar-events-menu.styles'; + +const ITEMS_PER_PAGE = 10; + +export default function WorkflowHistoryNavigationBarEventsMenu({ + children, + isUngroupedHistoryView, + menuItems, + onClickEvent, +}: { + children: React.ReactNode; + isUngroupedHistoryView: boolean; + menuItems: Array; + onClickEvent: (eventId: string) => void; +}) { + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(menuItems.length / ITEMS_PER_PAGE); + + const paginatedItems = useMemo(() => { + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + const endIndex = startIndex + ITEMS_PER_PAGE; + return menuItems.slice(startIndex, endIndex); + }, [menuItems, currentPage]); + + return ( + ( + + {paginatedItems.map(({ eventId, label }) => ( + + + + ))} + {totalPages > 1 && ( + + { + setCurrentPage(nextPage); + }} + size="mini" + overrides={overrides.pagination} + /> + + )} + + )} + autoFocus={false} + placement="auto" + accessibilityType="menu" + > + {children} + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx index adc4c2f20..3d9250099 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx @@ -7,6 +7,8 @@ import { import Button from '@/components/button/button'; +import WorkflowHistoryNavigationBarEventsMenu from '../workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu'; + import { styled, overrides } from './workflow-history-navigation-bar.styles'; import { type Props } from './workflow-history-navigation-bar.types'; @@ -15,6 +17,10 @@ export default function WorkflowHistoryNavigationBar({ onScrollDown, areAllItemsExpanded, onToggleAllItemsExpanded, + isUngroupedView, + failedEventsMenuItems, + pendingEventsMenuItems, + onClickEvent, }: Props) { return ( @@ -53,6 +59,46 @@ export default function WorkflowHistoryNavigationBar({ > + {failedEventsMenuItems.length > 0 && ( + <> + + + + + + )} + {pendingEventsMenuItems.length > 0 && ( + <> + + + + + + )} ); } diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts index 2e98f9f30..13d2421d9 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts @@ -1,6 +1,18 @@ +import { type WorkflowHistoryEventFilteringType } from '@/views/workflow-history/workflow-history-filters-type/workflow-history-filters-type.types'; + export type Props = { onScrollUp: () => void; onScrollDown: () => void; areAllItemsExpanded: boolean; onToggleAllItemsExpanded: () => void; + isUngroupedView: boolean; + failedEventsMenuItems: Array; + pendingEventsMenuItems: Array; + onClickEvent: (eventId: string) => void; +}; + +export type NavigationBarEventsMenuItem = { + type: WorkflowHistoryEventFilteringType; + eventId: string; + label: string; }; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index c1f4d0e17..abff3b06b 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -27,6 +27,7 @@ import useInitialSelectedEvent from '../workflow-history/hooks/use-initial-selec import useWorkflowHistoryFetcher from '../workflow-history/hooks/use-workflow-history-fetcher'; import useWorkflowHistoryGrouper from '../workflow-history/hooks/use-workflow-history-grouper'; import { WorkflowHistoryContext } from '../workflow-history/workflow-history-context-provider/workflow-history-context-provider'; +import filterGroupsByGroupStatus from '../workflow-history/workflow-history-filters-status/helpers/filter-groups-by-group-status'; import workflowPageQueryParamsConfig from '../workflow-page/config/workflow-page-query-params.config'; import { useSuspenseDescribeWorkflow } from '../workflow-page/hooks/use-describe-workflow'; import { type WorkflowPageTabContentParams } from '../workflow-page/workflow-page-tab-content/workflow-page-tab-content.types'; @@ -35,6 +36,7 @@ import WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG from './config/workflow- import workflowHistoryFiltersConfig from './config/workflow-history-filters.config'; import WORKFLOW_HISTORY_RENDER_FETCHED_EVENTS_THROTTLE_MS_CONFIG from './config/workflow-history-render-fetched-events-throttle-ms.config'; import WORKFLOW_HISTORY_SET_RANGE_THROTTLE_MS_CONFIG from './config/workflow-history-set-range-throttle-ms.config'; +import getNavigationBarEventsMenuItems from './helpers/get-navigation-bar-events-menu-items'; import WorkflowHistoryGroupedTable from './workflow-history-grouped-table/workflow-history-grouped-table'; import WorkflowHistoryHeader from './workflow-history-header/workflow-history-header'; import WorkflowHistoryNavigationBar from './workflow-history-navigation-bar/workflow-history-navigation-bar'; @@ -144,13 +146,19 @@ export default function WorkflowHistoryV2({ params }: Props) { }); }, [wfExecutionDescription, updateGrouperPendingEvents]); - const filteredEventGroupsById = useMemo( + const sortedEventGroupsEntries = useMemo( () => sortBy( Object.entries(eventGroups), ([_, { firstEventId }]) => getSortableEventId(firstEventId), 'ASC' - ).filter(([_, g]) => + ), + [eventGroups] + ); + + const filteredEventGroupsEntries = useMemo( + () => + sortedEventGroupsEntries.filter(([_, g]) => workflowHistoryFiltersConfig.every((f) => f.filterFunc(g, { historyEventTypes: queryParams.historyEventTypes, @@ -159,7 +167,7 @@ export default function WorkflowHistoryV2({ params }: Props) { ) ), [ - eventGroups, + sortedEventGroupsEntries, queryParams.historyEventTypes, queryParams.historyEventStatuses, ] @@ -205,7 +213,7 @@ export default function WorkflowHistoryV2({ params }: Props) { const ungroupedEventsInfo = useMemo>( () => - filteredEventGroupsById + filteredEventGroupsEntries .map(([_, group]) => [ ...group.events.map((event, index) => ({ id: event.eventId ?? event.computedEventId, @@ -219,7 +227,7 @@ export default function WorkflowHistoryV2({ params }: Props) { ]) .flat(1) .sort(compareUngroupedEvents), - [filteredEventGroupsById] + [filteredEventGroupsEntries] ); const [_, setVisibleGroupsRange] = useThrottledState( @@ -251,7 +259,7 @@ export default function WorkflowHistoryV2({ params }: Props) { } = useInitialSelectedEvent({ selectedEventId: selectedEventIdWithinGroup, eventGroups, - filteredEventGroupsEntries: filteredEventGroupsById, + filteredEventGroupsEntries, }); const isLastPageEmpty = @@ -326,6 +334,90 @@ export default function WorkflowHistoryV2({ params }: Props) { }); }, [isUngroupedHistoryViewEnabled]); + const [scrollToEventId, setScrollToEventId] = useState( + undefined + ); + + useEffect(() => { + if (!scrollToEventId) return; + + const ref = isUngroupedHistoryViewEnabled + ? ungroupedTableVirtuosoRef + : groupedTableVirtuosoRef; + + if (!ref.current) return; + + const indexToScrollTo = isUngroupedHistoryViewEnabled + ? ungroupedEventsInfo.findIndex((e) => e.id === scrollToEventId) + : filteredEventGroupsEntries.findIndex(([_, group]) => + group.events.some((e) => e.eventId === scrollToEventId) + ); + + if (indexToScrollTo !== -1) { + ref.current.scrollToIndex({ + index: indexToScrollTo, + behavior: 'auto', + align: 'center', + }); + } + + setTimeout(() => { + setScrollToEventId(undefined); + }, 3000); + }, [ + scrollToEventId, + isUngroupedHistoryViewEnabled, + ungroupedEventsInfo, + filteredEventGroupsEntries, + ]); + + const failedEventsMenuItems = useMemo( + () => + getNavigationBarEventsMenuItems(sortedEventGroupsEntries, (group) => + filterGroupsByGroupStatus(group, { historyEventStatuses: ['FAILED'] }) + ), + [sortedEventGroupsEntries] + ); + + const pendingEventsMenuItems = useMemo( + () => + getNavigationBarEventsMenuItems(sortedEventGroupsEntries, (group) => + filterGroupsByGroupStatus(group, { + historyEventStatuses: ['PENDING'], + }) + ), + [sortedEventGroupsEntries] + ); + + const onClickNavMenuEvent = useCallback( + (eventId: string) => { + const isEventVisible = filteredEventGroupsEntries.some( + ([_, eventGroup]) => + eventGroup.events.some( + ({ eventId: eventIdInGroup }) => eventId === eventIdInGroup + ) + ); + + if (!isEventVisible) { + setQueryParams( + { historyEventStatuses: undefined, historyEventTypes: undefined }, + { replace: true } + ); + } + + setTimeout(() => { + setScrollToEventId(eventId); + if (!getIsItemExpanded(eventId)) toggleIsItemExpanded(eventId); + }, 0); + }, + [ + filteredEventGroupsEntries, + setQueryParams, + getIsItemExpanded, + toggleIsItemExpanded, + ] + ); + if (contentIsLoading) { return ; } @@ -357,7 +449,7 @@ export default function WorkflowHistoryV2({ params }: Props) { })) } decodedPageUrlParams={decodedParams} - selectedEventId={selectedEventIdWithinGroup} + selectedEventId={scrollToEventId ?? selectedEventIdWithinGroup} resetToDecisionEventId={setResetToDecisionEventId} getIsEventExpanded={getIsItemExpanded} toggleIsEventExpanded={toggleIsItemExpanded} @@ -368,7 +460,7 @@ export default function WorkflowHistoryV2({ params }: Props) { /> ) : ( @@ -383,7 +475,9 @@ export default function WorkflowHistoryV2({ params }: Props) { workflowCloseStatus={workflowExecutionInfo?.closeStatus} workflowIsArchived={workflowExecutionInfo?.isArchived || false} workflowCloseTimeMs={workflowCloseTimeMs} - selectedEventId={queryParams.historySelectedEventId} + selectedEventId={ + scrollToEventId ?? queryParams.historySelectedEventId + } resetToDecisionEventId={setResetToDecisionEventId} getIsEventExpanded={getIsItemExpanded} toggleIsEventExpanded={toggleIsItemExpanded} @@ -411,6 +505,10 @@ export default function WorkflowHistoryV2({ params }: Props) { onScrollDown={handleScrollDown} areAllItemsExpanded={areAllItemsExpanded} onToggleAllItemsExpanded={toggleAreAllItemsExpanded} + isUngroupedView={isUngroupedHistoryViewEnabled} + failedEventsMenuItems={failedEventsMenuItems} + pendingEventsMenuItems={pendingEventsMenuItems} + onClickEvent={onClickNavMenuEvent} /> ); diff --git a/src/views/workflow-history-v2/workflow-history-v2.types.ts b/src/views/workflow-history-v2/workflow-history-v2.types.ts index a96195ffc..180445012 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.types.ts +++ b/src/views/workflow-history-v2/workflow-history-v2.types.ts @@ -1,7 +1,10 @@ +import { type HistoryEventsGroup } from '../workflow-history/workflow-history.types'; import { type WorkflowPageTabContentProps } from '../workflow-page/workflow-page-tab-content/workflow-page-tab-content.types'; export type Props = WorkflowPageTabContentProps; +export type EventGroupEntry = [string, HistoryEventsGroup]; + export type WorkflowHistoryEventFilteringTypeColors = { content: string; background: string; From a53c0ecf899f94f09e353bd321438a7cc05c28a4 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 2 Jan 2026 16:58:23 +0100 Subject: [PATCH 02/11] more changes Signed-off-by: Adhitya Mamallan --- ...ry-clear-scroll-event-timeout-ms.config.ts | 3 + ...istory-navigation-bar-events-menu.test.tsx | 187 ++++++++++++++++++ ...ry-navigation-bar-events-menu.constants.ts | 1 + ...low-history-navigation-bar-events-menu.tsx | 34 ++-- ...istory-navigation-bar-events-menu.types.ts | 14 ++ .../workflow-history-navigation-bar.test.tsx | 82 ++++++++ .../workflow-history-navigation-bar.styles.ts | 34 +++- .../workflow-history-navigation-bar.tsx | 16 +- .../workflow-history-navigation-bar.types.ts | 8 +- .../workflow-history-v2.tsx | 3 +- 10 files changed, 349 insertions(+), 33 deletions(-) create mode 100644 src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts create mode 100644 src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/__tests__/workflow-history-navigation-bar-events-menu.test.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.constants.ts create mode 100644 src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.types.ts diff --git a/src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts b/src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts new file mode 100644 index 000000000..5105b33f5 --- /dev/null +++ b/src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts @@ -0,0 +1,3 @@ +const WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG = 3000; + +export default WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG; diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/__tests__/workflow-history-navigation-bar-events-menu.test.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/__tests__/workflow-history-navigation-bar-events-menu.test.tsx new file mode 100644 index 000000000..0915e1c99 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/__tests__/workflow-history-navigation-bar-events-menu.test.tsx @@ -0,0 +1,187 @@ +import { useState } from 'react'; + +import { type StatefulPopoverProps } from 'baseui/popover'; + +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import WorkflowHistoryNavigationBarEventsMenu from '../workflow-history-navigation-bar-events-menu'; +import { type Props } from '../workflow-history-navigation-bar-events-menu.types'; + +// Mock StatefulPopover to render content immediately in tests +jest.mock('baseui/popover', () => { + const originalModule = jest.requireActual('baseui/popover'); + return { + ...originalModule, + StatefulPopover: ({ content, children }: StatefulPopoverProps) => { + const [isShown, setIsShown] = useState(false); + + return ( +
+
setIsShown(true)}>{children}
+ {isShown ? ( +
+ {typeof content === 'function' && + content({ + close: () => { + setIsShown(false); + }, + })} +
+ ) : null} +
+ ); + }, + }; +}); + +describe(WorkflowHistoryNavigationBarEventsMenu.name, () => { + it('renders children', () => { + setup(); + + expect(screen.getByText('Open Popover')).toBeInTheDocument(); + }); + + it('does not show popover content initially', () => { + setup(); + + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument(); + }); + + it('renders all menu items when popover is opened', async () => { + const { user } = setup({ + menuItems: [ + { + eventId: 'event-1', + label: 'Event 1', + type: 'ACTIVITY', + }, + { + eventId: 'event-2', + label: 'Event 2', + type: 'DECISION', + }, + ], + }); + + await user.click(screen.getByText('Open Popover')); + + expect(screen.getByTestId('popover-content')).toBeInTheDocument(); + expect(screen.getByText('Event 1')).toBeInTheDocument(); + expect(screen.getByText('Event 2')).toBeInTheDocument(); + }); + + it('calls onClickEvent and closes popover when menu item is clicked', async () => { + const { user, mockOnClickEvent } = setup({ + menuItems: [ + { + eventId: 'event-1', + label: 'Event 1', + type: 'ACTIVITY', + }, + ], + }); + + await user.click(screen.getByText('Open Popover')); + + expect(screen.queryByTestId('popover-content')).toBeInTheDocument(); + const eventButton = screen.getByText('Event 1'); + await user.click(eventButton); + + expect(mockOnClickEvent).toHaveBeenCalledWith('event-1'); + expect(mockOnClickEvent).toHaveBeenCalledTimes(1); + + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument(); + }); + + it('shows pagination when there are more than 10 items', async () => { + const { user } = setup({ + menuItems: Array.from({ length: 15 }, (_, i) => ({ + eventId: `event-${i + 1}`, + label: `Event ${i + 1}`, + type: 'ACTIVITY', + })), + }); + + await user.click(screen.getByText('Open Popover')); + + expect(screen.getByTestId('pagination-container')).toBeInTheDocument(); + }); + + it('does not show pagination when there are 10 or fewer items', async () => { + const { user } = setup({ + menuItems: Array.from({ length: 10 }, (_, i) => ({ + eventId: `event-${i + 1}`, + label: `Event ${i + 1}`, + type: 'ACTIVITY', + })), + }); + + await user.click(screen.getByText('Open Popover')); + + expect( + screen.queryByTestId('pagination-container') + ).not.toBeInTheDocument(); + }); + + it('shows first 10 items on initial page', async () => { + const { user } = setup({ + menuItems: Array.from({ length: 15 }, (_, i) => ({ + eventId: `event-${i + 1}`, + label: `Event ${i + 1}`, + type: 'ACTIVITY', + })), + }); + + await user.click(screen.getByText('Open Popover')); + + expect(screen.getByText('Event 1')).toBeInTheDocument(); + expect(screen.getByText('Event 10')).toBeInTheDocument(); + expect(screen.queryByText('Event 11')).not.toBeInTheDocument(); + }); + + it('shows correct items when pagination page is changed', async () => { + const { user } = setup({ + menuItems: Array.from({ length: 15 }, (_, i) => ({ + eventId: `event-${i + 1}`, + label: `Event ${i + 1}`, + type: 'ACTIVITY', + })), + }); + + await user.click(screen.getByText('Open Popover')); + + const nextPageButton = await screen.findByLabelText(/next page/i); + await user.click(nextPageButton); + + expect(screen.queryByText('Event 1')).not.toBeInTheDocument(); + expect(screen.getByText('Event 11')).toBeInTheDocument(); + expect(screen.getByText('Event 15')).toBeInTheDocument(); + }); +}); + +function setup(overrides: Partial = {}) { + const user = userEvent.setup(); + const mockOnClickEvent = jest.fn(); + + render( + + + + ); + + return { + user, + mockOnClickEvent, + }; +} diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.constants.ts b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.constants.ts new file mode 100644 index 000000000..2fc3e8773 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.constants.ts @@ -0,0 +1 @@ +export const NAVBAR_MENU_ITEMS_PER_PAGE = 10; diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx index ab09b0a47..42085c349 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx @@ -3,42 +3,40 @@ import { useState, useMemo } from 'react'; import { Button } from 'baseui/button'; import { Pagination } from 'baseui/pagination'; import { StatefulPopover } from 'baseui/popover'; +import { MdCircle, MdOutlineCircle } from 'react-icons/md'; -import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar/workflow-history-navigation-bar.types'; +import workflowHistoryEventFilteringTypeColorsConfig from '../config/workflow-history-event-filtering-type-colors.config'; +import { NAVBAR_MENU_ITEMS_PER_PAGE } from './workflow-history-navigation-bar-events-menu.constants'; import { styled, overrides, } from './workflow-history-navigation-bar-events-menu.styles'; - -const ITEMS_PER_PAGE = 10; +import { type Props } from './workflow-history-navigation-bar-events-menu.types'; export default function WorkflowHistoryNavigationBarEventsMenu({ children, isUngroupedHistoryView, menuItems, onClickEvent, -}: { - children: React.ReactNode; - isUngroupedHistoryView: boolean; - menuItems: Array; - onClickEvent: (eventId: string) => void; -}) { +}: Props) { const [currentPage, setCurrentPage] = useState(1); - const totalPages = Math.ceil(menuItems.length / ITEMS_PER_PAGE); + const totalPages = Math.ceil(menuItems.length / NAVBAR_MENU_ITEMS_PER_PAGE); const paginatedItems = useMemo(() => { - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; + const startIndex = (currentPage - 1) * NAVBAR_MENU_ITEMS_PER_PAGE; + const endIndex = startIndex + NAVBAR_MENU_ITEMS_PER_PAGE; return menuItems.slice(startIndex, endIndex); }, [menuItems, currentPage]); + const MenuItemIcon = isUngroupedHistoryView ? MdOutlineCircle : MdCircle; + return ( ( - {paginatedItems.map(({ eventId, label }) => ( + {paginatedItems.map(({ eventId, label, type }) => ( ))} {totalPages > 1 && ( - + ; + onClickEvent: (eventId: string) => void; +}; + +export type NavigationBarEventsMenuItem = { + type: WorkflowHistoryEventFilteringType; + eventId: string; + label: string; +}; diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/__tests__/workflow-history-navigation-bar.test.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar/__tests__/workflow-history-navigation-bar.test.tsx index 709193f51..133725035 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/__tests__/workflow-history-navigation-bar.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/__tests__/workflow-history-navigation-bar.test.tsx @@ -3,6 +3,14 @@ import { render, screen, userEvent } from '@/test-utils/rtl'; import WorkflowHistoryNavigationBar from '../workflow-history-navigation-bar'; import { type Props } from '../workflow-history-navigation-bar.types'; +jest.mock( + '../../workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu', + () => + jest.fn(({ children }: { children: React.ReactNode }) => { + return
{children}
; + }) +); + describe(WorkflowHistoryNavigationBar.name, () => { it('renders all navigation buttons', () => { setup(); @@ -54,6 +62,74 @@ describe(WorkflowHistoryNavigationBar.name, () => { expect(mockOnScrollUp).toHaveBeenCalledTimes(1); }); + + it('renders failed events button when failedEventsMenuItems is not empty', () => { + setup({ + failedEventsMenuItems: [ + { + eventId: 'event-1', + label: 'Failed Event 1', + type: 'ACTIVITY', + }, + ], + }); + + expect(screen.getByLabelText('Failed events')).toBeInTheDocument(); + expect(screen.getByText('1 failed event')).toBeInTheDocument(); + }); + + it('renders plural failed events text when multiple failed events exist', () => { + setup({ + failedEventsMenuItems: [ + { + eventId: 'event-1', + label: 'Failed Event 1', + type: 'ACTIVITY', + }, + { + eventId: 'event-2', + label: 'Failed Event 2', + type: 'DECISION', + }, + ], + }); + + expect(screen.getByText('2 failed events')).toBeInTheDocument(); + }); + + it('renders pending events button when pendingEventsMenuItems is not empty', () => { + setup({ + pendingEventsMenuItems: [ + { + eventId: 'event-1', + label: 'Pending Event 1', + type: 'ACTIVITY', + }, + ], + }); + + expect(screen.getByLabelText('Pending events')).toBeInTheDocument(); + expect(screen.getByText('1 pending event')).toBeInTheDocument(); + }); + + it('renders plural pending events text when multiple pending events exist', () => { + setup({ + pendingEventsMenuItems: [ + { + eventId: 'event-1', + label: 'Pending Event 1', + type: 'ACTIVITY', + }, + { + eventId: 'event-2', + label: 'Pending Event 2', + type: 'DECISION', + }, + ], + }); + + expect(screen.getByText('2 pending events')).toBeInTheDocument(); + }); }); function setup(overrides: Partial = {}) { @@ -61,6 +137,7 @@ function setup(overrides: Partial = {}) { const mockOnScrollUp = jest.fn(); const mockOnScrollDown = jest.fn(); const mockOnToggleAllItemsExpanded = jest.fn(); + const mockOnClickEvent = jest.fn(); render( = {}) { onScrollDown={mockOnScrollDown} areAllItemsExpanded={false} onToggleAllItemsExpanded={mockOnToggleAllItemsExpanded} + isUngroupedView={false} + failedEventsMenuItems={[]} + pendingEventsMenuItems={[]} + onClickEvent={mockOnClickEvent} {...overrides} /> ); @@ -77,5 +158,6 @@ function setup(overrides: Partial = {}) { mockOnScrollUp, mockOnScrollDown, mockOnToggleAllItemsExpanded, + mockOnClickEvent, }; } diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.styles.ts b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.styles.ts index 8ab30ed35..52fd225d6 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.styles.ts +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.styles.ts @@ -31,14 +31,38 @@ export const styled = { ), }; +const buttonRootOverrides = { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + paddingTop: $theme.sizing.scale200, + paddingBottom: $theme.sizing.scale200, + paddingLeft: $theme.sizing.scale200, + paddingRight: $theme.sizing.scale200, + }), +}; + export const overrides = { navActionButton: { - Root: { + Root: buttonRootOverrides, + } satisfies ButtonOverrides, + failedEventsButton: { + Root: buttonRootOverrides, + BaseButton: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + backgroundColor: $theme.colors.backgroundNegative, + ':hover': { + backgroundColor: $theme.colors.backgroundNegative, + }, + }), + }, + } satisfies ButtonOverrides, + pendingEventsButton: { + Root: buttonRootOverrides, + BaseButton: { style: ({ $theme }: { $theme: Theme }): StyleObject => ({ - paddingTop: $theme.sizing.scale200, - paddingBottom: $theme.sizing.scale200, - paddingLeft: $theme.sizing.scale200, - paddingRight: $theme.sizing.scale200, + backgroundColor: $theme.colors.backgroundAccent, + ':hover': { + backgroundColor: $theme.colors.backgroundAccent, + }, }), }, } satisfies ButtonOverrides, diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx index 3d9250099..2a5fa1a74 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx @@ -1,6 +1,7 @@ import { MdArrowDownward, MdArrowUpward, + MdErrorOutline, MdUnfoldLess, MdUnfoldMore, } from 'react-icons/md'; @@ -69,12 +70,14 @@ export default function WorkflowHistoryNavigationBar({ > @@ -89,12 +92,13 @@ export default function WorkflowHistoryNavigationBar({ > diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts index 13d2421d9..584747598 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.types.ts @@ -1,4 +1,4 @@ -import { type WorkflowHistoryEventFilteringType } from '@/views/workflow-history/workflow-history-filters-type/workflow-history-filters-type.types'; +import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.types'; export type Props = { onScrollUp: () => void; @@ -10,9 +10,3 @@ export type Props = { pendingEventsMenuItems: Array; onClickEvent: (eventId: string) => void; }; - -export type NavigationBarEventsMenuItem = { - type: WorkflowHistoryEventFilteringType; - eventId: string; - label: string; -}; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index abff3b06b..486fc338f 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -32,6 +32,7 @@ import workflowPageQueryParamsConfig from '../workflow-page/config/workflow-page import { useSuspenseDescribeWorkflow } from '../workflow-page/hooks/use-describe-workflow'; import { type WorkflowPageTabContentParams } from '../workflow-page/workflow-page-tab-content/workflow-page-tab-content.types'; +import WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG from './config/workflow-history-clear-scroll-event-timeout-ms.config'; import WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG from './config/workflow-history-fetch-events-throttle-ms.config'; import workflowHistoryFiltersConfig from './config/workflow-history-filters.config'; import WORKFLOW_HISTORY_RENDER_FETCHED_EVENTS_THROTTLE_MS_CONFIG from './config/workflow-history-render-fetched-events-throttle-ms.config'; @@ -363,7 +364,7 @@ export default function WorkflowHistoryV2({ params }: Props) { setTimeout(() => { setScrollToEventId(undefined); - }, 3000); + }, WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG); }, [ scrollToEventId, isUngroupedHistoryViewEnabled, From 480b302e7c64d10891bfa97d07e8d6afb7621598 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 5 Jan 2026 14:22:10 +0100 Subject: [PATCH 03/11] add tests Signed-off-by: Adhitya Mamallan --- .../__tests__/workflow-history-v2.test.tsx | 88 +++++++++++++++++-- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx b/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx index 9292b2188..6fd5a0314 100644 --- a/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx +++ b/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx @@ -16,12 +16,29 @@ import { type HistoryEvent } from '@/__generated__/proto-ts/uber/cadence/api/v1/ import * as usePageFiltersModule from '@/components/page-filters/hooks/use-page-filters'; import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-page-query-params.types'; import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types'; +import { + type PendingActivityTaskStartEvent, + type PendingDecisionTaskStartEvent, +} from '@/views/workflow-history/workflow-history.types'; import { mockDescribeWorkflowResponse } from '@/views/workflow-page/__fixtures__/describe-workflow-response'; import type workflowPageQueryParamsConfig from '@/views/workflow-page/config/workflow-page-query-params.config'; -import { completedActivityTaskEvents } from '../../workflow-history/__fixtures__/workflow-history-activity-events'; -import { completedDecisionTaskEvents } from '../../workflow-history/__fixtures__/workflow-history-decision-events'; +import { + completedActivityTaskEvents, + failedActivityTaskEvents, + scheduleActivityTaskEvent, +} from '../../workflow-history/__fixtures__/workflow-history-activity-events'; +import { + completedDecisionTaskEvents, + failedDecisionTaskEvents, + scheduleDecisionTaskEvent, +} from '../../workflow-history/__fixtures__/workflow-history-decision-events'; +import { + pendingActivityTaskStartEvent, + pendingDecisionTaskStartEvent, +} from '../../workflow-history/__fixtures__/workflow-history-pending-events'; import { WorkflowHistoryContext } from '../../workflow-history/workflow-history-context-provider/workflow-history-context-provider'; +import { type Props as NavbarProps } from '../workflow-history-navigation-bar/workflow-history-navigation-bar.types'; import WorkflowHistoryV2 from '../workflow-history-v2'; jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => @@ -111,9 +128,22 @@ jest.mock( jest.mock( '../workflow-history-navigation-bar/workflow-history-navigation-bar', () => - jest.fn(() => ( -
Navigation Bar
- )) + jest.fn( + ({ failedEventsMenuItems, pendingEventsMenuItems }: NavbarProps) => ( +
+ {failedEventsMenuItems && failedEventsMenuItems.length > 0 && ( +
+ {failedEventsMenuItems.length} failed events +
+ )} + {pendingEventsMenuItems && pendingEventsMenuItems.length > 0 && ( +
+ {pendingEventsMenuItems.length} pending events +
+ )} +
+ ) + ) ); jest.mock('@/utils/decode-url-params', () => jest.fn((params) => params)); @@ -408,6 +438,30 @@ describe(WorkflowHistoryV2.name, () => { await screen.findByTestId('ungrouped-selected-event-id') ).toHaveTextContent('test-event-id'); }); + + it('passes failed events menu items to navigation bar when failed events exist', async () => { + await setup({ + historyEvents: [...failedActivityTaskEvents, ...failedDecisionTaskEvents], + }); + + const failedItemsCounter = await screen.findByTestId( + 'failed-events-menu-items-count' + ); + expect(failedItemsCounter).toHaveTextContent('2 failed events'); + }); + + it('passes pending events menu items to navigation bar when pending events exist', async () => { + await setup({ + historyEvents: [scheduleActivityTaskEvent, scheduleDecisionTaskEvent], + pendingActivities: [pendingActivityTaskStartEvent], + pendingDecision: pendingDecisionTaskStartEvent, + }); + + const pendingItemsCounter = await screen.findByTestId( + 'pending-events-menu-items-count' + ); + expect(pendingItemsCounter).toHaveTextContent('2 pending events'); + }); }); async function setup({ @@ -417,6 +471,9 @@ async function setup({ pageQueryParamsValues = {}, hasNextPage, ungroupedViewPreference, + historyEvents, + pendingActivities, + pendingDecision, }: { error?: boolean; summaryError?: boolean; @@ -426,7 +483,10 @@ async function setup({ >; hasNextPage?: boolean; ungroupedViewPreference?: boolean | null; -}) { + historyEvents?: Array; + pendingActivities?: Array; + pendingDecision?: PendingDecisionTaskStartEvent | null; +} = {}) { const user = userEvent.setup(); const mockSetQueryParams = jest.fn(); @@ -497,7 +557,8 @@ async function setup({ ); } - const events: Array = completedActivityTaskEvents; + const events: Array = + historyEvents ?? completedActivityTaskEvents; return HttpResponse.json( { @@ -526,7 +587,18 @@ async function setup({ }, } : { - jsonResponse: mockDescribeWorkflowResponse, + httpResolver: () => { + const describeResponse = { + ...mockDescribeWorkflowResponse, + ...(pendingActivities && pendingActivities.length > 0 + ? { + pendingActivities, + } + : {}), + ...(pendingDecision ? { pendingDecision } : {}), + }; + return HttpResponse.json(describeResponse, { status: 200 }); + }, }), }, ], From 805b2878d41079ff9686a063748028e4f6ac31d4 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 5 Jan 2026 14:29:20 +0100 Subject: [PATCH 04/11] add tests for nav bar helper Signed-off-by: Adhitya Mamallan --- ...t-navigation-bar-events-menu-items.test.ts | 145 ++++++++++++++++++ .../get-navigation-bar-events-menu-items.ts | 2 +- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts diff --git a/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts b/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts new file mode 100644 index 000000000..2f1744e96 --- /dev/null +++ b/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts @@ -0,0 +1,145 @@ +import { + mockActivityEventGroup, + mockDecisionEventGroup, +} from '@/views/workflow-history/__fixtures__/workflow-history-event-groups'; +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; + +import { type EventGroupEntry } from '../../workflow-history-v2.types'; +import getNavigationBarEventsMenuItems from '../get-navigation-bar-events-menu-items'; + +jest.mock( + '../../workflow-history-event-group/helpers/get-event-group-filtering-type', + () => + jest.fn((group: HistoryEventsGroup) => { + if (group.groupType === 'Activity') return 'ACTIVITY'; + if (group.groupType === 'Decision') return 'DECISION'; + return 'WORKFLOW'; + }) +); + +describe(getNavigationBarEventsMenuItems.name, () => { + it('should return an empty array when eventGroupsEntries is empty', () => { + const eventGroupsEntries: Array = []; + const filterFn = jest.fn(() => true); + + const result = getNavigationBarEventsMenuItems( + eventGroupsEntries, + filterFn + ); + + expect(result).toEqual([]); + expect(filterFn).not.toHaveBeenCalled(); + }); + + it('should skip groups with no events', () => { + const groupWithoutEvents: HistoryEventsGroup = { + ...mockActivityEventGroup, + events: [], + }; + const eventGroupsEntries: Array = [ + ['group1', groupWithoutEvents], + ]; + const filterFn = jest.fn(() => true); + + const result = getNavigationBarEventsMenuItems( + eventGroupsEntries, + filterFn + ); + + expect(result).toEqual([]); + expect(filterFn).not.toHaveBeenCalled(); + }); + + it('should skip groups filtered out by filterFn', () => { + const eventGroupsEntries: Array = [ + ['group1', mockActivityEventGroup], + ['group2', mockDecisionEventGroup], + ]; + const filterFn = jest.fn((group: HistoryEventsGroup) => { + return group.groupType === 'Decision'; + }); + + const result = getNavigationBarEventsMenuItems( + eventGroupsEntries, + filterFn + ); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + eventId: '4', // last eventId from completedDecisionTaskEvents + label: 'Mock decision', + type: 'DECISION', + }); + expect(filterFn).toHaveBeenCalledTimes(2); + expect(filterFn).toHaveBeenCalledWith(mockActivityEventGroup); + expect(filterFn).toHaveBeenCalledWith(mockDecisionEventGroup); + }); + + it('should include groups that pass filterFn', () => { + const eventGroupsEntries: Array = [ + ['group1', mockActivityEventGroup], + ['group2', mockDecisionEventGroup], + ]; + const filterFn = jest.fn(() => true); + + const result = getNavigationBarEventsMenuItems( + eventGroupsEntries, + filterFn + ); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + eventId: '10', // last eventId from completedActivityTaskEvents + label: 'Mock event', + type: 'ACTIVITY', + }); + expect(result[1]).toEqual({ + eventId: '4', // last eventId from completedDecisionTaskEvents + label: 'Mock decision', + type: 'DECISION', + }); + }); + + it('should use shortLabel when available, otherwise use label', () => { + const groupWithShortLabel: HistoryEventsGroup = { + ...mockActivityEventGroup, + label: 'Long Label', + shortLabel: 'Short', + }; + const groupWithoutShortLabel: HistoryEventsGroup = { + ...mockDecisionEventGroup, + label: 'Decision Label', + }; + const eventGroupsEntries: Array = [ + ['group1', groupWithShortLabel], + ['group2', groupWithoutShortLabel], + ]; + const filterFn = jest.fn(() => true); + + const result = getNavigationBarEventsMenuItems( + eventGroupsEntries, + filterFn + ); + + expect(result).toHaveLength(2); + expect(result[0].label).toBe('Short'); + expect(result[1].label).toBe('Decision Label'); + }); + + it('should use the last event eventId from each group', () => { + const eventGroupsEntries: Array = [ + ['group1', mockActivityEventGroup], + ]; + const filterFn = jest.fn(() => true); + + const result = getNavigationBarEventsMenuItems( + eventGroupsEntries, + filterFn + ); + + expect(result).toHaveLength(1); + // completedActivityTaskEvents has events with ids: '7', '9', '10' + // Last eventId should be '10' + expect(result[0].eventId).toBe('10'); + }); +}); diff --git a/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts b/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts index f7f47ec42..83e3f005c 100644 --- a/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts +++ b/src/views/workflow-history-v2/helpers/get-navigation-bar-events-menu-items.ts @@ -1,7 +1,7 @@ import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; import getEventGroupFilteringType from '../workflow-history-event-group/helpers/get-event-group-filtering-type'; -import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar/workflow-history-navigation-bar.types'; +import { type NavigationBarEventsMenuItem } from '../workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.types'; import { type EventGroupEntry } from '../workflow-history-v2.types'; export default function getNavigationBarEventsMenuItems( From f141b7fa6f4c08972a7f26afd084753e30210612 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 5 Jan 2026 15:40:55 +0100 Subject: [PATCH 05/11] Add icon for pending events Signed-off-by: Adhitya Mamallan --- .../workflow-history-navigation-bar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx index 2a5fa1a74..f2ebb8349 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx @@ -2,6 +2,7 @@ import { MdArrowDownward, MdArrowUpward, MdErrorOutline, + MdHourglassTop, MdUnfoldLess, MdUnfoldMore, } from 'react-icons/md'; @@ -94,6 +95,7 @@ export default function WorkflowHistoryNavigationBar({ size="mini" shape="pill" overrides={overrides.pendingEventsButton} + startEnhancer={} aria-label="Pending events" > {pendingEventsMenuItems.length === 1 From b8ec93e4acaf771ea35ee5ead9ff310effa38386 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 5 Jan 2026 15:58:43 +0100 Subject: [PATCH 06/11] address comments Signed-off-by: Adhitya Mamallan --- ...kflow-history-navigation-bar-events-menu.styles.ts | 11 ----------- .../workflow-history-navigation-bar-events-menu.tsx | 6 +++++- .../workflow-history-navigation-bar.tsx | 2 +- src/views/workflow-history-v2/workflow-history-v2.tsx | 6 +++++- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts index 057247b46..4ffd0f993 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.styles.ts @@ -1,5 +1,4 @@ import { styled as createStyled, type Theme } from 'baseui'; -import { type ButtonOverrides } from 'baseui/button'; import { type PaginationOverrides } from 'baseui/pagination'; import { type StyleObject } from 'styletron-react'; @@ -27,16 +26,6 @@ export const styled = { }; export const overrides = { - navActionButton: { - Root: { - style: ({ $theme }: { $theme: Theme }): StyleObject => ({ - paddingTop: $theme.sizing.scale200, - paddingBottom: $theme.sizing.scale200, - paddingLeft: $theme.sizing.scale200, - paddingRight: $theme.sizing.scale200, - }), - }, - } satisfies ButtonOverrides, pagination: { Select: { props: { diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx index 42085c349..54569c010 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar-events-menu/workflow-history-navigation-bar-events-menu.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Button } from 'baseui/button'; import { Pagination } from 'baseui/pagination'; @@ -22,6 +22,10 @@ export default function WorkflowHistoryNavigationBarEventsMenu({ }: Props) { const [currentPage, setCurrentPage] = useState(1); + useEffect(() => { + setCurrentPage(1); + }, [menuItems.length]); + const totalPages = Math.ceil(menuItems.length / NAVBAR_MENU_ITEMS_PER_PAGE); const paginatedItems = useMemo(() => { diff --git a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx index f2ebb8349..6a08d0fe4 100644 --- a/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx +++ b/src/views/workflow-history-v2/workflow-history-navigation-bar/workflow-history-navigation-bar.tsx @@ -95,7 +95,7 @@ export default function WorkflowHistoryNavigationBar({ size="mini" shape="pill" overrides={overrides.pendingEventsButton} - startEnhancer={} + startEnhancer={} aria-label="Pending events" > {pendingEventsMenuItems.length === 1 diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index 486fc338f..bba0d7c0f 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -362,9 +362,13 @@ export default function WorkflowHistoryV2({ params }: Props) { }); } - setTimeout(() => { + const timeoutId = setTimeout(() => { setScrollToEventId(undefined); }, WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG); + + return () => { + clearTimeout(timeoutId); + }; }, [ scrollToEventId, isUngroupedHistoryViewEnabled, From cff40e67bce38f03cab09e837ba9d6769c537d8d Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 9 Jan 2026 12:41:48 +0100 Subject: [PATCH 07/11] Address comments Signed-off-by: Adhitya Mamallan --- .../__tests__/workflow-history-v2.test.tsx | 7 ++----- .../__tests__/get-navigation-bar-events-menu-items.test.ts | 3 --- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx b/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx index 6fd5a0314..16abd9475 100644 --- a/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx +++ b/src/views/workflow-history-v2/__tests__/workflow-history-v2.test.tsx @@ -471,7 +471,7 @@ async function setup({ pageQueryParamsValues = {}, hasNextPage, ungroupedViewPreference, - historyEvents, + historyEvents = completedActivityTaskEvents, pendingActivities, pendingDecision, }: { @@ -557,13 +557,10 @@ async function setup({ ); } - const events: Array = - historyEvents ?? completedActivityTaskEvents; - return HttpResponse.json( { history: { - events, + events: historyEvents, }, archived: false, nextPageToken: hasNextPage ? 'mock-next-page-token' : '', diff --git a/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts b/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts index 2f1744e96..559b67057 100644 --- a/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts +++ b/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts @@ -70,9 +70,6 @@ describe(getNavigationBarEventsMenuItems.name, () => { label: 'Mock decision', type: 'DECISION', }); - expect(filterFn).toHaveBeenCalledTimes(2); - expect(filterFn).toHaveBeenCalledWith(mockActivityEventGroup); - expect(filterFn).toHaveBeenCalledWith(mockDecisionEventGroup); }); it('should include groups that pass filterFn', () => { From 81977bbc02edbfa2225117a7455653cc9eb8994d Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 9 Jan 2026 12:46:27 +0100 Subject: [PATCH 08/11] rearrange function calls in test Signed-off-by: Adhitya Mamallan --- ...get-navigation-bar-events-menu-items.test.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts b/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts index 559b67057..94bd12132 100644 --- a/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts +++ b/src/views/workflow-history-v2/helpers/__tests__/get-navigation-bar-events-menu-items.test.ts @@ -20,15 +20,13 @@ jest.mock( describe(getNavigationBarEventsMenuItems.name, () => { it('should return an empty array when eventGroupsEntries is empty', () => { const eventGroupsEntries: Array = []; - const filterFn = jest.fn(() => true); const result = getNavigationBarEventsMenuItems( eventGroupsEntries, - filterFn + () => true ); expect(result).toEqual([]); - expect(filterFn).not.toHaveBeenCalled(); }); it('should skip groups with no events', () => { @@ -39,15 +37,13 @@ describe(getNavigationBarEventsMenuItems.name, () => { const eventGroupsEntries: Array = [ ['group1', groupWithoutEvents], ]; - const filterFn = jest.fn(() => true); const result = getNavigationBarEventsMenuItems( eventGroupsEntries, - filterFn + () => true ); expect(result).toEqual([]); - expect(filterFn).not.toHaveBeenCalled(); }); it('should skip groups filtered out by filterFn', () => { @@ -77,11 +73,10 @@ describe(getNavigationBarEventsMenuItems.name, () => { ['group1', mockActivityEventGroup], ['group2', mockDecisionEventGroup], ]; - const filterFn = jest.fn(() => true); const result = getNavigationBarEventsMenuItems( eventGroupsEntries, - filterFn + () => true ); expect(result).toHaveLength(2); @@ -111,11 +106,10 @@ describe(getNavigationBarEventsMenuItems.name, () => { ['group1', groupWithShortLabel], ['group2', groupWithoutShortLabel], ]; - const filterFn = jest.fn(() => true); const result = getNavigationBarEventsMenuItems( eventGroupsEntries, - filterFn + () => true ); expect(result).toHaveLength(2); @@ -127,11 +121,10 @@ describe(getNavigationBarEventsMenuItems.name, () => { const eventGroupsEntries: Array = [ ['group1', mockActivityEventGroup], ]; - const filterFn = jest.fn(() => true); const result = getNavigationBarEventsMenuItems( eventGroupsEntries, - filterFn + () => true ); expect(result).toHaveLength(1); From 35f55f7fcc9c5ac1cbadd0fe77a2fb9187a578e4 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 14 Jan 2026 15:34:10 +0530 Subject: [PATCH 09/11] Remove 0 timeout from onClickNavMenuEvent Signed-off-by: Adhitya Mamallan --- src/views/workflow-history-v2/workflow-history-v2.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index bba0d7c0f..77f6e8a38 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -410,10 +410,8 @@ export default function WorkflowHistoryV2({ params }: Props) { ); } - setTimeout(() => { - setScrollToEventId(eventId); - if (!getIsItemExpanded(eventId)) toggleIsItemExpanded(eventId); - }, 0); + setScrollToEventId(eventId); + if (!getIsItemExpanded(eventId)) toggleIsItemExpanded(eventId); }, [ filteredEventGroupsEntries, From e90f8a2450a74e632af79e5bbdd2b330c571c796 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 14 Jan 2026 22:00:46 +0530 Subject: [PATCH 10/11] hardcode scroll clear Signed-off-by: Adhitya Mamallan --- .../workflow-history-clear-scroll-event-timeout-ms.config.ts | 3 --- src/views/workflow-history-v2/workflow-history-v2.tsx | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts diff --git a/src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts b/src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts deleted file mode 100644 index 5105b33f5..000000000 --- a/src/views/workflow-history-v2/config/workflow-history-clear-scroll-event-timeout-ms.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -const WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG = 3000; - -export default WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index 77f6e8a38..b97364b17 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -32,7 +32,6 @@ import workflowPageQueryParamsConfig from '../workflow-page/config/workflow-page import { useSuspenseDescribeWorkflow } from '../workflow-page/hooks/use-describe-workflow'; import { type WorkflowPageTabContentParams } from '../workflow-page/workflow-page-tab-content/workflow-page-tab-content.types'; -import WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG from './config/workflow-history-clear-scroll-event-timeout-ms.config'; import WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG from './config/workflow-history-fetch-events-throttle-ms.config'; import workflowHistoryFiltersConfig from './config/workflow-history-filters.config'; import WORKFLOW_HISTORY_RENDER_FETCHED_EVENTS_THROTTLE_MS_CONFIG from './config/workflow-history-render-fetched-events-throttle-ms.config'; @@ -364,7 +363,8 @@ export default function WorkflowHistoryV2({ params }: Props) { const timeoutId = setTimeout(() => { setScrollToEventId(undefined); - }, WORKFLOW_HISTORY_CLEAR_SCROLL_EVENT_TIMEOUT_MS_CONFIG); + // We clear scrollToEventId after 2s to allow the fade-in animation to complete + }, 2000); return () => { clearTimeout(timeoutId); From 2823aaa1a8a8eeea5bd2935a419df92b73b11a20 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 14 Jan 2026 23:00:25 +0530 Subject: [PATCH 11/11] decouple event finding from scrolling Signed-off-by: Adhitya Mamallan --- .../workflow-history-v2.tsx | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index b97364b17..0e3ef0b22 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -338,43 +338,38 @@ export default function WorkflowHistoryV2({ params }: Props) { undefined ); - useEffect(() => { - if (!scrollToEventId) return; + const scrollToEventIndex = useMemo(() => { + if (!scrollToEventId) return undefined; + return isUngroupedHistoryViewEnabled + ? ungroupedEventsInfo.findIndex((e) => e.id === scrollToEventId) + : filteredEventGroupsEntries.findIndex(([_, group]) => + group.events.some((e) => e.eventId === scrollToEventId) + ); + }, [ + scrollToEventId, + isUngroupedHistoryViewEnabled, + ungroupedEventsInfo, + filteredEventGroupsEntries, + ]); + + useEffect(() => { const ref = isUngroupedHistoryViewEnabled ? ungroupedTableVirtuosoRef : groupedTableVirtuosoRef; if (!ref.current) return; - const indexToScrollTo = isUngroupedHistoryViewEnabled - ? ungroupedEventsInfo.findIndex((e) => e.id === scrollToEventId) - : filteredEventGroupsEntries.findIndex(([_, group]) => - group.events.some((e) => e.eventId === scrollToEventId) - ); - - if (indexToScrollTo !== -1) { + if (scrollToEventIndex && scrollToEventIndex !== -1) { ref.current.scrollToIndex({ - index: indexToScrollTo, + index: scrollToEventIndex, behavior: 'auto', align: 'center', }); } - const timeoutId = setTimeout(() => { - setScrollToEventId(undefined); - // We clear scrollToEventId after 2s to allow the fade-in animation to complete - }, 2000); - - return () => { - clearTimeout(timeoutId); - }; - }, [ - scrollToEventId, - isUngroupedHistoryViewEnabled, - ungroupedEventsInfo, - filteredEventGroupsEntries, - ]); + setTimeout(() => setScrollToEventId(undefined), 2000); + }, [scrollToEventIndex, isUngroupedHistoryViewEnabled]); const failedEventsMenuItems = useMemo( () =>