All Models Page server side sorting

This commit is contained in:
yuneng-jiang
2026-01-27 18:08:17 -08:00
parent 936a4fc884
commit 37bfab929d
4 changed files with 245 additions and 9 deletions
@@ -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),
});
};
@@ -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()}`;
}