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: () => (