From d2c71a95429215b7bc8be5c7ce51cd9d804c400f Mon Sep 17 00:00:00 2001 From: dvloper-bogdan Date: Fri, 5 Sep 2025 02:30:47 +0300 Subject: [PATCH] Fix pagination: disable Next correctly for workflows and run history (#3293) Co-authored-by: Fratiman --- skyvern-frontend/src/hooks/useRunsQuery.ts | 6 +- .../src/routes/history/RunHistory.tsx | 124 ++++++++++++----- .../src/routes/workflows/Workflows.tsx | 126 +++++++++++++----- 3 files changed, 189 insertions(+), 67 deletions(-) diff --git a/skyvern-frontend/src/hooks/useRunsQuery.ts b/skyvern-frontend/src/hooks/useRunsQuery.ts index 2f2cff99..2e4dc6fc 100644 --- a/skyvern-frontend/src/hooks/useRunsQuery.ts +++ b/skyvern-frontend/src/hooks/useRunsQuery.ts @@ -11,17 +11,19 @@ type UseQueryOptions = Omit< type Props = { page?: number; + pageSize?: number; statusFilters?: Array; } & UseQueryOptions; -function useRunsQuery({ page = 1, statusFilters }: Props) { +function useRunsQuery({ page = 1, pageSize = 10, statusFilters }: Props) { const credentialGetter = useCredentialGetter(); return useQuery>({ - queryKey: ["runs", { statusFilters }, page], + queryKey: ["runs", { statusFilters }, page, pageSize], queryFn: async () => { const client = await getClient(credentialGetter); const params = new URLSearchParams(); params.append("page", String(page)); + params.append("page_size", String(pageSize)); if (statusFilters) { statusFilters.forEach((status) => { params.append("status", status); diff --git a/skyvern-frontend/src/routes/history/RunHistory.tsx b/skyvern-frontend/src/routes/history/RunHistory.tsx index c103d75a..013c99ca 100644 --- a/skyvern-frontend/src/routes/history/RunHistory.tsx +++ b/skyvern-frontend/src/routes/history/RunHistory.tsx @@ -21,20 +21,53 @@ import { import { useRunsQuery } from "@/hooks/useRunsQuery"; import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat"; import { cn } from "@/util/utils"; +import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { getClient } from "@/api/AxiosClient"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; function isTask(run: Task | WorkflowRunApiResponse): run is Task { return "task_id" in run; } function RunHistory() { + const credentialGetter = useCredentialGetter(); const [searchParams, setSearchParams] = useSearchParams(); const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; + const itemsPerPage = searchParams.get("page_size") + ? Number(searchParams.get("page_size")) + : 5; const [statusFilters, setStatusFilters] = useState>([]); - const { data: runs, isFetching } = useRunsQuery({ page, statusFilters }); + const { data: runs, isFetching } = useRunsQuery({ + page, + pageSize: itemsPerPage, + statusFilters, + }); const navigate = useNavigate(); + const { data: nextPageRuns } = useQuery>( + { + queryKey: ["runs", { statusFilters }, page + 1, itemsPerPage], + queryFn: async () => { + const client = await getClient(credentialGetter); + const params = new URLSearchParams(); + params.append("page", String(page + 1)); + params.append("page_size", String(itemsPerPage)); + if (statusFilters) { + statusFilters.forEach((status) => { + params.append("status", status); + }); + } + return client.get("/runs", { params }).then((res) => res.data); + }, + enabled: runs && runs.length === itemsPerPage, + }, + ); + + const isNextDisabled = + isFetching || !nextPageRuns || nextPageRuns.length === 0; + function handleNavigate(event: React.MouseEvent, path: string) { if (event.ctrlKey || event.metaKey) { window.open( @@ -46,6 +79,22 @@ function RunHistory() { navigate(path); } } + + function setParamPatch(patch: Record) { + const params = new URLSearchParams(searchParams); + Object.entries(patch).forEach(([k, v]) => params.set(k, v)); + setSearchParams(params, { replace: true }); + } + + function handlePreviousPage() { + if (page === 1) return; + setParamPatch({ page: String(page - 1) }); + } + + function handleNextPage() { + if (isNextDisabled) return; + setParamPatch({ page: String(page + 1) }); + } return (
@@ -151,35 +200,50 @@ function RunHistory() { )} - - - - { - if (page === 1) { - return; - } - const params = new URLSearchParams(); - params.set("page", String(Math.max(1, page - 1))); - setSearchParams(params, { replace: true }); - }} - /> - - - {page} - - - { - const params = new URLSearchParams(); - params.set("page", String(page + 1)); - setSearchParams(params, { replace: true }); - }} - /> - - - +
+
+ Items per page + +
+ + + + + + + {page} + + + + + + +
); diff --git a/skyvern-frontend/src/routes/workflows/Workflows.tsx b/skyvern-frontend/src/routes/workflows/Workflows.tsx index 1d806129..977d68eb 100644 --- a/skyvern-frontend/src/routes/workflows/Workflows.tsx +++ b/skyvern-frontend/src/routes/workflows/Workflows.tsx @@ -49,10 +49,7 @@ import { WorkflowTemplates } from "../discover/WorkflowTemplates"; const emptyWorkflowRequest: WorkflowCreateYAMLRequest = { title: "New Workflow", description: "", - workflow_definition: { - blocks: [], - parameters: [], - }, + workflow_definition: { blocks: [], parameters: [] }, }; function Workflows() { @@ -63,13 +60,19 @@ function Workflows() { const [search, setSearch] = useState(""); const [debouncedSearch] = useDebounce(search, 500); const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; + const itemsPerPage = searchParams.get("page_size") + ? Number(searchParams.get("page_size")) + : 5; - const { data: workflows, isLoading } = useQuery>({ - queryKey: ["workflows", debouncedSearch, page], + const { data: workflows = [], isLoading } = useQuery< + Array + >({ + queryKey: ["workflows", debouncedSearch, page, itemsPerPage], queryFn: async () => { const client = await getClient(credentialGetter); const params = new URLSearchParams(); params.append("page", String(page)); + params.append("page_size", String(itemsPerPage)); params.append("only_workflows", "true"); params.append("title", debouncedSearch); return client @@ -80,6 +83,27 @@ function Workflows() { }, }); + const { data: nextPageWorkflows } = useQuery>({ + queryKey: ["workflows", debouncedSearch, page + 1, itemsPerPage], + queryFn: async () => { + const client = await getClient(credentialGetter); + const params = new URLSearchParams(); + params.append("page", String(page + 1)); + params.append("page_size", String(itemsPerPage)); + params.append("only_workflows", "true"); + params.append("title", debouncedSearch); + return client + .get(`/workflows`, { + params, + }) + .then((response) => response.data); + }, + enabled: workflows.length === itemsPerPage, + }); + + const isNextDisabled = + isLoading || !nextPageWorkflows || nextPageWorkflows.length === 0; + function handleRowClick( event: React.MouseEvent, workflowPermanentId: string, @@ -110,6 +134,22 @@ function Workflows() { navigate(path); } + function setParamPatch(patch: Record) { + const params = new URLSearchParams(searchParams); + Object.entries(patch).forEach(([k, v]) => params.set(k, v)); + setSearchParams(params, { replace: true }); + } + + function handlePreviousPage() { + if (page === 1) return; + setParamPatch({ page: String(page - 1) }); + } + + function handleNextPage() { + if (isNextDisabled) return; + setParamPatch({ page: String(page + 1) }); + } + return (
@@ -151,6 +191,7 @@ function Workflows() { value={search} onChange={(event) => { setSearch(event.target.value); + setParamPatch({ page: "1" }); }} placeholder="Search by title..." className="w-48 pl-9 lg:w-72" @@ -274,35 +315,50 @@ function Workflows() { )} - - - - { - if (page === 1) { - return; - } - const params = new URLSearchParams(); - params.set("page", String(Math.max(1, page - 1))); - setSearchParams(params, { replace: true }); - }} - /> - - - {page} - - - { - const params = new URLSearchParams(); - params.set("page", String(page + 1)); - setSearchParams(params, { replace: true }); - }} - /> - - - +
+
+ Items per page + +
+ + + + + + + {page} + + + + + + +