diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts index 13630f19bd..c57de675e0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts @@ -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({ 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), }); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx index 42189e391a..a5ec9d3578 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx @@ -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([]); // 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 = { + 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 = ({ - { + className?: string; + } +} + +interface AllModelsDataTableProps { + data: TData[]; + columns: ColumnDef[]; + isLoading?: boolean; + sorting?: SortingState; + onSortingChange?: OnChangeFn; + pagination?: PaginationState; + onPaginationChange?: OnChangeFn; + enablePagination?: boolean; +} + +export function AllModelsDataTable({ + data = [], + columns, + isLoading = false, + sorting = [], + onSortingChange, + pagination, + onPaginationChange, + enablePagination = false, +}: AllModelsDataTableProps) { + const [columnResizeMode] = React.useState("onChange"); + const [columnSizing, setColumnSizing] = React.useState({}); + const [columnVisibility, setColumnVisibility] = React.useState({}); + + 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 ( +
+
+
+ + + {tableInstance.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ {header.id !== "actions" && header.column.getCanSort() && ( +
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: , + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+ )} +
+ {header.column.getCanResize() && ( +
+ )} + + ))} + + ))} + + + {isLoading ? ( + + +
+

🚅 Loading models...

+
+
+
+ ) : tableInstance.getRowModel().rows.length > 0 ? ( + tableInstance.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + +
+

No models found

+
+
+
+ )} +
+
+
+
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index e0dccbe6e8..d66ef7c4bc 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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()}`; }