diff --git a/src/views/workflow-history/__tests__/workflow-history.test.tsx b/src/views/workflow-history/__tests__/workflow-history.test.tsx
index 99cdb5f67..797b1fde6 100644
--- a/src/views/workflow-history/__tests__/workflow-history.test.tsx
+++ b/src/views/workflow-history/__tests__/workflow-history.test.tsx
@@ -28,6 +28,15 @@ jest.mock('@/hooks/use-page-query-params/use-page-query-params', () =>
jest.fn(() => [{ historySelectedEventId: '1' }, jest.fn()])
);
+// Mock the hook to use minimal throttle delay for faster tests
+jest.mock('../hooks/use-workflow-history-fetcher', () => {
+ const actual = jest.requireActual('../hooks/use-workflow-history-fetcher');
+ return {
+ __esModule: true,
+ default: jest.fn((params) => actual.default(params, 0)), // 0ms throttle for tests
+ };
+});
+
jest.mock(
'../workflow-history-compact-event-card/workflow-history-compact-event-card',
() => jest.fn(() =>
Compact group Card
)
@@ -90,24 +99,24 @@ describe('WorkflowHistory', () => {
});
it('renders page header correctly', async () => {
- setup({});
+ await setup({});
expect(
await screen.findByText('Workflow history Header')
).toBeInTheDocument();
});
it('renders compact group cards', async () => {
- setup({});
+ await setup({});
expect(await screen.findByText('Compact group Card')).toBeInTheDocument();
});
it('renders timeline group cards', async () => {
- setup({});
+ await setup({});
expect(await screen.findByText('Timeline group card')).toBeInTheDocument();
});
it('renders load more section', async () => {
- setup({});
+ await setup({});
expect(await screen.findByText('Load more')).toBeInTheDocument();
});
@@ -180,7 +189,7 @@ describe('WorkflowHistory', () => {
});
it('should show no results when filtered events are empty', async () => {
- setup({ emptyEvents: true });
+ await setup({ emptyEvents: true });
expect(await screen.findByText('No Results')).toBeInTheDocument();
});
diff --git a/src/views/workflow-history/hooks/__tests__/use-keep-loading-events.test.ts b/src/views/workflow-history/hooks/__tests__/use-keep-loading-events.test.ts
deleted file mode 100644
index 4431ba5c7..000000000
--- a/src/views/workflow-history/hooks/__tests__/use-keep-loading-events.test.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { renderHook } from '@/test-utils/rtl';
-
-import useKeepLoadingEvents from '../use-keep-loading-events';
-import { type UseKeepLoadingEventsParams } from '../use-keep-loading-events.types';
-
-describe('useKeepLoadingEvents', () => {
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- it('should set reachedAvailableHistoryEnd to true when there are no more pages', () => {
- const { result } = setup({ hasNextPage: false });
- expect(result.current.reachedAvailableHistoryEnd).toBe(true);
- });
-
- it('should call fetchNextPage when shouldKeepLoading is true and there are more pages', () => {
- const { fetchNextPageMock } = setup({ shouldKeepLoading: true });
-
- expect(fetchNextPageMock).toHaveBeenCalled();
- });
-
- it('should not call fetchNextPage when shouldKeepLoading is false', () => {
- const { fetchNextPageMock } = setup({ shouldKeepLoading: false });
-
- expect(fetchNextPageMock).not.toHaveBeenCalled();
- });
-
- it('should not call fetchNextPage when isFetchingNextPage is true', () => {
- const { fetchNextPageMock } = setup({ isFetchingNextPage: true });
-
- expect(fetchNextPageMock).not.toHaveBeenCalled();
- });
-
- it('should not call fetchNextPage when stopAfterEndReached is true and reachedAvailableHistoryEnd is true', () => {
- const { fetchNextPageMock } = setup({
- hasNextPage: false,
- stopAfterEndReached: true,
- });
-
- expect(fetchNextPageMock).not.toHaveBeenCalled();
- });
-
- it('should not call fetchNextPage after error when continueLoadingAfterError is false', () => {
- const { fetchNextPageMock, rerender } = setup({
- isFetchNextPageError: true,
- continueLoadingAfterError: false,
- });
-
- rerender({ isFetchNextPageError: false });
-
- expect(fetchNextPageMock).not.toHaveBeenCalled();
- });
-
- it('should call fetchNextPage after error when continueLoadingAfterError is true', () => {
- const { fetchNextPageMock, rerender } = setup({
- isFetchNextPageError: true,
- continueLoadingAfterError: true,
- });
-
- rerender({ isFetchNextPageError: false });
-
- expect(fetchNextPageMock).toHaveBeenCalled();
- });
-
- it('should set stoppedDueToError to true when isFetchNextPageError is true', () => {
- const { result, rerender } = setup({
- isFetchNextPageError: false,
- });
-
- expect(result.current.stoppedDueToError).toBe(false);
-
- rerender({ isFetchNextPageError: true });
-
- expect(result.current.stoppedDueToError).toBe(true);
- });
-
- it('should not call fetchNextPage when stoppedDueToError is true', () => {
- const { fetchNextPageMock } = setup({ isFetchNextPageError: true });
-
- expect(fetchNextPageMock).not.toHaveBeenCalled();
- });
-
- it('should return isLoadingMore as true when keepLoadingMore conditions are met', () => {
- const { result, rerender } = setup({
- shouldKeepLoading: true,
- stopAfterEndReached: true,
- hasNextPage: true,
- isFetchNextPageError: false,
- });
-
- expect(result.current.isLoadingMore).toBe(true);
-
- rerender({
- shouldKeepLoading: true,
- hasNextPage: true,
- isFetchNextPageError: false,
- // stopAfterEndReached and simulate end by empty events page
- stopAfterEndReached: true,
- isLastPageEmpty: true,
- });
- expect(result.current.isLoadingMore).toBe(false);
-
- rerender({
- shouldKeepLoading: true,
- stopAfterEndReached: true,
- hasNextPage: true,
- // adding error
- isFetchNextPageError: true,
- });
- expect(result.current.isLoadingMore).toBe(false);
- });
-});
-
-function setup(params: Partial) {
- const fetchNextPage = jest.fn();
- const { result, rerender } = renderHook(
- (runTimeChanges?: Partial) =>
- useKeepLoadingEvents({
- shouldKeepLoading: true,
- stopAfterEndReached: true,
- isLastPageEmpty: false,
- hasNextPage: true,
- fetchNextPage,
- isFetchingNextPage: false,
- isFetchNextPageError: false,
- ...params,
- ...runTimeChanges,
- })
- );
-
- return { result, rerender, fetchNextPageMock: fetchNextPage };
-}
diff --git a/src/views/workflow-history/hooks/use-keep-loading-events.ts b/src/views/workflow-history/hooks/use-keep-loading-events.ts
deleted file mode 100644
index 8ce917239..000000000
--- a/src/views/workflow-history/hooks/use-keep-loading-events.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useEffect, useRef } from 'react';
-
-import { type UseKeepLoadingEventsParams } from './use-keep-loading-events.types';
-
-export default function useKeepLoadingEvents({
- shouldKeepLoading,
- isLastPageEmpty,
- hasNextPage,
- fetchNextPage,
- isFetchingNextPage,
- stopAfterEndReached,
- isFetchNextPageError,
- continueLoadingAfterError,
-}: UseKeepLoadingEventsParams) {
- const reachedAvailableHistoryEnd = useRef(false);
-
- const hadErrorOnce = useRef(isFetchNextPageError);
- // update reachedAvailableHistoryEnd
- const reached =
- !hasNextPage || (hasNextPage && isLastPageEmpty && !isFetchNextPageError);
- if (reached && !reachedAvailableHistoryEnd.current)
- reachedAvailableHistoryEnd.current = true;
-
- // update hadErrorOnce
- if (isFetchNextPageError && !hadErrorOnce.current)
- hadErrorOnce.current = true;
-
- const stopDueToError =
- isFetchNextPageError ||
- (hadErrorOnce.current && !continueLoadingAfterError);
-
- const canLoadMore =
- shouldKeepLoading &&
- !(stopAfterEndReached && reachedAvailableHistoryEnd.current) &&
- !stopDueToError &&
- hasNextPage;
-
- useEffect(() => {
- if (canLoadMore && !isFetchingNextPage) fetchNextPage();
- }, [isFetchingNextPage, fetchNextPage, canLoadMore]);
-
- return {
- reachedAvailableHistoryEnd: reachedAvailableHistoryEnd.current,
- stoppedDueToError: stopDueToError,
- isLoadingMore: canLoadMore,
- };
-}
diff --git a/src/views/workflow-history/workflow-history-header/__tests__/workflow-history-header.test.tsx b/src/views/workflow-history/workflow-history-header/__tests__/workflow-history-header.test.tsx
index f3b77a3ac..e7a151a52 100644
--- a/src/views/workflow-history/workflow-history-header/__tests__/workflow-history-header.test.tsx
+++ b/src/views/workflow-history/workflow-history-header/__tests__/workflow-history-header.test.tsx
@@ -227,8 +227,6 @@ function setup(props: Partial = {}) {
cluster: 'test-cluster',
workflowId: 'test-workflowId',
runId: 'test-runId',
- pageSize: 100,
- waitForNewEvent: 'true',
},
pageFiltersProps: {
activeFiltersCount: 0,
diff --git a/src/views/workflow-history/workflow-history-header/workflow-history-header.types.ts b/src/views/workflow-history/workflow-history-header/workflow-history-header.types.ts
index 2ee1aa20f..ce9f5483f 100644
--- a/src/views/workflow-history/workflow-history-header/workflow-history-header.types.ts
+++ b/src/views/workflow-history/workflow-history-header/workflow-history-header.types.ts
@@ -8,10 +8,6 @@ import { type Props as WorkflowHistoryExportJsonButtonProps } from '../workflow-
import { type Props as WorkflowHistoryTimelineChartProps } from '../workflow-history-timeline-chart/workflow-history-timeline-chart.types';
type WorkflowPageQueryParamsConfig = typeof workflowPageQueryParamsConfig;
-type WorkflowHistoryRequestArgs = WorkflowHistoryExportJsonButtonProps & {
- pageSize: number;
- waitForNewEvent: string;
-};
type PageFiltersProps = {
resetAllFilters: () => void;
@@ -25,7 +21,7 @@ export type Props = {
toggleIsExpandAllEvents: () => void;
isUngroupedHistoryViewEnabled: boolean;
onClickGroupModeToggle: () => void;
- wfHistoryRequestArgs: WorkflowHistoryRequestArgs;
+ wfHistoryRequestArgs: WorkflowHistoryExportJsonButtonProps;
pageFiltersProps: PageFiltersProps;
timelineChartProps: WorkflowHistoryTimelineChartProps;
isStickyEnabled?: boolean;
diff --git a/src/views/workflow-history/workflow-history.tsx b/src/views/workflow-history/workflow-history.tsx
index 6b89c1242..ef69c835b 100644
--- a/src/views/workflow-history/workflow-history.tsx
+++ b/src/views/workflow-history/workflow-history.tsx
@@ -2,16 +2,12 @@
import React, {
useCallback,
useContext,
+ useEffect,
useMemo,
useRef,
useState,
} from 'react';
-import {
- useSuspenseInfiniteQuery,
- type InfiniteData,
-} from '@tanstack/react-query';
-import queryString from 'query-string';
import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso';
import usePageFilters from '@/components/page-filters/hooks/use-page-filters';
@@ -19,11 +15,8 @@ import PageSection from '@/components/page-section/page-section';
import SectionLoadingIndicator from '@/components/section-loading-indicator/section-loading-indicator';
import useStyletronClasses from '@/hooks/use-styletron-classes';
import useThrottledState from '@/hooks/use-throttled-state';
-import { type GetWorkflowHistoryResponse } from '@/route-handlers/get-workflow-history/get-workflow-history.types';
import parseGrpcTimestamp from '@/utils/datetime/parse-grpc-timestamp';
import decodeUrlParams from '@/utils/decode-url-params';
-import request from '@/utils/request';
-import { type RequestError } from '@/utils/request/request-error';
import sortBy from '@/utils/sort-by';
import { resetWorkflowActionConfig } from '../workflow-actions/config/workflow-actions.config';
@@ -41,7 +34,7 @@ import pendingActivitiesInfoToEvents from './helpers/pending-activities-info-to-
import pendingDecisionInfoToEvent from './helpers/pending-decision-info-to-event';
import useEventExpansionToggle from './hooks/use-event-expansion-toggle';
import useInitialSelectedEvent from './hooks/use-initial-selected-event';
-import useKeepLoadingEvents from './hooks/use-keep-loading-events';
+import useWorkflowHistoryFetcher from './hooks/use-workflow-history-fetcher';
import WorkflowHistoryCompactEventCard from './workflow-history-compact-event-card/workflow-history-compact-event-card';
import { WorkflowHistoryContext } from './workflow-history-context-provider/workflow-history-context-provider';
import WorkflowHistoryHeader from './workflow-history-header/workflow-history-header';
@@ -63,8 +56,26 @@ export default function WorkflowHistory({ params }: Props) {
const wfHistoryRequestArgs = {
...historyQueryParams,
pageSize: WORKFLOW_HISTORY_PAGE_SIZE_CONFIG,
- waitForNewEvent: 'true',
+ waitForNewEvent: true,
};
+
+ const {
+ historyQuery,
+ startLoadingHistory,
+ stopLoadingHistory,
+ fetchSingleNextPage,
+ } = useWorkflowHistoryFetcher(
+ {
+ domain: wfHistoryRequestArgs.domain,
+ cluster: wfHistoryRequestArgs.cluster,
+ workflowId: wfHistoryRequestArgs.workflowId,
+ runId: wfHistoryRequestArgs.runId,
+ pageSize: wfHistoryRequestArgs.pageSize,
+ waitForNewEvent: wfHistoryRequestArgs.waitForNewEvent,
+ },
+ 2000
+ );
+
const [resetToDecisionEventId, setResetToDecisionEventId] = useState<
string | undefined
>(undefined);
@@ -96,38 +107,16 @@ export default function WorkflowHistory({ params }: Props) {
const {
data: result,
hasNextPage,
- fetchNextPage,
isFetchingNextPage,
+ isLoading,
+ isPending,
error,
isFetchNextPageError,
- } = useSuspenseInfiniteQuery<
- GetWorkflowHistoryResponse,
- RequestError,
- InfiniteData,
- [string, typeof wfHistoryRequestArgs],
- string | undefined
- >({
- queryKey: ['workflow_history_paginated', wfHistoryRequestArgs] as const,
- queryFn: ({ queryKey: [_, qp], pageParam }) =>
- request(
- `/api/domains/${qp.domain}/${qp.cluster}/workflows/${qp.workflowId}/${qp.runId}/history?${queryString.stringify(
- {
- nextPage: pageParam,
- pageSize: qp.pageSize,
- waitForNewEvent: qp.waitForNewEvent,
- }
- )}`
- ).then((res) => res.json()),
- initialPageParam: undefined,
- getNextPageParam: (lastPage) => {
- if (!lastPage?.nextPageToken) return undefined;
- return lastPage?.nextPageToken;
- },
- });
+ } = historyQuery;
const events = useMemo(
() =>
- (result.pages || [])
+ (result?.pages || [])
.flat(1)
.map(({ history }) => history?.events || [])
.flat(1),
@@ -194,15 +183,21 @@ export default function WorkflowHistory({ params }: Props) {
);
const [visibleGroupsRange, setTimelineListVisibleRange] =
- useThrottledState({
- startIndex: -1,
- endIndex: -1,
- compactStartIndex: -1,
- compactEndIndex: -1,
- ungroupedStartIndex: -1,
- ungroupedEndIndex: -1,
- });
-
+ useThrottledState(
+ {
+ startIndex: -1,
+ endIndex: -1,
+ compactStartIndex: -1,
+ compactEndIndex: -1,
+ ungroupedStartIndex: -1,
+ ungroupedEndIndex: -1,
+ },
+ 700,
+ {
+ leading: false,
+ trailing: true,
+ }
+ );
const onClickGroupModeToggle = useCallback(() => {
setUngroupedViewUserPreference(!isUngroupedHistoryViewEnabled);
@@ -243,7 +238,7 @@ export default function WorkflowHistory({ params }: Props) {
});
const isLastPageEmpty =
- result.pages[result.pages.length - 1].history?.events.length === 0;
+ result?.pages?.[result?.pages?.length - 1]?.history?.events.length === 0;
const visibleGroupsHasMissingEvents = useMemo(() => {
return getVisibleGroupsHasMissingEvents(
@@ -277,19 +272,31 @@ export default function WorkflowHistory({ params }: Props) {
ungroupedViewShouldLoadMoreEvents,
]);
- const { isLoadingMore, reachedAvailableHistoryEnd } = useKeepLoadingEvents({
- shouldKeepLoading: keepLoadingMoreEvents,
- stopAfterEndReached: true,
- continueLoadingAfterError: true,
- hasNextPage,
- fetchNextPage,
- isFetchingNextPage,
- isLastPageEmpty,
- isFetchNextPageError,
- });
+ const manualFetchNextPage = useCallback(() => {
+ if (keepLoadingMoreEvents) {
+ startLoadingHistory();
+ } else {
+ fetchSingleNextPage();
+ }
+ }, [keepLoadingMoreEvents, startLoadingHistory, fetchSingleNextPage]);
+
+ useEffect(() => {
+ if (keepLoadingMoreEvents) {
+ startLoadingHistory();
+ } else {
+ stopLoadingHistory();
+ }
+ }, [keepLoadingMoreEvents, startLoadingHistory, stopLoadingHistory]);
+
+ const reachedEndOfAvailableHistory =
+ (!hasNextPage && !isPending) ||
+ (hasNextPage && isLastPageEmpty && !isFetchNextPageError);
const contentIsLoading =
- shouldSearchForInitialEvent && !initialEventFound && isLoadingMore;
+ isLoading ||
+ (shouldSearchForInitialEvent &&
+ !initialEventFound &&
+ !reachedEndOfAvailableHistory);
const {
isExpandAllEvents,
@@ -339,7 +346,7 @@ export default function WorkflowHistory({ params }: Props) {
: hasNextPage,
hasMoreEvents: hasNextPage,
isFetchingMoreEvents: isFetchingNextPage,
- fetchMoreEvents: fetchNextPage,
+ fetchMoreEvents: manualFetchNextPage,
onClickEventGroup: (eventGroupIndex) => {
const eventId =
filteredEventGroupsEntries[eventGroupIndex][1].events[0]
@@ -389,7 +396,7 @@ export default function WorkflowHistory({ params }: Props) {
error={error}
hasMoreEvents={hasNextPage}
isFetchingMoreEvents={isFetchingNextPage}
- fetchMoreEvents={fetchNextPage}
+ fetchMoreEvents={manualFetchNextPage}
getIsEventExpanded={getIsEventExpanded}
toggleIsEventExpanded={toggleIsEventExpanded}
onVisibleRangeChange={({ startIndex, endIndex }) =>
@@ -428,7 +435,7 @@ export default function WorkflowHistory({ params }: Props) {
{...group}
statusReady={
!group.hasMissingEvents ||
- reachedAvailableHistoryEnd
+ reachedEndOfAvailableHistory
}
workflowCloseStatus={
workflowExecutionInfo?.closeStatus
@@ -458,7 +465,7 @@ export default function WorkflowHistory({ params }: Props) {
)}
endReached={() => {
- if (!isFetchingNextPage && hasNextPage) fetchNextPage();
+ manualFetchNextPage();
}}
/>
@@ -489,7 +496,8 @@ export default function WorkflowHistory({ params }: Props) {
key={groupId}
{...group}
showLoadingMoreEvents={
- group.hasMissingEvents && !reachedAvailableHistoryEnd
+ group.hasMissingEvents &&
+ !reachedEndOfAvailableHistory
}
resetToDecisionEventId={group.resetToDecisionEventId}
isLastEvent={
@@ -520,7 +528,7 @@ export default function WorkflowHistory({ params }: Props) {
Footer: () => (