diff --git a/dashboard/src/api/control-layer/client.ts b/dashboard/src/api/control-layer/client.ts index 3b4e96689..8da33b2fc 100644 --- a/dashboard/src/api/control-layer/client.ts +++ b/dashboard/src/api/control-layer/client.ts @@ -796,7 +796,9 @@ const paymentsApi = { if (!response.ok) { const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `Failed to create payment: ${response.status}`); + throw new Error( + errorData.message || `Failed to create payment: ${response.status}`, + ); } return response.json(); @@ -1046,13 +1048,13 @@ const filesApi = { // Get file content as JSONL (supports limit/offset query params) // Returns content, whether there are more results, and the last line number - async getContent( + async getFileContent( id: string, - options?: { limit?: number; offset?: number }, + options?: { limit?: number; skip?: number }, ): Promise<{ content: string; incomplete: boolean; lastLine: number }> { const params = new URLSearchParams(); if (options?.limit) params.set("limit", options.limit.toString()); - if (options?.offset) params.set("offset", options.offset.toString()); + if (options?.skip) params.set("skip", options.skip.toString()); const url = `/ai/v1/files/${id}/content${params.toString() ? "?" + params.toString() : ""}`; const response = await fetch(url); diff --git a/dashboard/src/api/control-layer/hooks.ts b/dashboard/src/api/control-layer/hooks.ts index 0dbfe69d8..2d4d62c2f 100644 --- a/dashboard/src/api/control-layer/hooks.ts +++ b/dashboard/src/api/control-layer/hooks.ts @@ -694,14 +694,28 @@ export function useFile(id: string) { }); } -// Deprecated: Use dwctlApi.files.getContent() with useQuery directly instead -// export function useFileRequests(id: string, options?: FileRequestsListQuery) { -// return useQuery({ -// queryKey: queryKeys.files.requestsList(id, options || {}), -// queryFn: () => dwctlApi.files.getContent(id, {limit: options?.limit, offset: options?.skip}), -// enabled: !!id, -// }); -// } +export function useFileContent( + id: string, + options?: { limit?: number; skip?: number }, +) { + const queryClient = useQueryClient(); + + return useQuery({ + queryKey: queryKeys.files.requestsList(id, options || {}), + queryFn: () => dwctlApi.files.getFileContent(id, options), + enabled: !!id, + // Prefetch next page + select: (data) => { + if (data.incomplete && options?.limit && options?.skip) { + queryClient.prefetchQuery({ + queryKey: queryKeys.files.requestsList(id, options), + queryFn: () => dwctlApi.files.getFileContent(id, options), + }); + } + return data; + }, + }); +} export function useUploadFile() { const queryClient = useQueryClient(); @@ -928,7 +942,7 @@ export function useProcessPayment(options?: { return useMutation({ mutationKey: ["payments", "process"], mutationFn: async (sessionId: string) => { - await dwctlApi.payments.process(sessionId) + await dwctlApi.payments.process(sessionId); }, onSuccess: () => { // Refetch user data to update balance after successful payment diff --git a/dashboard/src/components/features/batches/Batches/Batches.pagination.test.tsx b/dashboard/src/components/features/batches/Batches/Batches.pagination.test.tsx index 56147ffaf..bc4b16f3f 100644 --- a/dashboard/src/components/features/batches/Batches/Batches.pagination.test.tsx +++ b/dashboard/src/components/features/batches/Batches/Batches.pagination.test.tsx @@ -185,18 +185,24 @@ describe("Batches - Pagination", () => { ).toBeInTheDocument(); }); - // Verify we're on page 1 - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + // Verify we're on page 1 - look for the active pagination link + const activePage = within(container).getByRole("link", { + current: "page", + }); + expect(activePage).toHaveTextContent("1"); - // Click Next button - const nextButton = within(container).getByRole("button", { - name: /Next/i, + // Click Next button - uses aria-label "Go to next page" + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); // Verify page 2 is shown await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); // Verify the correct cursor was used (last item from page 1) @@ -280,28 +286,34 @@ describe("Batches - Pagination", () => { }); // Navigate to page 2 - const nextButton = within(container).getByRole("button", { - name: /Next/i, + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); // Now click Previous - should go back to page 1 using cursor history - const prevButton = within(container).getByRole("button", { - name: /Previous/i, + const prevButton = within(container).getByRole("link", { + name: /go to previous page/i, }); await user.click(prevButton); // Should be back on page 1 await waitFor(() => { - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + const activePage1 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage1).toHaveTextContent("1"); }); - // The previous button should now be disabled (we're on page 0) - expect(prevButton).toBeDisabled(); + // The previous button should have pointer-events-none class (disabled) + expect(prevButton).toHaveClass("pointer-events-none"); }); it("should show First button only when on page 2 or higher", async () => { @@ -385,37 +397,43 @@ describe("Batches - Pagination", () => { // Page 1: No First button expect( - within(container).queryByRole("button", { name: /First/i }), + within(container).queryByRole("link", { name: /First/i }), ).not.toBeInTheDocument(); // Navigate to page 2 - const nextButton = within(container).getByRole("button", { - name: /Next/i, + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); - // Page 2: Still no First button (only shows on page 3+) + // Page 2: First button should now appear (currentPage > 1) expect( - within(container).queryByRole("button", { name: /First/i }), - ).not.toBeInTheDocument(); + within(container).getByRole("link", { name: /First/i }), + ).toBeInTheDocument(); // Navigate to page 3 - const nextButton2 = within(container).getByRole("button", { - name: /Next/i, + const nextButton2 = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton2); await waitFor(() => { - expect(within(container).getByText(/Page 3/i)).toBeInTheDocument(); + const activePage3 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage3).toHaveTextContent("3"); }); - // Page 3: First button should appear + // Page 3: First button should still appear expect( - within(container).getByRole("button", { name: /First/i }), + within(container).getByRole("link", { name: /First/i }), ).toBeInTheDocument(); }); @@ -512,36 +530,45 @@ describe("Batches - Pagination", () => { }); // Navigate to page 2, then page 3 - const nextButton = within(container).getByRole("button", { - name: /Next/i, + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); - const nextButton2 = within(container).getByRole("button", { - name: /Next/i, + const nextButton2 = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton2); await waitFor(() => { - expect(within(container).getByText(/Page 3/i)).toBeInTheDocument(); + const activePage3 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage3).toHaveTextContent("3"); }); // Click First button - const firstButton = within(container).getByRole("button", { + const firstButton = within(container).getByRole("link", { name: /First/i, }); await user.click(firstButton); // Should be back on page 1 await waitFor(() => { - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + const activePage1 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage1).toHaveTextContent("1"); }); // First button should no longer be visible expect( - within(container).queryByRole("button", { name: /First/i }), + within(container).queryByRole("link", { name: /First/i }), ).not.toBeInTheDocument(); }); @@ -616,13 +643,16 @@ describe("Batches - Pagination", () => { }); // Navigate to page 2 - const nextButton = within(container).getByRole("button", { - name: /Next/i, + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); // Change page size by clicking the combobox trigger @@ -643,13 +673,17 @@ describe("Batches - Pagination", () => { // Should reset to page 1 after changing page size await waitFor(() => { - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + const activePage1 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage1).toHaveTextContent("1"); }); - // Previous button should be disabled (back at page 1) - expect( - within(container).getByRole("button", { name: /Previous/i }), - ).toBeDisabled(); + // Previous button should have pointer-events-none class (disabled) + const prevButton = within(container).getByRole("link", { + name: /go to previous page/i, + }); + expect(prevButton).toHaveClass("pointer-events-none"); }); }); @@ -714,17 +748,23 @@ describe("Batches - Pagination", () => { ); await waitFor(() => { - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + const activePage = within(container).getByRole("link", { + current: "page", + }); + expect(activePage).toHaveTextContent("1"); }); // Navigate to page 2 - const nextButton = within(container).getByRole("button", { - name: /Next/i, + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); }); @@ -788,31 +828,40 @@ describe("Batches - Pagination", () => { ); await waitFor(() => { - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + const activePage = within(container).getByRole("link", { + current: "page", + }); + expect(activePage).toHaveTextContent("1"); }); // Navigate to page 2 - const nextButton = within(container).getByRole("button", { - name: /Next/i, + const nextButton = within(container).getByRole("link", { + name: /go to next page/i, }); await user.click(nextButton); await waitFor(() => { - expect(within(container).getByText(/Page 2/i)).toBeInTheDocument(); + const activePage2 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage2).toHaveTextContent("2"); }); // Navigate back to page 1 - const prevButton = within(container).getByRole("button", { - name: /Previous/i, + const prevButton = within(container).getByRole("link", { + name: /go to previous page/i, }); await user.click(prevButton); await waitFor(() => { - expect(within(container).getByText(/Page 1/i)).toBeInTheDocument(); + const activePage1 = within(container).getByRole("link", { + current: "page", + }); + expect(activePage1).toHaveTextContent("1"); }); - // Previous button should be disabled on page 1 - expect(prevButton).toBeDisabled(); + // Previous button should have pointer-events-none class (disabled) on page 1 + expect(prevButton).toHaveClass("pointer-events-none"); }); }); }); diff --git a/dashboard/src/components/features/batches/Batches/Batches.tsx b/dashboard/src/components/features/batches/Batches/Batches.tsx index 34be44007..d665e957f 100644 --- a/dashboard/src/components/features/batches/Batches/Batches.tsx +++ b/dashboard/src/components/features/batches/Batches/Batches.tsx @@ -20,6 +20,7 @@ import { SelectValue, } from "../../../ui/select"; import { DataTable } from "../../../ui/data-table"; +import { CursorPagination } from "../../../ui/cursor-pagination"; import { createFileColumns } from "../FilesTable/columns"; import { createBatchColumns } from "../BatchesTable/columns"; import { useFiles, useBatches } from "../../../../api/control-layer/hooks"; @@ -597,48 +598,21 @@ export function Batches({ } /> - {/* Server-side pagination controls */} -