From 35e930ae5bc0be0984cfaf618abd8a563aac84a1 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 3 Apr 2026 17:46:03 +0800 Subject: [PATCH] feat(web): add infinite scroll to workflow run history panel --- .../header/__tests__/view-history.spec.tsx | 29 ++++++++++++----- .../workflow/header/view-history.tsx | 31 +++++++++++++++++-- web/service/use-workflow.ts | 16 ++++++++-- web/types/workflow.ts | 6 ++-- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/web/app/components/workflow/header/__tests__/view-history.spec.tsx b/web/app/components/workflow/header/__tests__/view-history.spec.tsx index 4481c72cf7..642bb6e9d3 100644 --- a/web/app/components/workflow/header/__tests__/view-history.spec.tsx +++ b/web/app/components/workflow/header/__tests__/view-history.spec.tsx @@ -117,8 +117,11 @@ describe('ViewHistory', () => { vi.clearAllMocks() mockIsChatMode = false mockUseWorkflowRunHistory.mockReturnValue({ - data: { data: [] } satisfies WorkflowRunHistoryResponse, + data: { pages: [{ data: [], has_more: false, limit: 20 }] satisfies WorkflowRunHistoryResponse[] }, isLoading: false, + hasNextPage: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), }) }) @@ -142,8 +145,11 @@ describe('ViewHistory', () => { it('renders the icon trigger variant and loading state, and clears log modals on trigger click', () => { const onClearLogAndMessageModal = vi.fn() mockUseWorkflowRunHistory.mockReturnValue({ - data: { data: [] } satisfies WorkflowRunHistoryResponse, + data: { pages: [{ data: [], has_more: false, limit: 20 }] satisfies WorkflowRunHistoryResponse[] }, isLoading: true, + hasNextPage: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), }) renderWorkflowComponent( @@ -187,9 +193,12 @@ describe('ViewHistory', () => { mockUseWorkflowRunHistory.mockReturnValue({ data: { - data: [pausedRun, failedRun, succeededRun], - } satisfies WorkflowRunHistoryResponse, + pages: [{ data: [pausedRun, failedRun, succeededRun], has_more: false, limit: 20 }] satisfies WorkflowRunHistoryResponse[], + }, isLoading: false, + hasNextPage: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), }) const { store } = renderWorkflowComponent(, { @@ -231,9 +240,12 @@ describe('ViewHistory', () => { mockUseWorkflowRunHistory.mockReturnValue({ data: { - data: [chatRun], - } satisfies WorkflowRunHistoryResponse, + pages: [{ data: [chatRun], has_more: false, limit: 20 }] satisfies WorkflowRunHistoryResponse[], + }, isLoading: false, + hasNextPage: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), }) renderWorkflowComponent(, { @@ -250,8 +262,11 @@ describe('ViewHistory', () => { it('closes the popup from the close button and clears log modals', () => { const onClearLogAndMessageModal = vi.fn() mockUseWorkflowRunHistory.mockReturnValue({ - data: { data: [] } satisfies WorkflowRunHistoryResponse, + data: { pages: [{ data: [], has_more: false, limit: 20 }] satisfies WorkflowRunHistoryResponse[] }, isLoading: false, + hasNextPage: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), }) renderWorkflowComponent( diff --git a/web/app/components/workflow/header/view-history.tsx b/web/app/components/workflow/header/view-history.tsx index 752f6735e3..df689b4c91 100644 --- a/web/app/components/workflow/header/view-history.tsx +++ b/web/app/components/workflow/header/view-history.tsx @@ -1,5 +1,7 @@ import { memo, + useEffect, + useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -57,8 +59,26 @@ const ViewHistory = ({ const { data, isLoading, + hasNextPage, + isFetchingNextPage, + fetchNextPage, } = useWorkflowRunHistory(historyUrl, shouldFetchHistory) + const sentinelRef = useRef(null) + useEffect(() => { + const el = sentinelRef.current + if (!el) + return + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) + fetchNextPage() + }) + observer.observe(el) + return () => observer.disconnect() + }, [fetchNextPage, hasNextPage, isFetchingNextPage]) + + const allRuns = data?.pages.flatMap(page => page.data) ?? [] + return ( ( -
+
{t('common.runHistory', { ns: 'workflow' })}