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 8dfe3b696..6ea4d406f 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 @@ -86,6 +86,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(); @@ -182,7 +190,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 () => { @@ -190,7 +200,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 () => { @@ -199,7 +211,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(); @@ -214,12 +228,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(); @@ -232,7 +250,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 () => { @@ -242,7 +262,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..9c4ba2dc8 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-event/workflow-history-ungrouped-event.types.ts @@ -0,0 +1,18 @@ +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; + workflowStartTimeMs: number | 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/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..6cc323e2f --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/helpers/__tests__/compare-ungrouped-events.test.ts @@ -0,0 +1,150 @@ +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, + type HistoryGroupEventMetadata, +} 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; +} + +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), + }; + + 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, + eventMetadata: createMockEventMetadata('Non-pending Event'), + eventGroup: createMockEventGroup( + 'Non-pending Event', + startWorkflowExecutionEvent + ), + }; + const pendingEvent: UngroupedEventInfo = { + id: '1', + label: 'Pending Event', + event: pendingActivityTaskStartEvent, + eventMetadata: createMockEventMetadata('Pending Event'), + 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, + 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), + }; + + 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, + 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), + }; + + 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.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..1305d1298 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.tsx @@ -0,0 +1,116 @@ +import { useMemo } from 'react'; + +import { Virtuoso } from 'react-virtuoso'; + +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, + 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) => ({ + id: event.eventId ?? event.computedEventId, + event, + eventMetadata: group.eventsMetadata[index], + eventGroup: group, + label: group.label, + shortLabel: group.shortLabel, + canReset: group.resetToDecisionEventId === event.eventId, + })), + ]) + .flat(1) + .sort(compareUngroupedEvents), + [eventGroupsById] + ); + + const workflowStartTimeMs = useMemo( + () => + eventGroupsById.length > 0 ? eventGroupsById[0][1].startTimeMs : null, + [eventGroupsById] + ); + + 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..8f7bd1726 --- /dev/null +++ b/src/views/workflow-history-v2/workflow-history-ungrouped-table/workflow-history-ungrouped-table.types.ts @@ -0,0 +1,50 @@ +import { type RefObject } from 'react'; + +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'; +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; + event: ExtendedHistoryEvent; + eventMetadata: HistoryGroupEventMetadata; + eventGroup: HistoryEventsGroup; + label: string; + shortLabel?: string; + canReset?: boolean; +}; diff --git a/src/views/workflow-history-v2/workflow-history-v2.tsx b/src/views/workflow-history-v2/workflow-history-v2.tsx index a304c9801..9ae2d3527 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, @@ -290,6 +291,7 @@ export default function WorkflowHistoryV2({ params }: Props) { !reachedEndOfAvailableHistory); const groupedTableVirtuosoRef = useRef(null); + const ungroupedTableVirtuosoRef = useRef(null); const workflowCloseTimeMs = workflowExecutionInfo?.closeTime ? parseGrpcTimestamp(workflowExecutionInfo?.closeTime) @@ -327,7 +329,26 @@ 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} + /> ) : (