Fix pagination: disable Next correctly for workflows and run history (#3293)
Co-authored-by: Fratiman <bogdanfrman@yahoo.com>
This commit is contained in:
@@ -11,17 +11,19 @@ type UseQueryOptions = Omit<
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
page?: number;
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
statusFilters?: Array<Status>;
|
statusFilters?: Array<Status>;
|
||||||
} & UseQueryOptions;
|
} & UseQueryOptions;
|
||||||
|
|
||||||
function useRunsQuery({ page = 1, statusFilters }: Props) {
|
function useRunsQuery({ page = 1, pageSize = 10, statusFilters }: Props) {
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
return useQuery<Array<Task | WorkflowRunApiResponse>>({
|
return useQuery<Array<Task | WorkflowRunApiResponse>>({
|
||||||
queryKey: ["runs", { statusFilters }, page],
|
queryKey: ["runs", { statusFilters }, page, pageSize],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const client = await getClient(credentialGetter);
|
const client = await getClient(credentialGetter);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("page", String(page));
|
params.append("page", String(page));
|
||||||
|
params.append("page_size", String(pageSize));
|
||||||
if (statusFilters) {
|
if (statusFilters) {
|
||||||
statusFilters.forEach((status) => {
|
statusFilters.forEach((status) => {
|
||||||
params.append("status", status);
|
params.append("status", status);
|
||||||
|
|||||||
@@ -21,20 +21,53 @@ import {
|
|||||||
import { useRunsQuery } from "@/hooks/useRunsQuery";
|
import { useRunsQuery } from "@/hooks/useRunsQuery";
|
||||||
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
|
||||||
import { cn } from "@/util/utils";
|
import { cn } from "@/util/utils";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
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 {
|
function isTask(run: Task | WorkflowRunApiResponse): run is Task {
|
||||||
return "task_id" in run;
|
return "task_id" in run;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RunHistory() {
|
function RunHistory() {
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
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<Array<Status>>([]);
|
const [statusFilters, setStatusFilters] = useState<Array<Status>>([]);
|
||||||
const { data: runs, isFetching } = useRunsQuery({ page, statusFilters });
|
const { data: runs, isFetching } = useRunsQuery({
|
||||||
|
page,
|
||||||
|
pageSize: itemsPerPage,
|
||||||
|
statusFilters,
|
||||||
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { data: nextPageRuns } = useQuery<Array<Task | WorkflowRunApiResponse>>(
|
||||||
|
{
|
||||||
|
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) {
|
function handleNavigate(event: React.MouseEvent, path: string) {
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
window.open(
|
window.open(
|
||||||
@@ -46,6 +79,22 @@ function RunHistory() {
|
|||||||
navigate(path);
|
navigate(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setParamPatch(patch: Record<string, string>) {
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
@@ -151,35 +200,50 @@ function RunHistory() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination className="pt-2">
|
<div className="relative px-3 py-3">
|
||||||
<PaginationContent>
|
<div className="absolute left-3 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm">
|
||||||
<PaginationItem>
|
<span className="text-slate-400">Items per page</span>
|
||||||
<PaginationPrevious
|
<select
|
||||||
className={cn({ "cursor-not-allowed": page === 1 })}
|
className="h-9 rounded-md border border-slate-300 bg-background px-3"
|
||||||
onClick={() => {
|
value={itemsPerPage}
|
||||||
if (page === 1) {
|
onChange={(e) => {
|
||||||
return;
|
const next = Number(e.target.value);
|
||||||
}
|
const params = new URLSearchParams(searchParams);
|
||||||
const params = new URLSearchParams();
|
params.set("page_size", String(next));
|
||||||
params.set("page", String(Math.max(1, page - 1)));
|
params.set("page", "1");
|
||||||
setSearchParams(params, { replace: true });
|
setSearchParams(params, { replace: true });
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</PaginationItem>
|
<option value={5}>5</option>
|
||||||
<PaginationItem>
|
<option value={10}>10</option>
|
||||||
<PaginationLink>{page}</PaginationLink>
|
<option value={20}>20</option>
|
||||||
</PaginationItem>
|
<option value={50}>50</option>
|
||||||
<PaginationItem>
|
</select>
|
||||||
<PaginationNext
|
</div>
|
||||||
onClick={() => {
|
<Pagination className="pt-0">
|
||||||
const params = new URLSearchParams();
|
<PaginationContent>
|
||||||
params.set("page", String(page + 1));
|
<PaginationItem>
|
||||||
setSearchParams(params, { replace: true });
|
<PaginationPrevious
|
||||||
}}
|
className={cn({
|
||||||
/>
|
"cursor-not-allowed opacity-50": page === 1,
|
||||||
</PaginationItem>
|
})}
|
||||||
</PaginationContent>
|
onClick={handlePreviousPage}
|
||||||
</Pagination>
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink>{page}</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
className={cn({
|
||||||
|
"cursor-not-allowed opacity-50": isNextDisabled,
|
||||||
|
})}
|
||||||
|
onClick={handleNextPage}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,10 +49,7 @@ import { WorkflowTemplates } from "../discover/WorkflowTemplates";
|
|||||||
const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
||||||
title: "New Workflow",
|
title: "New Workflow",
|
||||||
description: "",
|
description: "",
|
||||||
workflow_definition: {
|
workflow_definition: { blocks: [], parameters: [] },
|
||||||
blocks: [],
|
|
||||||
parameters: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Workflows() {
|
function Workflows() {
|
||||||
@@ -63,13 +60,19 @@ function Workflows() {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [debouncedSearch] = useDebounce(search, 500);
|
const [debouncedSearch] = useDebounce(search, 500);
|
||||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
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<Array<WorkflowApiResponse>>({
|
const { data: workflows = [], isLoading } = useQuery<
|
||||||
queryKey: ["workflows", debouncedSearch, page],
|
Array<WorkflowApiResponse>
|
||||||
|
>({
|
||||||
|
queryKey: ["workflows", debouncedSearch, page, itemsPerPage],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const client = await getClient(credentialGetter);
|
const client = await getClient(credentialGetter);
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("page", String(page));
|
params.append("page", String(page));
|
||||||
|
params.append("page_size", String(itemsPerPage));
|
||||||
params.append("only_workflows", "true");
|
params.append("only_workflows", "true");
|
||||||
params.append("title", debouncedSearch);
|
params.append("title", debouncedSearch);
|
||||||
return client
|
return client
|
||||||
@@ -80,6 +83,27 @@ function Workflows() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: nextPageWorkflows } = useQuery<Array<WorkflowApiResponse>>({
|
||||||
|
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(
|
function handleRowClick(
|
||||||
event: React.MouseEvent<HTMLTableCellElement>,
|
event: React.MouseEvent<HTMLTableCellElement>,
|
||||||
workflowPermanentId: string,
|
workflowPermanentId: string,
|
||||||
@@ -110,6 +134,22 @@ function Workflows() {
|
|||||||
navigate(path);
|
navigate(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setParamPatch(patch: Record<string, string>) {
|
||||||
|
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 (
|
return (
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
<div className="flex h-32 justify-between gap-6">
|
<div className="flex h-32 justify-between gap-6">
|
||||||
@@ -151,6 +191,7 @@ function Workflows() {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setSearch(event.target.value);
|
setSearch(event.target.value);
|
||||||
|
setParamPatch({ page: "1" });
|
||||||
}}
|
}}
|
||||||
placeholder="Search by title..."
|
placeholder="Search by title..."
|
||||||
className="w-48 pl-9 lg:w-72"
|
className="w-48 pl-9 lg:w-72"
|
||||||
@@ -274,35 +315,50 @@ function Workflows() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination className="pt-2">
|
<div className="relative px-3 py-3">
|
||||||
<PaginationContent>
|
<div className="absolute left-3 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm">
|
||||||
<PaginationItem>
|
<span className="text-slate-400">Items per page</span>
|
||||||
<PaginationPrevious
|
<select
|
||||||
className={cn({ "cursor-not-allowed": page === 1 })}
|
className="h-9 rounded-md border border-slate-300 bg-background px-3"
|
||||||
onClick={() => {
|
value={itemsPerPage}
|
||||||
if (page === 1) {
|
onChange={(e) => {
|
||||||
return;
|
const next = Number(e.target.value);
|
||||||
}
|
const params = new URLSearchParams(searchParams);
|
||||||
const params = new URLSearchParams();
|
params.set("page_size", String(next));
|
||||||
params.set("page", String(Math.max(1, page - 1)));
|
params.set("page", "1");
|
||||||
setSearchParams(params, { replace: true });
|
setSearchParams(params, { replace: true });
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</PaginationItem>
|
<option value={5}>5</option>
|
||||||
<PaginationItem>
|
<option value={10}>10</option>
|
||||||
<PaginationLink>{page}</PaginationLink>
|
<option value={20}>20</option>
|
||||||
</PaginationItem>
|
<option value={50}>50</option>
|
||||||
<PaginationItem>
|
</select>
|
||||||
<PaginationNext
|
</div>
|
||||||
onClick={() => {
|
<Pagination className="pt-0">
|
||||||
const params = new URLSearchParams();
|
<PaginationContent>
|
||||||
params.set("page", String(page + 1));
|
<PaginationItem>
|
||||||
setSearchParams(params, { replace: true });
|
<PaginationPrevious
|
||||||
}}
|
className={cn({
|
||||||
/>
|
"cursor-not-allowed opacity-50": page === 1,
|
||||||
</PaginationItem>
|
})}
|
||||||
</PaginationContent>
|
onClick={handlePreviousPage}
|
||||||
</Pagination>
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationLink>{page}</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
className={cn({
|
||||||
|
"cursor-not-allowed opacity-50": isNextDisabled,
|
||||||
|
})}
|
||||||
|
onClick={handleNextPage}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WorkflowTemplates />
|
<WorkflowTemplates />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user