llama-stack-mirror/src/llama_stack_ui/hooks/use-pagination.ts
Francisco Javier Arceo fcea9893a4
feat(UI): Adding Files API to Admin UI (#4319)
# What does this PR do?


## Files Admin Page

<img width="1919" height="1238" alt="Screenshot 2025-12-09 at 10 33
06 AM"
src="https://github.com/user-attachments/assets/3dd545f0-32bc-45be-af2b-1823800015f2"
/>

## Files Upload Modal
<img width="1919" height="1287" alt="Screenshot 2025-12-09 at 10 33
38 AM"
src="https://github.com/user-attachments/assets/776bb372-75d3-4ccd-b6b5-c9dfb3fcb350"
/>

## Files Detail
<img width="1918" height="1099" alt="Screenshot 2025-12-09 at 10 34
26 AM"
src="https://github.com/user-attachments/assets/f256dbf8-4047-4d79-923d-404161b05f36"
/>

Note, content preview has some handling for JSON, CSV, and PDF to enable
nicer rendering. Pure text rendering is trivial.

### Files Detail File Content Preview (TXT)
<img width="1918" height="1341" alt="Screenshot 2025-12-09 at 10 41
20 AM"
src="https://github.com/user-attachments/assets/4fa0ddb7-ffff-424b-b764-0bd4af6ed976"
/>

### Files Detail File Content Preview (JSON)
<img width="1909" height="1233" alt="Screenshot 2025-12-09 at 10 39
57 AM"
src="https://github.com/user-attachments/assets/b912f07a-2dff-483b-b73c-2f69dd0d87ad"
/>

### Files Detail File Content Preview (HTML)
<img width="1916" height="1348" alt="Screenshot 2025-12-09 at 10 40
27 AM"
src="https://github.com/user-attachments/assets/17ebec0a-8754-4552-977d-d3c44f7f6973"
/>

### Files Detail File Content Preview (CSV)
<img width="1919" height="1177" alt="Screenshot 2025-12-09 at 10 34
50 AM"
src="https://github.com/user-attachments/assets/20bd0755-1757-4a3a-99d2-fbd072f81f49"
/>

### Files Detail File Content Preview (PDF)
<img width="1917" height="1154" alt="Screenshot 2025-12-09 at 10 36
48 AM"
src="https://github.com/user-attachments/assets/2873e6fe-4da3-4cbd-941b-7d903270b749"
/>


Closes https://github.com/llamastack/llama-stack/issues/4144

## Test Plan
Added Tests

Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
2025-12-09 16:28:05 -05:00

182 lines
4.6 KiB
TypeScript

"use client";
import { useState, useCallback, useEffect, useRef } from "react";
import { PaginationStatus, UsePaginationOptions } from "@/lib/types";
import { useSession } from "next-auth/react";
import { useAuthClient } from "@/hooks/use-auth-client";
import { useRouter } from "next/navigation";
interface PaginationState<T> {
data: T[];
status: PaginationStatus;
hasMore: boolean;
error: Error | null;
lastId: string | null;
}
interface PaginationResponse<T> {
data: T[];
has_more: boolean;
last_id: string;
first_id: string;
object: "list";
}
export interface PaginationReturn<T> {
data: T[];
status: PaginationStatus;
hasMore: boolean;
error: Error | null;
loadMore: () => void;
refetch: () => void;
}
interface UsePaginationParams<T> extends UsePaginationOptions {
fetchFunction: (
client: ReturnType<typeof useAuthClient>,
params: {
after?: string;
limit: number;
model?: string;
order?: string;
}
) => Promise<PaginationResponse<T>>;
errorMessagePrefix: string;
enabled?: boolean;
useAuth?: boolean;
}
export function usePagination<T>({
limit = 20,
model,
order = "desc",
fetchFunction,
errorMessagePrefix,
enabled = true,
useAuth = true,
}: UsePaginationParams<T>): PaginationReturn<T> {
const { status: sessionStatus } = useSession();
const client = useAuthClient();
const router = useRouter();
const [state, setState] = useState<PaginationState<T>>({
data: [],
status: "loading",
hasMore: true,
error: null,
lastId: null,
});
// Use refs to avoid stale closures
const stateRef = useRef(state);
stateRef.current = state;
// Track if initial data has been fetched
const hasFetchedInitialData = useRef(false);
/**
* Fetches data from the API with cursor-based pagination
*/
const fetchData = useCallback(
async (after?: string, targetRows?: number) => {
const isInitialLoad = !after;
const fetchLimit = targetRows || limit;
try {
setState(prev => ({
...prev,
status: isInitialLoad ? "loading" : "loading-more",
error: null,
}));
const response = await fetchFunction(client, {
after: after || undefined,
limit: fetchLimit,
...(model && { model }),
...(order && { order }),
});
setState(prev => ({
...prev,
data: isInitialLoad
? response.data
: [...prev.data, ...response.data],
hasMore: response.has_more,
lastId: response.last_id || null,
status: "idle",
}));
} catch (err) {
// Check if it's a 401 unauthorized error
if (
err &&
typeof err === "object" &&
"status" in err &&
err.status === 401
) {
router.push("/auth/signin");
return;
}
const errorMessage = isInitialLoad
? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.`
: `Failed to load more ${errorMessagePrefix}. Please try again.`;
const error =
err instanceof Error
? new Error(`${errorMessage} ${err.message}`)
: new Error(errorMessage);
setState(prev => ({
...prev,
error,
status: "error",
}));
}
},
[limit, model, order, fetchFunction, errorMessagePrefix, client, router]
);
/**
* Loads more data for infinite scroll
*/
const loadMore = useCallback(() => {
const currentState = stateRef.current;
if (currentState.hasMore && currentState.status === "idle") {
fetchData(currentState.lastId || undefined);
}
}, [fetchData]);
/**
* Refetches data from the beginning (resets pagination)
*/
const refetch = useCallback(() => {
fetchData();
}, [fetchData]);
// Auto-load initial data on mount when enabled
useEffect(() => {
// If using auth, wait for session to load
const isAuthReady = !useAuth || sessionStatus !== "loading";
const shouldFetch = enabled && isAuthReady;
if (shouldFetch && !hasFetchedInitialData.current) {
hasFetchedInitialData.current = true;
fetchData();
} else if (!shouldFetch) {
// Reset the flag when disabled so it can fetch when re-enabled
hasFetchedInitialData.current = false;
}
}, [fetchData, enabled, useAuth, sessionStatus]);
// Override status if we're waiting for auth
const effectiveStatus =
useAuth && sessionStatus === "loading" ? "loading" : state.status;
return {
data: state.data,
status: effectiveStatus,
hasMore: state.hasMore,
error: state.error,
loadMore,
refetch,
};
}