mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 01:05:43 +00:00
All Models Page server side sorting
This commit is contained in:
@@ -27,7 +27,7 @@ const modelHubKeys = createQueryKeys("modelHub");
|
||||
const allProxyModelsKeys = createQueryKeys("allProxyModels");
|
||||
const selectedTeamModelsKeys = createQueryKeys("selectedTeamModels");
|
||||
|
||||
export const useModelsInfo = (page: number = 1, size: number = 50, search?: string, modelId?: string, teamId?: string) => {
|
||||
export const useModelsInfo = (page: number = 1, size: number = 50, search?: string, modelId?: string, teamId?: string, sortBy?: string, sortOrder?: string) => {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
return useQuery<PaginatedModelInfoResponse>({
|
||||
queryKey: modelKeys.list({
|
||||
@@ -39,9 +39,11 @@ export const useModelsInfo = (page: number = 1, size: number = 50, search?: stri
|
||||
...(search && { search }),
|
||||
...(modelId && { modelId }),
|
||||
...(teamId && { teamId }),
|
||||
...(sortBy && { sortBy }),
|
||||
...(sortOrder && { sortOrder }),
|
||||
},
|
||||
}),
|
||||
queryFn: async () => await modelInfoCall(accessToken!, userId!, userRole!, page, size, search, modelId, teamId),
|
||||
queryFn: async () => await modelInfoCall(accessToken!, userId!, userRole!, page, size, search, modelId, teamId, sortBy, sortOrder),
|
||||
enabled: Boolean(accessToken && userId && userRole),
|
||||
});
|
||||
};
|
||||
|
||||
+37
-5
@@ -2,11 +2,11 @@ import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap"
|
||||
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { ModelDataTable } from "@/components/model_dashboard/table";
|
||||
import { AllModelsDataTable } from "@/components/model_dashboard/all_models_table";
|
||||
import { columns } from "@/components/molecules/models/columns";
|
||||
import { getDisplayModelName } from "@/components/view_model/model_name_display";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { PaginationState } from "@tanstack/react-table";
|
||||
import { PaginationState, SortingState } from "@tanstack/react-table";
|
||||
import { Grid, Select, SelectItem, TabPanel, Text } from "@tremor/react";
|
||||
import { Skeleton, Spin } from "antd";
|
||||
import debounce from "lodash/debounce";
|
||||
@@ -49,6 +49,7 @@ const AllModelsTab = ({
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
// Debounce search input
|
||||
const debouncedUpdateSearch = useMemo(
|
||||
@@ -72,12 +73,33 @@ const AllModelsTab = ({
|
||||
// Determine teamId to pass to the query - only pass if not "personal"
|
||||
const teamIdForQuery = currentTeam === "personal" ? undefined : currentTeam.team_id;
|
||||
|
||||
// Convert sorting state to sortBy and sortOrder for API
|
||||
const sortBy = useMemo(() => {
|
||||
if (sorting.length === 0) return undefined;
|
||||
const sort = sorting[0];
|
||||
// Map column IDs to server-side field names
|
||||
// The server expects field names like "model_name", "created_at", etc.
|
||||
const columnIdToServerField: Record<string, string> = {
|
||||
input_cost: "costs", // Map input_cost column to "costs" for server-side sorting
|
||||
model_info_db_model: "status", // Map model_info.db_model column to "status" for server-side sorting
|
||||
};
|
||||
return columnIdToServerField[sort.id] || sort.id;
|
||||
}, [sorting]);
|
||||
|
||||
const sortOrder = useMemo(() => {
|
||||
if (sorting.length === 0) return undefined;
|
||||
const sort = sorting[0];
|
||||
return sort.desc ? "desc" : "asc";
|
||||
}, [sorting]);
|
||||
|
||||
const { data: rawModelData, isLoading: isLoadingModelsInfo } = useModelsInfo(
|
||||
currentPage,
|
||||
pageSize,
|
||||
debouncedSearch || undefined,
|
||||
undefined,
|
||||
teamIdForQuery
|
||||
teamIdForQuery,
|
||||
sortBy,
|
||||
sortOrder
|
||||
);
|
||||
const isLoading = isLoadingModelsInfo || isLoadingModelCostMap;
|
||||
|
||||
@@ -139,6 +161,7 @@ const AllModelsTab = ({
|
||||
|
||||
useEffect(() => {
|
||||
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
|
||||
setCurrentPage(1);
|
||||
}, [selectedModelGroup, selectedModelAccessGroupFilter]);
|
||||
|
||||
// Reset pagination when team changes
|
||||
@@ -147,6 +170,12 @@ const AllModelsTab = ({
|
||||
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
|
||||
}, [teamIdForQuery]);
|
||||
|
||||
// Reset pagination when sorting changes
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
|
||||
}, [sorting]);
|
||||
|
||||
const resetFilters = () => {
|
||||
setModelNameSearch("");
|
||||
setSelectedModelGroup("all");
|
||||
@@ -155,6 +184,7 @@ const AllModelsTab = ({
|
||||
setModelViewMode("current_team");
|
||||
setCurrentPage(1);
|
||||
setPagination({ pageIndex: 0, pageSize: 50 });
|
||||
setSorting([]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -439,7 +469,7 @@ const AllModelsTab = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelDataTable
|
||||
<AllModelsDataTable
|
||||
columns={columns(
|
||||
userRole,
|
||||
userId,
|
||||
@@ -453,7 +483,9 @@ const AllModelsTab = ({
|
||||
setExpandedRows,
|
||||
)}
|
||||
data={filteredData}
|
||||
isLoading={false}
|
||||
isLoading={isLoadingModelsInfo}
|
||||
sorting={sorting}
|
||||
onSortingChange={setSorting}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
enablePagination={true}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
ColumnResizeMode,
|
||||
VisibilityState,
|
||||
PaginationState,
|
||||
OnChangeFn,
|
||||
} from "@tanstack/react-table";
|
||||
import React from "react";
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
|
||||
|
||||
// Extend the column meta type to include className
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData, TValue> {
|
||||
className?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface AllModelsDataTableProps<TData, TValue> {
|
||||
data: TData[];
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
isLoading?: boolean;
|
||||
sorting?: SortingState;
|
||||
onSortingChange?: OnChangeFn<SortingState>;
|
||||
pagination?: PaginationState;
|
||||
onPaginationChange?: OnChangeFn<PaginationState>;
|
||||
enablePagination?: boolean;
|
||||
}
|
||||
|
||||
export function AllModelsDataTable<TData, TValue>({
|
||||
data = [],
|
||||
columns,
|
||||
isLoading = false,
|
||||
sorting = [],
|
||||
onSortingChange,
|
||||
pagination,
|
||||
onPaginationChange,
|
||||
enablePagination = false,
|
||||
}: AllModelsDataTableProps<TData, TValue>) {
|
||||
const [columnResizeMode] = React.useState<ColumnResizeMode>("onChange");
|
||||
const [columnSizing, setColumnSizing] = React.useState({});
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||
|
||||
const tableInstance = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnSizing,
|
||||
columnVisibility,
|
||||
...(enablePagination && pagination ? { pagination } : {}),
|
||||
},
|
||||
columnResizeMode,
|
||||
onSortingChange: onSortingChange,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
...(enablePagination && onPaginationChange ? { onPaginationChange } : {}),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
// NO getSortedRowModel - sorting is handled server-side
|
||||
...(enablePagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
|
||||
enableSorting: true,
|
||||
enableColumnResizing: true,
|
||||
manualSorting: true, // Enable manual sorting for server-side sorting
|
||||
defaultColumn: {
|
||||
minSize: 40,
|
||||
maxSize: 500,
|
||||
},
|
||||
});
|
||||
|
||||
const getHeaderText = (header: any): string => {
|
||||
if (typeof header === "string") {
|
||||
return header;
|
||||
}
|
||||
if (typeof header === "function") {
|
||||
const headerElement = header();
|
||||
if (headerElement && headerElement.props && headerElement.props.children) {
|
||||
const children = headerElement.props.children;
|
||||
if (typeof children === "string") {
|
||||
return children;
|
||||
}
|
||||
if (children.props && children.props.children) {
|
||||
return children.props.children;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="relative min-w-full">
|
||||
<Table className="[&_td]:py-2 [&_th]:py-2 w-full">
|
||||
<TableHead>
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className={`py-1 h-8 relative ${
|
||||
header.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] w-[120px] ml-8"
|
||||
: ""
|
||||
} ${header.column.columnDef.meta?.className || ""}`}
|
||||
style={{
|
||||
width: header.id === "actions" ? 120 : header.getSize(),
|
||||
position: header.id === "actions" ? "sticky" : "relative",
|
||||
right: header.id === "actions" ? 0 : "auto",
|
||||
}}
|
||||
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && header.column.getCanSort() && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{header.column.getCanResize() && (
|
||||
<div
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={`absolute right-0 top-0 h-full w-2 cursor-col-resize select-none touch-none ${
|
||||
header.column.getIsResizing() ? "bg-blue-500" : "hover:bg-blue-200"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>🚅 Loading models...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : tableInstance.getRowModel().rows.length > 0 ? (
|
||||
tableInstance.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`py-0.5 ${
|
||||
cell.column.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] w-[120px] ml-8"
|
||||
: ""
|
||||
} ${cell.column.columnDef.meta?.className || ""}`}
|
||||
style={{
|
||||
width: cell.column.id === "actions" ? 120 : cell.column.getSize(),
|
||||
position: cell.column.id === "actions" ? "sticky" : "relative",
|
||||
right: cell.column.id === "actions" ? 0 : "auto",
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No models found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2007,12 +2007,12 @@ export const regenerateKeyCall = async (accessToken: string, keyToRegenerate: st
|
||||
let ModelListerrorShown = false;
|
||||
let errorTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
export const modelInfoCall = async (accessToken: string, userID: string, userRole: string, page: number = 1, size: number = 50, search?: string, modelId?: string, teamId?: string) => {
|
||||
export const modelInfoCall = async (accessToken: string, userID: string, userRole: string, page: number = 1, size: number = 50, search?: string, modelId?: string, teamId?: string, sortBy?: string, sortOrder?: string) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
*/
|
||||
try {
|
||||
console.log("modelInfoCall:", accessToken, userID, userRole, page, size, search, modelId, teamId);
|
||||
console.log("modelInfoCall:", accessToken, userID, userRole, page, size, search, modelId, teamId, sortBy, sortOrder);
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/model/info` : `/v2/model/info`;
|
||||
const params = new URLSearchParams();
|
||||
params.append("include_team_models", "true");
|
||||
@@ -2027,6 +2027,12 @@ export const modelInfoCall = async (accessToken: string, userID: string, userRol
|
||||
if (teamId && teamId.trim()) {
|
||||
params.append("teamId", teamId.trim());
|
||||
}
|
||||
if (sortBy && sortBy.trim()) {
|
||||
params.append("sortBy", sortBy.trim());
|
||||
}
|
||||
if (sortOrder && sortOrder.trim()) {
|
||||
params.append("sortOrder", sortOrder.trim());
|
||||
}
|
||||
if (params.toString()) {
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user