From cd6517141b0d2186175d3cf191ec6ca3dd4e8a3d Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 24 Nov 2025 15:37:06 +0100 Subject: [PATCH 1/4] Add grouped table with raw JSON of events Signed-off-by: Adhitya Mamallan --- .../__tests__/workflow-history-v2.test.tsx | 381 ++++++++++++++---- .../workflow-history-grouped-table.test.tsx | 124 +++++- .../workflow-history-grouped-table.tsx | 74 +++- .../workflow-history-grouped-table.types.ts | 24 ++ .../workflow-history-v2.tsx | 226 ++++++++++- .../workflow-history-v2.types.ts | 9 +- 6 files changed, 740 insertions(+), 98 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.types.ts 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 16e0dac9e..fbe5cff8c 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 @@ -1,20 +1,61 @@ -import React from 'react'; +import { Suspense } from 'react'; -import { render, screen, userEvent } from '@/test-utils/rtl'; +import { HttpResponse } from 'msw'; +import { VirtuosoMockContext } from 'react-virtuoso'; +import { + act, + render, + screen, + userEvent, + waitFor, + waitForElementToBeRemoved, +} from '@/test-utils/rtl'; + +import { type HistoryEvent } from '@/__generated__/proto-ts/uber/cadence/api/v1/HistoryEvent'; import * as usePageFiltersModule from '@/components/page-filters/hooks/use-page-filters'; -import { type WorkflowPageParams } from '@/views/workflow-page/workflow-page.types'; +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 { 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 { WorkflowHistoryContext } from '../../workflow-history/workflow-history-context-provider/workflow-history-context-provider'; import WorkflowHistoryV2 from '../workflow-history-v2'; +jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => + jest.fn(() => [{ historySelectedEventId: '1' }, jest.fn()]) +); + +// Mock the hook to use minimal throttle delay for faster tests +jest.mock('../../workflow-history/hooks/use-workflow-history-fetcher', () => { + const actual = jest.requireActual( + '../../workflow-history/hooks/use-workflow-history-fetcher' + ); + return { + __esModule: true, + default: jest.fn((params, onEventsChange) => + actual.default(params, onEventsChange, 0) + ), // 0ms throttle for tests + }; +}); + jest.mock('@/components/page-filters/hooks/use-page-filters', () => jest.fn().mockReturnValue({}) ); +jest.mock('../config/workflow-history-filters.config', () => []); + +jest.mock( + '@/components/section-loading-indicator/section-loading-indicator', + () => jest.fn(() =>
keep loading events
) +); + jest.mock('../workflow-history-header/workflow-history-header', () => jest.fn(({ isUngroupedHistoryViewEnabled, onClickGroupModeToggle }) => (
+
Workflow history Header
{String(isUngroupedHistoryViewEnabled)}
@@ -35,75 +76,171 @@ jest.mock( jest.mock('@/utils/decode-url-params', () => jest.fn((params) => params)); -const mockSetQueryParams = jest.fn(); const mockResetAllFilters = jest.fn(); -const mockSetUngroupedViewUserPreference = jest.fn(); describe(WorkflowHistoryV2.name, () => { - beforeEach(() => { - jest.clearAllMocks(); + afterEach(() => { + jest.restoreAllMocks(); }); - it('should render WorkflowHistoryHeader', () => { - setup(); - expect(screen.getByTestId('workflow-history-header')).toBeInTheDocument(); + it('renders page header correctly', async () => { + await setup({}); + expect( + await screen.findByText('Workflow history Header') + ).toBeInTheDocument(); }); - it('should render grouped table by default when ungroupedHistoryViewEnabled is not set and user preference is false', () => { - setup({ ungroupedViewUserPreference: false }); + it('renders grouped table', async () => { + await setup({}); expect( - screen.getByTestId('workflow-history-grouped-table') + await screen.findByTestId('workflow-history-grouped-table') + ).toBeInTheDocument(); + }); + + it('throws an error if the request fails', async () => { + try { + await act(async () => await setup({ error: true })); + } catch (error) { + expect((error as Error)?.message).toBe( + 'Failed to fetch workflow history' + ); + } + }); + + it('throws an error if the workflow summary request fails', async () => { + try { + await act(async () => await setup({ summaryError: true })); + } catch (error) { + expect((error as Error)?.message).toBe( + 'Failed to fetch workflow summary' + ); + } + }); + + it('should show loading while searching for initial selectedEventId', async () => { + const { getRequestResolver } = await setup({ + resolveLoadMoreManually: true, + pageQueryParamsValues: { + historySelectedEventId: completedDecisionTaskEvents[1].eventId, + }, + hasNextPage: true, + }); + + await waitFor(() => { + expect(screen.getByText('keep loading events')).toBeInTheDocument(); + }); + + // Load first page + await act(async () => { + const resolver = getRequestResolver(); + resolver({ + history: { + events: [completedDecisionTaskEvents[0]], + }, + archived: false, + nextPageToken: 'mock-next-page-token', + rawHistory: [], + }); + }); + + await waitFor(() => { + expect(screen.getByText('keep loading events')).toBeInTheDocument(); + }); + + // Load second page + await act(async () => { + const secondPageResolver = getRequestResolver(); + secondPageResolver({ + history: { + events: [completedDecisionTaskEvents[1]], + }, + archived: false, + nextPageToken: 'mock-next-page-token', + rawHistory: [], + }); + }); + + await waitFor(() => { + expect(screen.queryByText('keep loading events')).not.toBeInTheDocument(); + }); + }); + + it('should render grouped table by default when ungroupedHistoryViewEnabled is not set and user preference is false', async () => { + await setup({ ungroupedViewPreference: false }); + expect( + await screen.findByTestId('workflow-history-grouped-table') ).toBeInTheDocument(); expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument(); }); - it('should render grouped table by default when ungroupedHistoryViewEnabled is not set and user preference is null', () => { - setup({ ungroupedViewUserPreference: null }); + it('should render grouped table by default when ungroupedHistoryViewEnabled is not set and user preference is null', async () => { + await setup({ ungroupedViewPreference: null }); expect( - screen.getByTestId('workflow-history-grouped-table') + await screen.findByTestId('workflow-history-grouped-table') ).toBeInTheDocument(); expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument(); }); - it('should render ungrouped table when ungroupedHistoryViewEnabled query param is true', () => { - setup({ - queryParams: { + it('should render ungrouped table when ungroupedHistoryViewEnabled query param is true', async () => { + await setup({ + pageQueryParamsValues: { ungroupedHistoryViewEnabled: true, }, }); - expect(screen.getByText('WIP: ungrouped table')).toBeInTheDocument(); + expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); expect( screen.queryByTestId('workflow-history-grouped-table') ).not.toBeInTheDocument(); }); - it('should render grouped table when ungroupedHistoryViewEnabled query param is false', () => { - setup({ - queryParams: { + it('should render grouped table when ungroupedHistoryViewEnabled query param is false', async () => { + await setup({ + pageQueryParamsValues: { ungroupedHistoryViewEnabled: false, }, }); expect( - screen.getByTestId('workflow-history-grouped-table') + await screen.findByTestId('workflow-history-grouped-table') ).toBeInTheDocument(); expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument(); }); - it('should render ungrouped table when user preference is true and query param is not set', () => { - setup({ ungroupedViewUserPreference: true }); - expect(screen.getByText('WIP: ungrouped table')).toBeInTheDocument(); + it('should render ungrouped table when user preference is true and query param is not set', async () => { + await setup({ ungroupedViewPreference: true }); + expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); expect( screen.queryByTestId('workflow-history-grouped-table') ).not.toBeInTheDocument(); }); - it('should call setUngroupedViewUserPreference and setQueryParams when toggle is clicked from grouped to ungrouped', async () => { - const { user } = setup({ - queryParams: { - ungroupedHistoryViewEnabled: false, - }, + it('should show ungrouped table when query param overrides preference', async () => { + await setup({ + pageQueryParamsValues: { ungroupedHistoryViewEnabled: true }, + ungroupedViewPreference: false, }); + // Should show ungrouped table even though preference is false + expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); + }); + + it('should use user preference when query param is undefined for ungrouped view', async () => { + await setup({ + pageQueryParamsValues: { ungroupedHistoryViewEnabled: undefined }, + ungroupedViewPreference: true, + }); + + // Should use preference (true) when query param is undefined + expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); + }); + + it('should call setUngroupedViewUserPreference and setQueryParams when toggle is clicked from grouped to ungrouped', async () => { + const { user, mockSetUngroupedViewUserPreference, mockSetQueryParams } = + await setup({ + pageQueryParamsValues: { + ungroupedHistoryViewEnabled: false, + }, + }); + const toggleButton = screen.getByTestId('toggle-group-mode'); await user.click(toggleButton); @@ -114,11 +251,12 @@ describe(WorkflowHistoryV2.name, () => { }); it('should call setUngroupedViewUserPreference and setQueryParams when toggle is clicked from ungrouped to grouped', async () => { - const { user } = setup({ - queryParams: { - ungroupedHistoryViewEnabled: true, - }, - }); + const { user, mockSetUngroupedViewUserPreference, mockSetQueryParams } = + await setup({ + pageQueryParamsValues: { + ungroupedHistoryViewEnabled: true, + }, + }); const toggleButton = screen.getByTestId('toggle-group-mode'); await user.click(toggleButton); @@ -130,63 +268,146 @@ describe(WorkflowHistoryV2.name, () => { }); }); -function setup({ - params = { - cluster: 'test-cluster', - domain: 'test-domain', - workflowId: 'test-workflow-id', - runId: 'test-run-id', - }, - queryParams = { - historyEventTypes: undefined, - historyEventStatuses: undefined, - historySelectedEventId: undefined, - ungroupedHistoryViewEnabled: undefined, - }, - ungroupedViewUserPreference = false, +async function setup({ + error, + summaryError, + resolveLoadMoreManually, + pageQueryParamsValues = {}, + hasNextPage, + ungroupedViewPreference, }: { - params?: WorkflowPageParams; - queryParams?: { - historyEventTypes?: unknown; - historyEventStatuses?: unknown; - historySelectedEventId?: unknown; - ungroupedHistoryViewEnabled?: boolean; - activeFiltersCount?: number; - }; - ungroupedViewUserPreference?: boolean | null; -} = {}) { + error?: boolean; + summaryError?: boolean; + resolveLoadMoreManually?: boolean; + pageQueryParamsValues?: Partial< + PageQueryParamValues + >; + hasNextPage?: boolean; + ungroupedViewPreference?: boolean | null; +}) { const user = userEvent.setup(); + const mockSetQueryParams = jest.fn(); jest.spyOn(usePageFiltersModule, 'default').mockReturnValue({ - resetAllFilters: mockResetAllFilters, - activeFiltersCount: queryParams.activeFiltersCount ?? 0, - queryParams: { - historyEventTypes: queryParams.historyEventTypes, - historyEventStatuses: queryParams.historyEventStatuses, - historySelectedEventId: queryParams.historySelectedEventId, - ungroupedHistoryViewEnabled: queryParams.ungroupedHistoryViewEnabled, - }, + queryParams: pageQueryParamsValues, setQueryParams: mockSetQueryParams, + activeFiltersCount: 0, + resetAllFilters: mockResetAllFilters, }); - const mockContextValue = { - ungroupedViewUserPreference, - setUngroupedViewUserPreference: mockSetUngroupedViewUserPreference, - }; + const mockSetUngroupedViewUserPreference = jest.fn(); + + type ReqResolver = (r: GetWorkflowHistoryResponse) => void; + let requestResolver: ReqResolver = () => {}; + let requestRejector = () => {}; + const getRequestResolver = () => requestResolver; + const getRequestRejector = () => requestRejector; + let requestIndex = -1; const renderResult = render( - - + - + > + + + , + { + endpointsMocks: [ + { + path: '/api/domains/:domain/:cluster/workflows/:workflowId/:runId/history', + httpMethod: 'GET', + mockOnce: false, + httpResolver: async () => { + requestIndex = requestIndex + 1; + + if (requestIndex > 0 && resolveLoadMoreManually) { + return await new Promise((resolve, reject) => { + requestResolver = (result: GetWorkflowHistoryResponse) => + resolve(HttpResponse.json(result, { status: 200 })); + + requestRejector = () => + reject( + HttpResponse.json( + { message: 'Failed to fetch workflow history' }, + { status: 500 } + ) + ); + }); + } else { + if (error) { + return HttpResponse.json( + { message: 'Failed to fetch workflow history' }, + { status: 500 } + ); + } + + const events: Array = completedActivityTaskEvents; + + return HttpResponse.json( + { + history: { + events, + }, + archived: false, + nextPageToken: hasNextPage ? 'mock-next-page-token' : '', + rawHistory: [], + } satisfies GetWorkflowHistoryResponse, + { status: 200 } + ); + } + }, + }, + { + path: '/api/domains/:domain/:cluster/workflows/:workflowId/:runId', + httpMethod: 'GET', + ...(summaryError + ? { + httpResolver: () => { + return HttpResponse.json( + { message: 'Failed to fetch workflow summary' }, + { status: 500 } + ); + }, + } + : { + jsonResponse: mockDescribeWorkflowResponse, + }), + }, + ], + }, + { + wrapper: ({ children }) => ( + + {children} + + ), + } ); + if (!error && !summaryError) + await waitForElementToBeRemoved(() => + screen.queryAllByText('Suspense placeholder') + ); return { user, + getRequestResolver, + getRequestRejector, ...renderResult, + mockSetQueryParams, + mockSetUngroupedViewUserPreference, }; } diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx b/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx index a4c96d619..b5858ff2b 100644 --- a/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx @@ -1,14 +1,34 @@ import React from 'react'; -import { render, screen } from '@/test-utils/rtl'; +import { VirtuosoMockContext } from 'react-virtuoso'; + +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import { RequestError } from '@/utils/request/request-error'; +import { mockActivityEventGroup } from '@/views/workflow-history/__fixtures__/workflow-history-event-groups'; +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; import WorkflowHistoryGroupedTable from '../workflow-history-grouped-table'; +jest.mock( + '@/views/workflow-history/workflow-history-timeline-load-more/workflow-history-timeline-load-more', + () => + jest.fn(({ error, hasNextPage, isFetchingNextPage, fetchNextPage }) => ( +
+ {error &&
Error loading more
} + {hasNextPage &&
Has more
} + {isFetchingNextPage &&
Fetching...
} + +
+ )) +); + describe(WorkflowHistoryGroupedTable.name, () => { it('should render all column headers in correct order', () => { setup(); - expect(screen.getByText('ID')).toBeInTheDocument(); expect(screen.getByText('Event group')).toBeInTheDocument(); expect(screen.getByText('Status')).toBeInTheDocument(); expect(screen.getByText('Time')).toBeInTheDocument(); @@ -19,14 +39,108 @@ describe(WorkflowHistoryGroupedTable.name, () => { it('should apply grid layout to table header', () => { setup(); - const header = screen.getByText('ID').parentElement; + const header = screen.getByText('Event group').parentElement; expect(header).toHaveStyle({ display: 'grid', gridTemplateColumns: '0.3fr 2fr 1fr 1.2fr 1fr 3fr minmax(0, 70px)', }); }); + + it('should render event groups data', () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + ['group-1', mockActivityEventGroup], + ]; + setup({ eventGroupsById: mockEventGroups }); + + expect( + screen.getByText(JSON.stringify(mockActivityEventGroup)) + ).toBeInTheDocument(); + }); + + it('should render timeline load more component with correct props', () => { + const mockFetchMoreEvents = jest.fn(); + + setup({ + error: new RequestError('Test error', '/mock-history-url', 500), + hasMoreEvents: true, + isFetchingMoreEvents: false, + fetchMoreEvents: mockFetchMoreEvents, + }); + + expect(screen.getByTestId('timeline-load-more')).toBeInTheDocument(); + expect(screen.getByTestId('has-next-page')).toBeInTheDocument(); + }); + + it('should show error state in load more component', () => { + setup({ error: new RequestError('Test error', '/mock-history-url', 500) }); + + expect(screen.getByTestId('load-more-error')).toBeInTheDocument(); + }); + + it('should show fetching state in load more component', () => { + setup({ isFetchingMoreEvents: true }); + + expect(screen.getByTestId('is-fetching')).toBeInTheDocument(); + }); + + it('should call fetchMoreEvents when fetch button is clicked', async () => { + const mockFetchMoreEvents = jest.fn(); + const { user } = setup({ fetchMoreEvents: mockFetchMoreEvents }); + + const fetchButton = screen.getByTestId('fetch-more-button'); + await user.click(fetchButton); + + expect(mockFetchMoreEvents).toHaveBeenCalledTimes(1); + }); }); -function setup() { - return render(); +function setup({ + eventGroupsById = [], + error = null, + hasMoreEvents = false, + isFetchingMoreEvents = false, + fetchMoreEvents = jest.fn(), + setVisibleRange = jest.fn(), + initialStartIndex, +}: { + eventGroupsById?: Array<[string, HistoryEventsGroup]>; + error?: RequestError | null; + hasMoreEvents?: boolean; + isFetchingMoreEvents?: boolean; + fetchMoreEvents?: () => void; + setVisibleRange?: ({ + startIndex, + endIndex, + }: { + startIndex: number; + endIndex: number; + }) => void; + initialStartIndex?: number; +} = {}) { + const virtuosoRef = { current: null }; + const user = userEvent.setup(); + + render( + + + + ); + + return { + user, + virtuosoRef, + mockFetchMoreEvents: fetchMoreEvents, + mockSetVisibleRange: setVisibleRange, + }; } diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx index f5191bedd..1c6cbd8a7 100644 --- a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx @@ -1,20 +1,82 @@ +import { Virtuoso } from 'react-virtuoso'; + +import WorkflowHistoryTimelineLoadMore from '@/views/workflow-history/workflow-history-timeline-load-more/workflow-history-timeline-load-more'; + import { styled } from './workflow-history-grouped-table.styles'; +import { type Props } from './workflow-history-grouped-table.types'; -/** - * To be used in History v2 - */ -export default function WorkflowHistoryGroupedTable() { +export default function WorkflowHistoryGroupedTable({ + eventGroupsById, + virtuosoRef, + initialStartIndex, + setVisibleRange, + error, + hasMoreEvents, + fetchMoreEvents, + isFetchingMoreEvents, +}: Props) { return ( <> -
ID
+
Event group
Status
Time
Duration
Details
- {/* TODO @adhityamamallan: Add table body with Virtuoso with new design*/} + ( +
{JSON.stringify(group)}
+ // { + // if (group.resetToDecisionEventId) { + // setResetToDecisionEventId(group.resetToDecisionEventId); + // } + // }} + // selected={group.events.some( + // (e) => e.eventId === queryParams.historySelectedEventId + // )} + // workflowCloseStatus={workflowExecutionInfo?.closeStatus} + // workflowIsArchived={workflowExecutionInfo?.isArchived || false} + // workflowCloseTimeMs={workflowCloseTimeMs} + // /> + )} + components={{ + Footer: () => ( + + ), + }} + /> ); } diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.types.ts b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.types.ts new file mode 100644 index 000000000..f53cb41b2 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.types.ts @@ -0,0 +1,24 @@ +import { type RefObject } from 'react'; + +import { type VirtuosoHandle } from 'react-virtuoso'; + +import { type RequestError } from '@/utils/request/request-error'; +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; + +export type Props = { + eventGroupsById: Array<[string, HistoryEventsGroup]>; + virtuosoRef: RefObject; + initialStartIndex?: number; + setVisibleRange: ({ + startIndex, + endIndex, + }: { + startIndex: number; + endIndex: number; + }) => void; + // Props to fetch more history + error: RequestError | null; + hasMoreEvents: boolean; + fetchMoreEvents: () => void; + isFetchingMoreEvents: boolean; +}; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index 57344b3c7..ac2afcc91 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -1,18 +1,35 @@ -import { useCallback, useContext, useMemo } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; + +import { type VirtuosoHandle } from 'react-virtuoso'; import usePageFilters from '@/components/page-filters/hooks/use-page-filters'; +import SectionLoadingIndicator from '@/components/section-loading-indicator/section-loading-indicator'; +import useThrottledState from '@/hooks/use-throttled-state'; import decodeUrlParams from '@/utils/decode-url-params'; +import sortBy from '@/utils/sort-by'; import { WORKFLOW_HISTORY_PAGE_SIZE_CONFIG } from '../workflow-history/config/workflow-history-page-size.config'; +import compareUngroupedEvents from '../workflow-history/helpers/compare-ungrouped-events'; +import getSortableEventId from '../workflow-history/helpers/get-sortable-event-id'; +import pendingActivitiesInfoToEvents from '../workflow-history/helpers/pending-activities-info-to-events'; +import pendingDecisionInfoToEvent from '../workflow-history/helpers/pending-decision-info-to-event'; +import useInitialSelectedEvent from '../workflow-history/hooks/use-initial-selected-event'; +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 { type WorkflowHistoryUngroupedEventInfo } from '../workflow-history/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types'; 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'; import workflowHistoryFiltersConfig from './config/workflow-history-filters.config'; import WorkflowHistoryGroupedTable from './workflow-history-grouped-table/workflow-history-grouped-table'; import WorkflowHistoryHeader from './workflow-history-header/workflow-history-header'; import { styled } from './workflow-history-v2.styles'; -import { type Props } from './workflow-history-v2.types'; +import { + type VisibleHistoryRanges, + type Props, +} from './workflow-history-v2.types'; export default function WorkflowHistoryV2({ params }: Props) { const decodedParams = decodeUrlParams(params); @@ -34,6 +51,97 @@ export default function WorkflowHistoryV2({ params }: Props) { pageFiltersConfig: workflowHistoryFiltersConfig, }); + const { + eventGroups, + updateEvents: updateGrouperEvents, + updatePendingEvents: updateGrouperPendingEvents, + } = useWorkflowHistoryGrouper(); + + const { + historyQuery, + startLoadingHistory, + stopLoadingHistory, + fetchSingleNextPage, + } = useWorkflowHistoryFetcher( + { + domain: wfHistoryRequestArgs.domain, + cluster: wfHistoryRequestArgs.cluster, + workflowId: wfHistoryRequestArgs.workflowId, + runId: wfHistoryRequestArgs.runId, + pageSize: wfHistoryRequestArgs.pageSize, + waitForNewEvent: wfHistoryRequestArgs.waitForNewEvent, + }, + updateGrouperEvents, + 2000 + ); + + const { data: wfExecutionDescription } = useSuspenseDescribeWorkflow({ + ...params, + }); + const { + data: result, + hasNextPage, + isFetchingNextPage, + isLoading, + isPending, + error, + isFetchNextPageError, + } = historyQuery; + + useEffect(() => { + const pendingStartActivities = pendingActivitiesInfoToEvents( + wfExecutionDescription.pendingActivities + ); + const pendingStartDecision = wfExecutionDescription.pendingDecision + ? pendingDecisionInfoToEvent(wfExecutionDescription.pendingDecision) + : null; + + updateGrouperPendingEvents({ + pendingStartActivities, + pendingStartDecision, + }); + }, [wfExecutionDescription, updateGrouperPendingEvents]); + + const filteredEventGroupsById = useMemo( + () => + sortBy( + Object.entries(eventGroups), + ([_, { firstEventId }]) => getSortableEventId(firstEventId), + 'ASC' + ).filter(([_, g]) => + workflowHistoryFiltersConfig.every((f) => + f.filterFunc(g, { + historyEventTypes: queryParams.historyEventTypes, + historyEventStatuses: queryParams.historyEventStatuses, + }) + ) + ), + [ + eventGroups, + queryParams.historyEventTypes, + queryParams.historyEventStatuses, + ] + ); + + const sortedUngroupedEvents: Array = + useMemo( + () => + filteredEventGroupsById + .map(([_, group]) => [ + ...group.events.map((event, index) => ({ + event, + eventMetadata: group.eventsMetadata[index], + label: group.label, + shortLabel: group.shortLabel, + id: event.eventId ?? event.computedEventId, + canReset: group.resetToDecisionEventId === event.eventId, + })), + ]) + .flat(1) + .sort(compareUngroupedEvents), + [filteredEventGroupsById] + ); + const { ungroupedViewUserPreference, setUngroupedViewUserPreference } = useContext(WorkflowHistoryContext); @@ -72,6 +180,103 @@ export default function WorkflowHistoryV2({ params }: Props) { setUngroupedViewUserPreference, ]); + const [visibleGroupsRange, setVisibleGroupsRange] = + useThrottledState( + { + groupedStartIndex: -1, + groupedEndIndex: -1, + ungroupedStartIndex: -1, + ungroupedEndIndex: -1, + }, + 700, + { + leading: false, + trailing: true, + } + ); + + const { + initialEventFound, + initialEventGroupIndex, + shouldSearchForInitialEvent, + } = useInitialSelectedEvent({ + selectedEventId: queryParams.historySelectedEventId, + eventGroups, + filteredEventGroupsEntries: filteredEventGroupsById, + }); + + const isLastPageEmpty = + result?.pages?.[result?.pages?.length - 1]?.history?.events.length === 0; + + const visibleGroupsHasMissingEvents = useMemo( + () => + filteredEventGroupsById + .slice( + visibleGroupsRange.groupedStartIndex, + visibleGroupsRange.groupedEndIndex + 1 + ) + .some(([_, { hasMissingEvents }]) => hasMissingEvents), + + [filteredEventGroupsById, visibleGroupsRange] + ); + + const ungroupedViewShouldLoadMoreEvents = useMemo( + () => + isUngroupedHistoryViewEnabled && + // Pre-load more as we're approaching the end + sortedUngroupedEvents.length - visibleGroupsRange.ungroupedEndIndex < + WORKFLOW_HISTORY_PAGE_SIZE_CONFIG * 1, + [ + isUngroupedHistoryViewEnabled, + sortedUngroupedEvents.length, + visibleGroupsRange.ungroupedEndIndex, + ] + ); + + const keepLoadingMoreEvents = useMemo(() => { + if (shouldSearchForInitialEvent && !initialEventFound) return true; + if (visibleGroupsHasMissingEvents) return true; + if (ungroupedViewShouldLoadMoreEvents) return true; + return false; + }, [ + shouldSearchForInitialEvent, + initialEventFound, + visibleGroupsHasMissingEvents, + ungroupedViewShouldLoadMoreEvents, + ]); + + const manualFetchNextPage = useCallback(() => { + if (keepLoadingMoreEvents) { + startLoadingHistory(); + } else { + fetchSingleNextPage(); + } + }, [keepLoadingMoreEvents, startLoadingHistory, fetchSingleNextPage]); + + useEffect(() => { + if (keepLoadingMoreEvents) { + startLoadingHistory(); + } else { + stopLoadingHistory(); + } + }, [keepLoadingMoreEvents, startLoadingHistory, stopLoadingHistory]); + + const reachedEndOfAvailableHistory = + (!hasNextPage && !isPending) || + (hasNextPage && isLastPageEmpty && !isFetchNextPageError); + + const contentIsLoading = + isLoading || + (shouldSearchForInitialEvent && + !initialEventFound && + !reachedEndOfAvailableHistory); + + const groupedTableVirtuosoRef = useRef(null); + + if (contentIsLoading) { + return ; + } + return ( WIP: ungrouped table
) : ( - + + setVisibleGroupsRange((prevRange) => ({ + ...prevRange, + groupedStartIndex: startIndex, + groupedEndIndex: endIndex, + })) + } + error={error} + hasMoreEvents={hasNextPage} + fetchMoreEvents={manualFetchNextPage} + isFetchingMoreEvents={isFetchingNextPage} + /> )} 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 52201ab84..4205b139d 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.types.ts +++ b/src/views/workflow-history-v2/workflow-history-v2.types.ts @@ -2,8 +2,9 @@ import { type WorkflowPageTabContentProps } from '../workflow-page/workflow-page export type Props = WorkflowPageTabContentProps; -export type WorkflowHistoryEventFilteringTypeColors = { - content: string; - background: string; - backgroundHighlighted: string; +export type VisibleHistoryRanges = { + groupedStartIndex: number; + groupedEndIndex: number; + ungroupedStartIndex: number; + ungroupedEndIndex: number; }; From 6992b528c1dd3b726bdbd0aa8ec4e1e7c0335a01 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 24 Nov 2025 17:29:01 +0100 Subject: [PATCH 2/4] Address copilot comments Signed-off-by: Adhitya Mamallan --- ...ow-history-set-range-throttle-ms.config.ts | 3 ++ .../workflow-history-grouped-table.tsx | 28 ++------------- .../workflow-history-v2.tsx | 34 ++++++------------- .../workflow-history-v2.types.ts | 6 ++++ 4 files changed, 22 insertions(+), 49 deletions(-) create mode 100644 src/views/workflow-history-v2/config/workflow-history-set-range-throttle-ms.config.ts diff --git a/src/views/workflow-history-v2/config/workflow-history-set-range-throttle-ms.config.ts b/src/views/workflow-history-v2/config/workflow-history-set-range-throttle-ms.config.ts new file mode 100644 index 000000000..f5667dd6c --- /dev/null +++ b/src/views/workflow-history-v2/config/workflow-history-set-range-throttle-ms.config.ts @@ -0,0 +1,3 @@ +const WORKFLOW_HISTORY_SET_RANGE_THROTTLE_MS_CONFIG = 700; + +export default WORKFLOW_HISTORY_SET_RANGE_THROTTLE_MS_CONFIG; diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx index 1c6cbd8a7..17313aed5 100644 --- a/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/workflow-history-grouped-table.tsx @@ -40,32 +40,8 @@ export default function WorkflowHistoryGroupedTable({ behavior: 'auto', }, })} - itemContent={(_, [__, group]) => ( -
{JSON.stringify(group)}
- // { - // if (group.resetToDecisionEventId) { - // setResetToDecisionEventId(group.resetToDecisionEventId); - // } - // }} - // selected={group.events.some( - // (e) => e.eventId === queryParams.historySelectedEventId - // )} - // workflowCloseStatus={workflowExecutionInfo?.closeStatus} - // workflowIsArchived={workflowExecutionInfo?.isArchived || false} - // workflowCloseTimeMs={workflowCloseTimeMs} - // /> - )} + // TODO: update this with the actual implementation for groupedEntry + itemContent={(_, [__, group]) =>
{JSON.stringify(group)}
} components={{ Footer: () => ( = - useMemo( - () => - filteredEventGroupsById - .map(([_, group]) => [ - ...group.events.map((event, index) => ({ - event, - eventMetadata: group.eventsMetadata[index], - label: group.label, - shortLabel: group.shortLabel, - id: event.eventId ?? event.computedEventId, - canReset: group.resetToDecisionEventId === event.eventId, - })), - ]) - .flat(1) - .sort(compareUngroupedEvents), - [filteredEventGroupsById] - ); + const filteredEventsCount = useMemo( + () => + filteredEventGroupsById.reduce((acc, [_, group]) => { + return acc + group.events.length; + }, 0), + [filteredEventGroupsById] + ); const { ungroupedViewUserPreference, setUngroupedViewUserPreference } = useContext(WorkflowHistoryContext); @@ -188,7 +176,7 @@ export default function WorkflowHistoryV2({ params }: Props) { ungroupedStartIndex: -1, ungroupedEndIndex: -1, }, - 700, + WORKFLOW_HISTORY_SET_RANGE_THROTTLE_MS_CONFIG, { leading: false, trailing: true, @@ -224,11 +212,11 @@ export default function WorkflowHistoryV2({ params }: Props) { () => isUngroupedHistoryViewEnabled && // Pre-load more as we're approaching the end - sortedUngroupedEvents.length - visibleGroupsRange.ungroupedEndIndex < + filteredEventsCount - visibleGroupsRange.ungroupedEndIndex < WORKFLOW_HISTORY_PAGE_SIZE_CONFIG * 1, [ isUngroupedHistoryViewEnabled, - sortedUngroupedEvents.length, + filteredEventsCount, visibleGroupsRange.ungroupedEndIndex, ] ); 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 4205b139d..a96195ffc 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.types.ts +++ b/src/views/workflow-history-v2/workflow-history-v2.types.ts @@ -2,6 +2,12 @@ import { type WorkflowPageTabContentProps } from '../workflow-page/workflow-page export type Props = WorkflowPageTabContentProps; +export type WorkflowHistoryEventFilteringTypeColors = { + content: string; + background: string; + backgroundHighlighted: string; +}; + export type VisibleHistoryRanges = { groupedStartIndex: number; groupedEndIndex: number; From c00499c8387c423ccfdce77c9b775d2625f45465 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 24 Nov 2025 17:35:42 +0100 Subject: [PATCH 3/4] Address one last comment Signed-off-by: Adhitya Mamallan --- .../config/workflow-history-fetch-events-throttle-ms.config.ts | 3 +++ src/views/workflow-history-v2/workflow-history-v2.tsx | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts diff --git a/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts b/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts new file mode 100644 index 000000000..fd845f0b5 --- /dev/null +++ b/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts @@ -0,0 +1,3 @@ +const WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG = 700; + +export default WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_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 149dc6ecb..5900f657a 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -20,6 +20,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_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_SET_RANGE_THROTTLE_MS_CONFIG from './config/workflow-history-set-range-throttle-ms.config'; import WorkflowHistoryGroupedTable from './workflow-history-grouped-table/workflow-history-grouped-table'; @@ -71,7 +72,7 @@ export default function WorkflowHistoryV2({ params }: Props) { waitForNewEvent: wfHistoryRequestArgs.waitForNewEvent, }, updateGrouperEvents, - 2000 + WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG ); const { data: wfExecutionDescription } = useSuspenseDescribeWorkflow({ From 061d0f3d992d5f0fb3ef7b8f0e3f95b254a40e56 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Tue, 25 Nov 2025 15:37:32 +0100 Subject: [PATCH 4/4] Address comments Signed-off-by: Adhitya Mamallan --- ...workflow-history-fetch-events-throttle-ms.config.ts | 2 +- .../__tests__/workflow-history-grouped-table.test.tsx | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts b/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts index fd845f0b5..ea9d20e1d 100644 --- a/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts +++ b/src/views/workflow-history-v2/config/workflow-history-fetch-events-throttle-ms.config.ts @@ -1,3 +1,3 @@ -const WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG = 700; +const WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG = 2000; export default WORKFLOW_HISTORY_FETCH_EVENTS_THROTTLE_MS_CONFIG; diff --git a/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx b/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx index b5858ff2b..ac6171b33 100644 --- a/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx +++ b/src/views/workflow-history-v2/workflow-history-grouped-table/__tests__/workflow-history-grouped-table.test.tsx @@ -82,16 +82,6 @@ describe(WorkflowHistoryGroupedTable.name, () => { expect(screen.getByTestId('is-fetching')).toBeInTheDocument(); }); - - it('should call fetchMoreEvents when fetch button is clicked', async () => { - const mockFetchMoreEvents = jest.fn(); - const { user } = setup({ fetchMoreEvents: mockFetchMoreEvents }); - - const fetchButton = screen.getByTestId('fetch-more-button'); - await user.click(fetchButton); - - expect(mockFetchMoreEvents).toHaveBeenCalledTimes(1); - }); }); function setup({