From 4db12a96bfd5390bc6b1b8269124eedf46c46597 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Thu, 4 Dec 2025 16:35:02 +0100 Subject: [PATCH 1/4] Add basic ungrouped table Signed-off-by: Adhitya Mamallan --- .../__tests__/workflow-history-v2.test.tsx | 36 ++- ...workflow-history-ungrouped-event.styles.ts | 9 + .../workflow-history-ungrouped-event.tsx | 8 + .../workflow-history-ungrouped-event.types.ts | 19 ++ .../workflow-history-ungrouped-table.test.tsx | 265 ++++++++++++++++++ ...kflow-history-ungrouped-table.constants.ts | 3 + ...workflow-history-ungrouped-table.styles.ts | 22 ++ .../workflow-history-ungrouped-table.tsx | 117 ++++++++ .../workflow-history-ungrouped-table.types.ts | 82 ++++++ .../workflow-history-v2.tsx | 24 +- 10 files changed, 577 insertions(+), 8 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/__tests__/workflow-history-ungrouped-table.test.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.constants.ts create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.styles.ts create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-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 fbe5cff8c..5c683ec72 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 @@ -74,6 +74,14 @@ jest.mock( )) ); +jest.mock( + '../workflow-history-ungrouped-table/workflow-history-ungrouped-table', + () => + jest.fn(() => ( +
Ungrouped Table
+ )) +); + jest.mock('@/utils/decode-url-params', () => jest.fn((params) => params)); const mockResetAllFilters = jest.fn(); @@ -170,7 +178,9 @@ describe(WorkflowHistoryV2.name, () => { expect( await screen.findByTestId('workflow-history-grouped-table') ).toBeInTheDocument(); - expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('workflow-history-ungrouped-table') + ).not.toBeInTheDocument(); }); it('should render grouped table by default when ungroupedHistoryViewEnabled is not set and user preference is null', async () => { @@ -178,7 +188,9 @@ describe(WorkflowHistoryV2.name, () => { expect( await screen.findByTestId('workflow-history-grouped-table') ).toBeInTheDocument(); - expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('workflow-history-ungrouped-table') + ).not.toBeInTheDocument(); }); it('should render ungrouped table when ungroupedHistoryViewEnabled query param is true', async () => { @@ -187,7 +199,9 @@ describe(WorkflowHistoryV2.name, () => { ungroupedHistoryViewEnabled: true, }, }); - expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); + expect( + await screen.findByTestId('workflow-history-ungrouped-table') + ).toBeInTheDocument(); expect( screen.queryByTestId('workflow-history-grouped-table') ).not.toBeInTheDocument(); @@ -202,12 +216,16 @@ describe(WorkflowHistoryV2.name, () => { expect( await screen.findByTestId('workflow-history-grouped-table') ).toBeInTheDocument(); - expect(screen.queryByText('WIP: ungrouped table')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('workflow-history-ungrouped-table') + ).not.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( + await screen.findByTestId('workflow-history-ungrouped-table') + ).toBeInTheDocument(); expect( screen.queryByTestId('workflow-history-grouped-table') ).not.toBeInTheDocument(); @@ -220,7 +238,9 @@ describe(WorkflowHistoryV2.name, () => { }); // Should show ungrouped table even though preference is false - expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); + expect( + await screen.findByTestId('workflow-history-ungrouped-table') + ).toBeInTheDocument(); }); it('should use user preference when query param is undefined for ungrouped view', async () => { @@ -230,7 +250,9 @@ describe(WorkflowHistoryV2.name, () => { }); // Should use preference (true) when query param is undefined - expect(await screen.findByText('WIP: ungrouped table')).toBeInTheDocument(); + expect( + await screen.findByTestId('workflow-history-ungrouped-table') + ).toBeInTheDocument(); }); it('should call setUngroupedViewUserPreference and setQueryParams when toggle is clicked from grouped to ungrouped', async () => { diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.styles.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.styles.ts new file mode 100644 index 000000000..8a65a8bd9 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.styles.ts @@ -0,0 +1,9 @@ +import { styled as createStyled, type Theme } from 'baseui'; + +export const styled = { + TempContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + ...$theme.typography.MonoParagraphXSmall, + padding: $theme.sizing.scale300, + ...$theme.borders.border100, + })), +}; diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.tsx b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.tsx new file mode 100644 index 000000000..47d543dc9 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.tsx @@ -0,0 +1,8 @@ +import { styled } from './workflow-history-ungrouped-event.styles'; +import { type Props } from './workflow-history-ungrouped-event.types'; + +export default function WorkflowHistoryUngroupedEvent({ eventInfo }: Props) { + return ( + {JSON.stringify(eventInfo)} + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts new file mode 100644 index 000000000..ccfb609ea --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts @@ -0,0 +1,19 @@ +import { type Timestamp } from '@/__generated__/proto-ts/google/protobuf/Timestamp'; +import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; + +import { type UngroupedEventInfo } from '../workflow-history-ungrouped-table/workflow-history-ungrouped-table.types'; + +export type Props = { + // Core data props + eventInfo: UngroupedEventInfo; + workflowStartTime: Timestamp | null; + decodedPageUrlParams: WorkflowPageTabsParams; + + // Expansion state + isExpanded: boolean; + toggleIsExpanded: () => void; + + // UI behavior + animateOnEnter?: boolean; + onReset?: () => void; +}; diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/__tests__/workflow-history-ungrouped-table.test.tsx b/src/views/workflow-history-v2/workflow-history-ungrouped-table/__tests__/workflow-history-ungrouped-table.test.tsx new file mode 100644 index 000000000..9d4744566 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/__tests__/workflow-history-ungrouped-table.test.tsx @@ -0,0 +1,265 @@ +import React from 'react'; + +import { VirtuosoMockContext } from 'react-virtuoso'; + +import { render, screen, userEvent, waitFor } from '@/test-utils/rtl'; + +import { type 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 { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; + +import WorkflowHistoryUngroupedTable from '../workflow-history-ungrouped-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...
} + +
+ )) +); + +jest.mock( + '../../workflow-history-ungrouped-event/workflow-history-ungrouped-event', + () => + jest.fn( + ({ + eventInfo, + isExpanded, + toggleIsExpanded, + onReset, + animateOnEnter, + }) => ( +
+ +
Event ID: {eventInfo.id}
+
Label: {eventInfo.label}
+ {onReset && } +
+ ) + ) +); + +describe(WorkflowHistoryUngroupedTable.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(); + expect(screen.getByText('Duration')).toBeInTheDocument(); + expect(screen.getByText('Details')).toBeInTheDocument(); + }); + + it('should render events from event groups', () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + ['group-1', mockActivityEventGroup], + ]; + setup({ eventGroupsById: mockEventGroups }); + + const events = screen.getAllByTestId('workflow-history-ungrouped-event'); + expect(events.length).toBeGreaterThan(0); + expect(events[0]).toHaveTextContent('Event ID:'); + }); + + it('should render events with correct labels from groups', () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + ['group-1', mockActivityEventGroup], + ]; + setup({ eventGroupsById: mockEventGroups }); + + const events = screen.getAllByTestId('workflow-history-ungrouped-event'); + expect(events[0]).toHaveTextContent( + `Label: ${mockActivityEventGroup.label}` + ); + }); + + it('should handle event expansion toggle', async () => { + const { user, mockToggleIsEventExpanded } = setup({ + eventGroupsById: [['group-1', mockActivityEventGroup]], + }); + + const toggleButtons = screen.getAllByText('Toggle Event'); + await user.click(toggleButtons[0]); + + const firstEventId = + mockActivityEventGroup.events[0].eventId ?? + mockActivityEventGroup.events[0].computedEventId; + expect(mockToggleIsEventExpanded).toHaveBeenCalledWith(firstEventId); + }); + + it('should pass isExpanded state to events', () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + ['group-1', mockActivityEventGroup], + ]; + const firstEventId = + mockActivityEventGroup.events[0].eventId ?? + mockActivityEventGroup.events[0].computedEventId; + + setup({ + eventGroupsById: mockEventGroups, + getIsEventExpanded: jest.fn((id) => id === firstEventId), + }); + + const events = screen.getAllByTestId('workflow-history-ungrouped-event'); + expect(events[0]).toHaveAttribute('data-expanded', 'true'); + if (events.length > 1) { + expect(events[1]).toHaveAttribute('data-expanded', 'false'); + } + }); + + it('should pass hasMoreEvents to load more component', () => { + setup({ + hasMoreEvents: true, + isFetchingMoreEvents: false, + fetchMoreEvents: jest.fn(), + }); + + expect(screen.getByTestId('timeline-load-more')).toBeInTheDocument(); + expect(screen.getByTestId('has-next-page')).toBeInTheDocument(); + }); + + it('should pass animateOnEnter for selectedEventId', async () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + ['group-1', mockActivityEventGroup], + ]; + const firstEventId = + mockActivityEventGroup.events[0].eventId ?? + mockActivityEventGroup.events[0].computedEventId; + + setup({ + eventGroupsById: mockEventGroups, + selectedEventId: firstEventId, + }); + + await waitFor(() => { + const events = screen.getAllByTestId('workflow-history-ungrouped-event'); + expect(events[0]).toHaveAttribute('data-animate-on-enter', 'true'); + }); + }); + + it('should call resetToDecisionEventId when reset button is clicked on resettable event', async () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + [ + 'group-1', + { + ...mockActivityEventGroup, + resetToDecisionEventId: mockActivityEventGroup.events[0].eventId, + }, + ], + ]; + const { user, mockResetToDecisionEventId } = setup({ + eventGroupsById: mockEventGroups, + }); + + const resetButtons = screen.getAllByText('Reset Event'); + await user.click(resetButtons[0]); + + const firstEventId = + mockActivityEventGroup.events[0].eventId ?? + mockActivityEventGroup.events[0].computedEventId; + expect(mockResetToDecisionEventId).toHaveBeenCalledWith(firstEventId); + }); + + it('should not show reset button for non-resettable events', () => { + const mockEventGroups: Array<[string, HistoryEventsGroup]> = [ + [ + 'group-1', + { + ...mockActivityEventGroup, + resetToDecisionEventId: undefined, + }, + ], + ]; + setup({ eventGroupsById: mockEventGroups }); + + expect(screen.queryByText('Reset Event')).not.toBeInTheDocument(); + }); +}); + +function setup({ + eventGroupsById = [], + error = null, + hasMoreEvents = false, + isFetchingMoreEvents = false, + fetchMoreEvents = jest.fn(), + setVisibleRange = jest.fn(), + initialStartIndex, + decodedPageUrlParams = { + domain: 'test-domain', + cluster: 'test-cluster', + workflowId: 'test-workflow-id', + runId: 'test-run-id', + workflowTab: 'history', + }, + selectedEventId, + getIsEventExpanded = jest.fn(() => false), + toggleIsEventExpanded = jest.fn(), + resetToDecisionEventId = jest.fn(), +}: { + eventGroupsById?: Array<[string, HistoryEventsGroup]>; + error?: RequestError | null; + hasMoreEvents?: boolean; + isFetchingMoreEvents?: boolean; + fetchMoreEvents?: () => void; + setVisibleRange?: ({ + startIndex, + endIndex, + }: { + startIndex: number; + endIndex: number; + }) => void; + initialStartIndex?: number; + decodedPageUrlParams?: WorkflowPageTabsParams; + selectedEventId?: string; + getIsEventExpanded?: (eventId: string) => boolean; + toggleIsEventExpanded?: (eventId: string) => void; + resetToDecisionEventId?: (decisionEventId: string) => void; +} = {}) { + const virtuosoRef = { current: null }; + const user = userEvent.setup(); + + render( + + + + ); + + return { + user, + virtuosoRef, + mockFetchMoreEvents: fetchMoreEvents, + mockSetVisibleRange: setVisibleRange, + mockToggleIsEventExpanded: toggleIsEventExpanded, + mockResetToDecisionEventId: resetToDecisionEventId, + }; +} diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.constants.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.constants.ts new file mode 100644 index 000000000..0d63bc6fc --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.constants.ts @@ -0,0 +1,3 @@ +// icon, ID, event group, status, time, duration, details, Reset button +export const WORKFLOW_HISTORY_UNGROUPED_GRID_TEMPLATE_COLUMNS = + 'minmax(0, 24px) 0.3fr 2fr 1fr 1.2fr 1fr 3fr minmax(0, 70px)'; diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.styles.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.styles.ts new file mode 100644 index 000000000..98c3d452f --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.styles.ts @@ -0,0 +1,22 @@ +import { styled as createStyled, type Theme } from 'baseui'; + +import { WORKFLOW_HISTORY_UNGROUPED_GRID_TEMPLATE_COLUMNS } from './workflow-history-ungrouped-table.constants'; + +export const styled = { + TableHeader: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'none', + [$theme.mediaQuery.medium]: { + // border thickness + accordion panel left padding + paddingLeft: `calc(2px + ${$theme.sizing.scale300})`, + // accordion panel right padding + border thickness + paddingRight: `calc(${$theme.sizing.scale300} + 2px)`, + paddingBottom: $theme.sizing.scale200, + ...$theme.typography.LabelXSmall, + color: $theme.colors.contentSecondary, + display: 'grid', + gridTemplateColumns: WORKFLOW_HISTORY_UNGROUPED_GRID_TEMPLATE_COLUMNS, + gap: $theme.sizing.scale600, + width: '100%', + }, + })), +}; diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx new file mode 100644 index 000000000..10bc54648 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx @@ -0,0 +1,117 @@ +import { useMemo } from 'react'; + +import { Virtuoso } from 'react-virtuoso'; + +import compareUngroupedEvents from '@/views/workflow-history/helpers/compare-ungrouped-events'; +import WorkflowHistoryTimelineLoadMore from '@/views/workflow-history/workflow-history-timeline-load-more/workflow-history-timeline-load-more'; + +import WorkflowHistoryUngroupedEvent from '../workflow-history-ungrouped-event/workflow-history-ungrouped-event'; + +import { styled } from './workflow-history-ungrouped-table.styles'; +import { + type UngroupedEventInfo, + type Props, +} from './workflow-history-ungrouped-table.types'; + +export default function WorkflowHistoryUngroupedTable({ + eventGroupsById, + virtuosoRef, + initialStartIndex, + setVisibleRange, + decodedPageUrlParams, + selectedEventId, + getIsEventExpanded, + toggleIsEventExpanded, + resetToDecisionEventId, + error, + hasMoreEvents, + fetchMoreEvents, + isFetchingMoreEvents, +}: Props) { + const eventsInfoFromGroups = useMemo>( + () => + eventGroupsById + .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), + [eventGroupsById] + ); + + const workflowStartTime = useMemo( + () => + eventsInfoFromGroups.length > 0 + ? eventsInfoFromGroups[0].event.eventTime + : null, + [eventsInfoFromGroups] + ); + + const maybeHighlightedEventId = useMemo( + () => eventsInfoFromGroups.findIndex((v) => v.id === selectedEventId), + [eventsInfoFromGroups, selectedEventId] + ); + + return ( + <> + +
+
ID
+
Event group
+
Status
+
Time
+
Duration
+
Details
+ + ( + toggleIsEventExpanded(eventInfo.id)} + animateOnEnter={eventInfo.id === selectedEventId} + {...(eventInfo.canReset + ? { onReset: () => resetToDecisionEventId(eventInfo.id) } + : {})} + /> + )} + {...(maybeHighlightedEventId !== -1 && { + initialTopMostItemIndex: maybeHighlightedEventId, + })} + components={{ + Footer: () => ( + + ), + }} + /> + + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts new file mode 100644 index 000000000..c0fbcfa8c --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts @@ -0,0 +1,82 @@ +import { type RefObject } from 'react'; + +import { type VirtuosoHandle } from 'react-virtuoso'; + +import { type RequestError } from '@/utils/request/request-error'; +import { + type ExtendedHistoryEvent, + type HistoryEventsGroup, + type HistoryGroupEventMetadata, +} from '@/views/workflow-history/workflow-history.types'; +import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; + +export type Props = { + // Data and state props + eventGroupsById: Array<[string, HistoryEventsGroup]>; + selectedEventId?: string; + decodedPageUrlParams: WorkflowPageTabsParams; + resetToDecisionEventId: (decisionEventId: string) => void; + + // React Query props + error: RequestError | null; + hasMoreEvents: boolean; + isFetchingMoreEvents: boolean; + fetchMoreEvents: () => void; + + // Event expansion state management + getIsEventExpanded: (eventId: string) => boolean; + toggleIsEventExpanded: (eventId: string) => void; + + // Virtualization props + setVisibleRange: ({ + startIndex, + endIndex, + }: { + startIndex: number; + endIndex: number; + }) => void; + initialStartIndex?: number; + virtuosoRef: RefObject; +}; + +export type UngroupedEventInfo = { + id: string; + label: string; + shortLabel?: string; + event: ExtendedHistoryEvent; + eventMetadata: HistoryGroupEventMetadata; + canReset?: boolean; +}; + +// import { type ListRange, type VirtuosoHandle } from 'react-virtuoso'; + +// import { type RequestError } from '@/utils/request/request-error'; +// import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; + +// import { +// type GetIsEventExpanded, +// type ToggleIsEventExpanded, +// } from '../hooks/use-event-expansion-toggle.types'; +// import { type WorkflowHistoryUngroupedEventInfo } from '../workflow-history-ungrouped-event/workflow-history-ungrouped-event.types'; + +// export type Props = { +// // Data and state props +// eventsInfo: Array; +// selectedEventId?: string; +// decodedPageUrlParams: WorkflowPageTabsParams; +// onResetToEventId: (eventId: string) => void; + +// // React Query props +// error: RequestError | null; +// hasMoreEvents: boolean; +// isFetchingMoreEvents: boolean; +// fetchMoreEvents: () => void; + +// // Event expansion state management +// getIsEventExpanded: GetIsEventExpanded; +// toggleIsEventExpanded: ToggleIsEventExpanded; + +// // Virtualization props +// onVisibleRangeChange: (r: ListRange) => void; +// virtuosoRef: React.RefObject; +// }; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index 80c7b1e52..94848be42 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -36,6 +36,7 @@ import workflowHistoryFiltersConfig from './config/workflow-history-filters.conf 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'; import WorkflowHistoryHeader from './workflow-history-header/workflow-history-header'; +import WorkflowHistoryUngroupedTable from './workflow-history-ungrouped-table/workflow-history-ungrouped-table'; import { styled } from './workflow-history-v2.styles'; import { type VisibleHistoryRanges, @@ -283,6 +284,7 @@ export default function WorkflowHistoryV2({ params }: Props) { !reachedEndOfAvailableHistory); const groupedTableVirtuosoRef = useRef(null); + const ungroupedTableVirtuosoRef = useRef(null); const workflowCloseTimeMs = workflowExecutionInfo?.closeTime ? parseGrpcTimestamp(workflowExecutionInfo?.closeTime) @@ -320,7 +322,27 @@ export default function WorkflowHistoryV2({ params }: Props) { /> {isUngroupedHistoryViewEnabled ? ( -
WIP: ungrouped table
+ + setVisibleGroupsRange((prevRange) => ({ + ...prevRange, + ungroupedStartIndex: startIndex, + ungroupedEndIndex: endIndex, + })) + } + decodedPageUrlParams={decodedParams} + selectedEventId={queryParams.historySelectedEventId} + resetToDecisionEventId={setResetToDecisionEventId} + getIsEventExpanded={getIsItemExpanded} + toggleIsEventExpanded={toggleIsItemExpanded} + error={error} + hasMoreEvents={hasNextPage} + fetchMoreEvents={manualFetchNextPage} + isFetchingMoreEvents={isFetchingNextPage} + /> ) : ( Date: Thu, 4 Dec 2025 19:25:23 +0100 Subject: [PATCH 2/4] Change types and add helper Signed-off-by: Adhitya Mamallan --- .../workflow-history-ungrouped-event.types.ts | 3 +- .../compare-ungrouped-events.test.ts | 130 ++++++++++++++++++ .../helpers/compare-ungrouped-events.ts | 29 ++++ .../workflow-history-ungrouped-table.tsx | 18 ++- .../workflow-history-ungrouped-table.types.ts | 38 +---- 5 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts create mode 100644 src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/compare-ungrouped-events.ts diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts index ccfb609ea..9c4ba2dc8 100644 --- a/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts @@ -1,4 +1,3 @@ -import { type Timestamp } from '@/__generated__/proto-ts/google/protobuf/Timestamp'; import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; import { type UngroupedEventInfo } from '../workflow-history-ungrouped-table/workflow-history-ungrouped-table.types'; @@ -6,7 +5,7 @@ import { type UngroupedEventInfo } from '../workflow-history-ungrouped-table/wor export type Props = { // Core data props eventInfo: UngroupedEventInfo; - workflowStartTime: Timestamp | null; + workflowStartTimeMs: number | null; decodedPageUrlParams: WorkflowPageTabsParams; // Expansion state diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts new file mode 100644 index 000000000..9cdc2de37 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts @@ -0,0 +1,130 @@ +import { type Timestamp } from '@/__generated__/proto-ts/google/protobuf/Timestamp'; +import { + pendingActivityTaskStartEvent, + pendingDecisionTaskStartEvent, +} from '@/views/workflow-history/__fixtures__/workflow-history-pending-events'; +import { startWorkflowExecutionEvent } from '@/views/workflow-history/__fixtures__/workflow-history-single-events'; +import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; + +import { type UngroupedEventInfo } from '../../workflow-history-ungrouped-table.types'; +import compareUngroupedEvents from '../compare-ungrouped-events'; + +function createMockEventGroup( + label: string, + event: UngroupedEventInfo['event'] +): HistoryEventsGroup { + return { + groupType: 'Event', + label, + eventsMetadata: [], + status: 'COMPLETED', + hasMissingEvents: false, + timeMs: null, + startTimeMs: null, + timeLabel: '', + firstEventId: null, + events: [event], + } as HistoryEventsGroup; +} + +describe(compareUngroupedEvents.name, () => { + it('orders non-pending events by event ID', () => { + const eventA: UngroupedEventInfo = { + id: '1', + label: 'Event A', + event: startWorkflowExecutionEvent, + eventGroup: createMockEventGroup('Event A', startWorkflowExecutionEvent), + }; + const eventB: UngroupedEventInfo = { + id: '2', + label: 'Event B', + event: startWorkflowExecutionEvent, + eventGroup: createMockEventGroup('Event B', startWorkflowExecutionEvent), + }; + + expect(compareUngroupedEvents(eventA, eventB)).toBe(-1); + expect(compareUngroupedEvents(eventB, eventA)).toBe(1); + expect(compareUngroupedEvents(eventA, eventA)).toBe(0); + }); + + it('puts non-pending events before pending events', () => { + const nonPendingEvent: UngroupedEventInfo = { + id: '2', + label: 'Non-pending Event', + event: startWorkflowExecutionEvent, + eventGroup: createMockEventGroup( + 'Non-pending Event', + startWorkflowExecutionEvent + ), + }; + const pendingEvent: UngroupedEventInfo = { + id: '1', + label: 'Pending Event', + event: pendingActivityTaskStartEvent, + eventGroup: createMockEventGroup( + 'Pending Event', + pendingActivityTaskStartEvent + ), + }; + + expect(compareUngroupedEvents(nonPendingEvent, pendingEvent)).toBe(-1); + expect(compareUngroupedEvents(pendingEvent, nonPendingEvent)).toBe(1); + }); + + it('orders pending events by event time', () => { + const eventTimeA: Timestamp = { seconds: '1000', nanos: 0 }; + const eventTimeB: Timestamp = { seconds: '2000', nanos: 0 }; + + const eventA = { + ...pendingActivityTaskStartEvent, + eventTime: eventTimeA, + }; + const eventB = { + ...pendingDecisionTaskStartEvent, + eventTime: eventTimeB, + }; + + const pendingEventA: UngroupedEventInfo = { + id: '1', + label: 'Pending Event A', + event: eventA, + eventGroup: createMockEventGroup('Pending Event A', eventA), + }; + const pendingEventB: UngroupedEventInfo = { + id: '2', + label: 'Pending Event B', + event: eventB, + eventGroup: createMockEventGroup('Pending Event B', eventB), + }; + + expect(compareUngroupedEvents(pendingEventA, pendingEventB)).toBe(-1000000); + expect(compareUngroupedEvents(pendingEventB, pendingEventA)).toBe(1000000); + expect(compareUngroupedEvents(pendingEventA, pendingEventA)).toBe(0); + }); + + it('returns 0 when pending events have no event time', () => { + const eventA = { + ...pendingActivityTaskStartEvent, + eventTime: null, + }; + const eventB = { + ...pendingDecisionTaskStartEvent, + eventTime: null, + }; + + const pendingEventA: UngroupedEventInfo = { + id: '1', + label: 'Pending Event A', + event: eventA, + eventGroup: createMockEventGroup('Pending Event A', eventA), + }; + const pendingEventB: UngroupedEventInfo = { + id: '2', + label: 'Pending Event B', + event: eventB, + eventGroup: createMockEventGroup('Pending Event B', eventB), + }; + + expect(compareUngroupedEvents(pendingEventA, pendingEventB)).toBe(0); + }); +}); diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/compare-ungrouped-events.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/compare-ungrouped-events.ts new file mode 100644 index 000000000..4ec09d45a --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/compare-ungrouped-events.ts @@ -0,0 +1,29 @@ +import parseGrpcTimestamp from '@/utils/datetime/parse-grpc-timestamp'; +import isPendingHistoryEvent from '@/views/workflow-history/workflow-history-event-details/helpers/is-pending-history-event'; + +import { type UngroupedEventInfo } from '../workflow-history-ungrouped-table.types'; + +export default function compareUngroupedEvents( + eventA: UngroupedEventInfo, + eventB: UngroupedEventInfo +) { + const isPendingA = isPendingHistoryEvent(eventA.event); + const isPendingB = isPendingHistoryEvent(eventB.event); + + // If both history events are non-pending ones, order by event ID + if (!isPendingA && !isPendingB) { + return parseInt(eventA.id) - parseInt(eventB.id); + } + + // Put non-pending history events before pending ones + if (!isPendingA) return -1; + if (!isPendingB) return 1; + + if (!eventA.event.eventTime || !eventB.event.eventTime) return 0; + + // Sort pending events by scheduled time + return ( + parseGrpcTimestamp(eventA.event.eventTime) - + parseGrpcTimestamp(eventB.event.eventTime) + ); +} diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx index 10bc54648..f6c9e3f2f 100644 --- a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx @@ -2,11 +2,11 @@ import { useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; -import compareUngroupedEvents from '@/views/workflow-history/helpers/compare-ungrouped-events'; import WorkflowHistoryTimelineLoadMore from '@/views/workflow-history/workflow-history-timeline-load-more/workflow-history-timeline-load-more'; import WorkflowHistoryUngroupedEvent from '../workflow-history-ungrouped-event/workflow-history-ungrouped-event'; +import compareUngroupedEvents from './helpers/compare-ungrouped-events'; import { styled } from './workflow-history-ungrouped-table.styles'; import { type UngroupedEventInfo, @@ -32,12 +32,12 @@ export default function WorkflowHistoryUngroupedTable({ () => eventGroupsById .map(([_, group]) => [ - ...group.events.map((event, index) => ({ + ...group.events.map((event) => ({ + id: event.eventId ?? event.computedEventId, event, - eventMetadata: group.eventsMetadata[index], + eventGroup: group, label: group.label, shortLabel: group.shortLabel, - id: event.eventId ?? event.computedEventId, canReset: group.resetToDecisionEventId === event.eventId, })), ]) @@ -46,12 +46,10 @@ export default function WorkflowHistoryUngroupedTable({ [eventGroupsById] ); - const workflowStartTime = useMemo( + const workflowStartTimeMs = useMemo( () => - eventsInfoFromGroups.length > 0 - ? eventsInfoFromGroups[0].event.eventTime - : null, - [eventsInfoFromGroups] + eventGroupsById.length > 0 ? eventGroupsById[0][1].startTimeMs : null, + [eventGroupsById] ); const maybeHighlightedEventId = useMemo( @@ -88,7 +86,7 @@ export default function WorkflowHistoryUngroupedTable({ itemContent={(_, eventInfo) => ( toggleIsEventExpanded(eventInfo.id)} diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts index c0fbcfa8c..1c84a396a 100644 --- a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts @@ -6,7 +6,6 @@ import { type RequestError } from '@/utils/request/request-error'; import { type ExtendedHistoryEvent, type HistoryEventsGroup, - type HistoryGroupEventMetadata, } from '@/views/workflow-history/workflow-history.types'; import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; @@ -41,42 +40,9 @@ export type Props = { export type UngroupedEventInfo = { id: string; + event: ExtendedHistoryEvent; + eventGroup: HistoryEventsGroup; label: string; shortLabel?: string; - event: ExtendedHistoryEvent; - eventMetadata: HistoryGroupEventMetadata; canReset?: boolean; }; - -// import { type ListRange, type VirtuosoHandle } from 'react-virtuoso'; - -// import { type RequestError } from '@/utils/request/request-error'; -// import { type WorkflowPageTabsParams } from '@/views/workflow-page/workflow-page-tabs/workflow-page-tabs.types'; - -// import { -// type GetIsEventExpanded, -// type ToggleIsEventExpanded, -// } from '../hooks/use-event-expansion-toggle.types'; -// import { type WorkflowHistoryUngroupedEventInfo } from '../workflow-history-ungrouped-event/workflow-history-ungrouped-event.types'; - -// export type Props = { -// // Data and state props -// eventsInfo: Array; -// selectedEventId?: string; -// decodedPageUrlParams: WorkflowPageTabsParams; -// onResetToEventId: (eventId: string) => void; - -// // React Query props -// error: RequestError | null; -// hasMoreEvents: boolean; -// isFetchingMoreEvents: boolean; -// fetchMoreEvents: () => void; - -// // Event expansion state management -// getIsEventExpanded: GetIsEventExpanded; -// toggleIsEventExpanded: ToggleIsEventExpanded; - -// // Virtualization props -// onVisibleRangeChange: (r: ListRange) => void; -// virtuosoRef: React.RefObject; -// }; From dda574d6002af9482823530872c04deea5649c54 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 8 Dec 2025 15:26:53 +0100 Subject: [PATCH 3/4] address comments Signed-off-by: Adhitya Mamallan --- .../workflow-history-ungrouped-table.tsx | 3 ++- .../workflow-history-ungrouped-table.types.ts | 2 ++ src/views/workflow-history-v2/workflow-history-v2.tsx | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx index f6c9e3f2f..1305d1298 100644 --- a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx @@ -32,9 +32,10 @@ export default function WorkflowHistoryUngroupedTable({ () => eventGroupsById .map(([_, group]) => [ - ...group.events.map((event) => ({ + ...group.events.map((event, index) => ({ id: event.eventId ?? event.computedEventId, event, + eventMetadata: group.eventsMetadata[index], eventGroup: group, label: group.label, shortLabel: group.shortLabel, diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts index 1c84a396a..8f7bd1726 100644 --- a/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts @@ -4,6 +4,7 @@ import { type VirtuosoHandle } from 'react-virtuoso'; import { type RequestError } from '@/utils/request/request-error'; import { + type HistoryGroupEventMetadata, type ExtendedHistoryEvent, type HistoryEventsGroup, } from '@/views/workflow-history/workflow-history.types'; @@ -41,6 +42,7 @@ export type Props = { export type UngroupedEventInfo = { id: string; event: ExtendedHistoryEvent; + eventMetadata: HistoryGroupEventMetadata; eventGroup: HistoryEventsGroup; label: string; shortLabel?: string; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index 94848be42..700a59137 100644 --- a/src/views/workflow-history-v2/workflow-history-v2.tsx +++ b/src/views/workflow-history-v2/workflow-history-v2.tsx @@ -325,7 +325,6 @@ export default function WorkflowHistoryV2({ params }: Props) { setVisibleGroupsRange((prevRange) => ({ ...prevRange, From dfd235a4cab6db1145873da19849b08f68dc08a0 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 8 Dec 2025 15:38:10 +0100 Subject: [PATCH 4/4] fix types Signed-off-by: Adhitya Mamallan --- .../compare-ungrouped-events.test.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts index 9cdc2de37..6cc323e2f 100644 --- a/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts @@ -4,7 +4,10 @@ import { pendingDecisionTaskStartEvent, } from '@/views/workflow-history/__fixtures__/workflow-history-pending-events'; import { startWorkflowExecutionEvent } from '@/views/workflow-history/__fixtures__/workflow-history-single-events'; -import { type HistoryEventsGroup } from '@/views/workflow-history/workflow-history.types'; +import { + type HistoryEventsGroup, + type HistoryGroupEventMetadata, +} from '@/views/workflow-history/workflow-history.types'; import { type UngroupedEventInfo } from '../../workflow-history-ungrouped-table.types'; import compareUngroupedEvents from '../compare-ungrouped-events'; @@ -27,18 +30,29 @@ function createMockEventGroup( } as HistoryEventsGroup; } +function createMockEventMetadata(label: string): HistoryGroupEventMetadata { + return { + label, + status: 'COMPLETED', + timeMs: null, + timeLabel: '', + }; +} + describe(compareUngroupedEvents.name, () => { it('orders non-pending events by event ID', () => { const eventA: UngroupedEventInfo = { id: '1', label: 'Event A', event: startWorkflowExecutionEvent, + eventMetadata: createMockEventMetadata('Event A'), eventGroup: createMockEventGroup('Event A', startWorkflowExecutionEvent), }; const eventB: UngroupedEventInfo = { id: '2', label: 'Event B', event: startWorkflowExecutionEvent, + eventMetadata: createMockEventMetadata('Event B'), eventGroup: createMockEventGroup('Event B', startWorkflowExecutionEvent), }; @@ -52,6 +66,7 @@ describe(compareUngroupedEvents.name, () => { id: '2', label: 'Non-pending Event', event: startWorkflowExecutionEvent, + eventMetadata: createMockEventMetadata('Non-pending Event'), eventGroup: createMockEventGroup( 'Non-pending Event', startWorkflowExecutionEvent @@ -61,6 +76,7 @@ describe(compareUngroupedEvents.name, () => { id: '1', label: 'Pending Event', event: pendingActivityTaskStartEvent, + eventMetadata: createMockEventMetadata('Pending Event'), eventGroup: createMockEventGroup( 'Pending Event', pendingActivityTaskStartEvent @@ -88,12 +104,14 @@ describe(compareUngroupedEvents.name, () => { id: '1', label: 'Pending Event A', event: eventA, + eventMetadata: createMockEventMetadata('Pending Event A'), eventGroup: createMockEventGroup('Pending Event A', eventA), }; const pendingEventB: UngroupedEventInfo = { id: '2', label: 'Pending Event B', event: eventB, + eventMetadata: createMockEventMetadata('Pending Event B'), eventGroup: createMockEventGroup('Pending Event B', eventB), }; @@ -116,12 +134,14 @@ describe(compareUngroupedEvents.name, () => { id: '1', label: 'Pending Event A', event: eventA, + eventMetadata: createMockEventMetadata('Pending Event A'), eventGroup: createMockEventGroup('Pending Event A', eventA), }; const pendingEventB: UngroupedEventInfo = { id: '2', label: 'Pending Event B', event: eventB, + eventMetadata: createMockEventMetadata('Pending Event B'), eventGroup: createMockEventGroup('Pending Event B', eventB), };