From f17b5730a56d2525829de2b18d5748e6e6705a10 Mon Sep 17 00:00:00 2001 From: tanjiro <56165694+NANDINI-star@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:06:29 +0900 Subject: [PATCH] client side pagination of models page --- .../src/components/model_dashboard/table.tsx | 3 +- .../components/templates/model_dashboard.tsx | 159 +++++++++++------- 2 files changed, 96 insertions(+), 66 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_dashboard/table.tsx b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx index 30f01d91db5..cca7bc08608 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/table.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx @@ -8,6 +8,7 @@ import { useReactTable, ColumnResizeMode, VisibilityState, + PaginationState, } from "@tanstack/react-table"; import React from "react"; import { @@ -18,7 +19,7 @@ import { TableRow, TableCell, } from "@tremor/react"; -import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, TableIcon } from "@heroicons/react/outline"; +import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; // Extend the column meta type to include className declare module "@tanstack/react-table" { diff --git a/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx b/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx index 4dd66d0bf5e..2eff0bf3e36 100644 --- a/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/templates/model_dashboard.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react" +import React, { useState, useEffect, useRef, useMemo } from "react" import { Card, Title, @@ -61,7 +61,7 @@ import HealthCheckComponent from "../model_dashboard/HealthCheckComponent"; import PassThroughSettings from "../pass_through_settings"; import ModelGroupAliasSettings from "../model_group_alias_settings"; import { all_admin_roles } from "@/utils/roles"; -import { Table as TableInstance } from "@tanstack/react-table"; +import { Table as TableInstance, PaginationState } from "@tanstack/react-table"; import NotificationsManager from "../molecules/notifications_manager"; interface ModelDashboardProps { @@ -200,6 +200,12 @@ const ModelDashboard: React.FC = ({ const [expandedRows, setExpandedRows] = useState>(new Set()) const dropdownRef = useRef(null) const tableRef = useRef>(null) + + // Pagination state + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 50, + }) const [selectedTabIndex, setSelectedTabIndex] = useState(0) const handleCreateNewModelClick = () => { @@ -215,8 +221,57 @@ const ModelDashboard: React.FC = ({ setSelectedModelAccessGroupFilter(null) setCurrentTeam("personal") setModelViewMode("current_team") + setPagination({ pageIndex: 0, pageSize: 50 }) } + // Memoize filtered data to prevent unnecessary re-calculations + const filteredData = useMemo(() => { + if (!modelData || !modelData.data || modelData.data.length === 0) { + return []; + } + + return modelData.data.filter((model: any) => { + const searchMatch = + modelNameSearch === "" || + model.model_name.toLowerCase().includes(modelNameSearch.toLowerCase()) + + const modelNameMatch = + selectedModelGroup === "all" || + model.model_name === selectedModelGroup || + !selectedModelGroup || + (selectedModelGroup === "wildcard" && model.model_name?.includes("*")) + + const accessGroupMatch = + selectedModelAccessGroupFilter === "all" || + model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || + !selectedModelAccessGroupFilter + + let teamAccessMatch = true + if (modelViewMode === "current_team") { + if (currentTeam === "personal") { + teamAccessMatch = model.model_info?.direct_access === true + } else { + teamAccessMatch = + model.model_info?.access_via_team_ids?.includes(currentTeam) === true + } + } + + return searchMatch && modelNameMatch && accessGroupMatch && teamAccessMatch + }); + }, [modelData, modelNameSearch, selectedModelGroup, selectedModelAccessGroupFilter, currentTeam, modelViewMode]); + + // Memoize paginated data + const paginatedData = useMemo(() => { + const startIndex = pagination.pageIndex * pagination.pageSize; + const endIndex = startIndex + pagination.pageSize; + return filteredData.slice(startIndex, endIndex); + }, [filteredData, pagination.pageIndex, pagination.pageSize]); + + // Reset pagination when filters change + useEffect(() => { + setPagination(prev => ({ ...prev, pageIndex: 0 })) + }, [modelNameSearch, selectedModelGroup, selectedModelAccessGroupFilter, currentTeam, modelViewMode]) + const setProviderModelsFn = (provider: Providers) => { const _providerModels = getProviderModels(provider, modelMap) setProviderModels(_providerModels) @@ -1208,40 +1263,45 @@ const ModelDashboard: React.FC = ({ )} - {/* Results Count */} + {/* Results Count and Pagination Controls */}
- Showing{" "} - {modelData && modelData.data.length > 0 - ? modelData.data.filter((model: any) => { - const searchMatch = - modelNameSearch === "" || - model.model_name.toLowerCase().includes(modelNameSearch.toLowerCase()) - - const modelNameMatch = - selectedModelGroup === "all" || - model.model_name === selectedModelGroup || - !selectedModelGroup || - (selectedModelGroup === "wildcard" && model.model_name?.includes("*")) - const accessGroupMatch = - selectedModelAccessGroupFilter === "all" || - model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || - !selectedModelAccessGroupFilter - let teamAccessMatch = true - if (modelViewMode === "current_team") { - if (currentTeam === "personal") { - teamAccessMatch = model.model_info?.direct_access === true - } else { - teamAccessMatch = - model.model_info?.access_via_team_ids?.includes(currentTeam) === true - } - } - - return searchMatch && modelNameMatch && accessGroupMatch && teamAccessMatch - }).length - : 0}{" "} - results + {filteredData.length > 0 ? ( + `Showing ${pagination.pageIndex * pagination.pageSize + 1} - ${Math.min( + (pagination.pageIndex + 1) * pagination.pageSize, + filteredData.length + )} of ${filteredData.length} results` + ) : ( + "Showing 0 results" + )} + + {/* Pagination Controls */} + {filteredData.length > pagination.pageSize && ( +
+ + + +
+ )}
@@ -1260,38 +1320,7 @@ const ModelDashboard: React.FC = ({ expandedRows, setExpandedRows, )} - data={modelData.data.filter((model: any) => { - // Model name search filter - const searchMatch = - modelNameSearch === "" || - model.model_name.toLowerCase().includes(modelNameSearch.toLowerCase()) - - // Model name filter - const modelNameMatch = - selectedModelGroup === "all" || - model.model_name === selectedModelGroup || - !selectedModelGroup || - (selectedModelGroup === "wildcard" && model.model_name?.includes("*")) - // Model access group filter - const accessGroupMatch = - selectedModelAccessGroupFilter === "all" || - model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || - !selectedModelAccessGroupFilter - // Team access filter based on current team and view mode - let teamAccessMatch = true - if (modelViewMode === "current_team") { - if (currentTeam === "personal") { - // Show only models with direct access - teamAccessMatch = model.model_info?.direct_access === true - } else { - // Show only models accessible by the current team - teamAccessMatch = model.model_info?.access_via_team_ids?.includes(currentTeam) === true - } - } - // For 'all' mode, show all models (teamAccessMatch remains true) - - return searchMatch && modelNameMatch && accessGroupMatch && teamAccessMatch - })} + data={paginatedData} isLoading={false} table={tableRef} />