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}
+ />
) : (