mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-12-17 05:42:37 +00:00
# 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>
182 lines
4.6 KiB
TypeScript
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,
|
|
};
|
|
}
|