From b69c1dcee7e989574905ca1b4181e219ff0fe622 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 29 Dec 2025 15:09:33 -0800 Subject: [PATCH 1/2] Key List table uses expand from /key/list --- .../src/components/all_keys_table.tsx | 79 ++++++++++--------- .../components/key_team_helpers/key_list.tsx | 20 ++++- .../src/components/networking.tsx | 6 ++ .../components/templates/view_key_table.tsx | 22 +++--- 4 files changed, 75 insertions(+), 52 deletions(-) diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index 210ce09fa34..10f678be03f 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -1,23 +1,39 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { ColumnDef, ColumnResizeMode, ColumnResizeDirection } from "@tanstack/react-table"; -import { Select, SelectItem } from "@tremor/react"; -import { Button } from "@tremor/react"; -import KeyInfoView from "./templates/key_info_view"; -import { Tooltip } from "antd"; -import { Team, KeyResponse } from "./key_team_helpers/key_list"; -import FilterComponent from "./molecules/filter"; -import { FilterOption } from "./molecules/filter"; -import { Organization, userListCall } from "./networking"; -import { useFilterLogic } from "./key_team_helpers/filter_logic"; import { Setter } from "@/types"; -import { updateExistingKeys } from "@/utils/dataUtils"; -import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table"; -import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react"; -import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; -import { Badge, Text } from "@tremor/react"; +import { formatNumberWithCommas, updateExistingKeys } from "@/utils/dataUtils"; +import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline"; +import { + ColumnDef, + ColumnResizeDirection, + ColumnResizeMode, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { + Badge, + Button, + Icon, + Select, + SelectItem, + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + Text, +} from "@tremor/react"; +import { Tooltip } from "antd"; +import React, { useEffect, useState } from "react"; import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { useFilterLogic } from "./key_team_helpers/filter_logic"; +import { KeyResponse, Team } from "./key_team_helpers/key_list"; +import FilterComponent, { FilterOption } from "./molecules/filter"; +import { Organization } from "./networking"; +import KeyInfoView from "./templates/key_info_view"; interface AllKeysTableProps { keys: KeyResponse[]; @@ -124,7 +140,6 @@ export function AllKeysTable({ setAccessToken, }: AllKeysTableProps) { const [selectedKeyId, setSelectedKeyId] = useState(null); - const [userList, setUserList] = useState([]); const [columnResizeMode, setColumnResizeMode] = React.useState("onChange"); const [columnResizeDirection, setColumnResizeDirection] = React.useState("ltr"); const [sorting, setSorting] = React.useState(() => { @@ -155,17 +170,6 @@ export function AllKeysTable({ accessToken, }); - useEffect(() => { - if (accessToken) { - const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null); - const fetchUserList = async () => { - const userListData = await userListCall(accessToken, user_IDs, 1, 100); - setUserList(userListData.users); - }; - fetchUserList(); - } - }, [accessToken, keys]); - // Add a useEffect to call refresh when a key is created useEffect(() => { if (refresh) { @@ -269,18 +273,19 @@ export function AllKeysTable({ }, { id: "user_email", - accessorKey: "user_id", + accessorKey: "user", header: "User Email", size: 160, cell: (info) => { - const userId = info.getValue() as string; - const user = userList.find((u) => u.user_id === userId); - return user?.user_email ? ( - - {user?.user_email.slice(0, 20)}... + const user = info.getValue() as any; + const value = user?.user_email; + const width = info.cell.column.getSize(); + return ( + + + {value ?? "-"} + - ) : ( - "-" ); }, }, diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 6bb014e6187..9f95ba46cc1 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react"; -import { keyListCall, Member, Organization } from "../networking"; import { Setter } from "@/types"; +import { useEffect, useState } from "react"; +import { keyListCall, Member, Organization } from "../networking"; export interface Team { team_id: string; @@ -106,6 +106,7 @@ interface UseKeyListProps { selectedKeyAlias: string | null; accessToken: string; createClicked: boolean; + expand?: string[]; } interface PaginationData { @@ -129,6 +130,7 @@ const useKeyList = ({ selectedKeyAlias, accessToken, createClicked, + expand = [], }: UseKeyListProps): UseKeyListReturn => { const [keyData, setKeyData] = useState({ keys: [], @@ -151,7 +153,19 @@ const useKeyList = ({ const page = typeof params.page === "number" ? params.page : 1; const pageSize = typeof params.pageSize === "number" ? params.pageSize : 100; - const data = await keyListCall(accessToken, null, null, null, null, null, page, pageSize); + const data = await keyListCall( + accessToken, + null, + null, + null, + null, + null, + page, + pageSize, + null, + null, + expand.join(","), + ); console.log("data", data); setKeyData(data); setError(null); diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index aeba207db26..987aaf96cb3 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -3250,6 +3250,7 @@ export const keyListCall = async ( pageSize: number, sortBy: string | null = null, sortOrder: string | null = null, + expand: string | null = null, ) => { /** * Get all available teams on proxy @@ -3294,6 +3295,11 @@ export const keyListCall = async ( if (sortOrder) { queryParams.append("sort_order", sortOrder); } + + if (expand) { + queryParams.append("expand", expand); + } + queryParams.append("return_full_object", "true"); queryParams.append("include_team_keys", "true"); queryParams.append("include_created_by_keys", "true"); diff --git a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx index 5defc287a07..f8a86f0fad4 100644 --- a/ui/litellm-dashboard/src/components/templates/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -1,17 +1,14 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { keyDeleteCall, Organization } from "../networking"; -import { add } from "date-fns"; -import { regenerateKeyCall } from "../networking"; -import { Grid, Col, Button, Text, Title, TextInput } from "@tremor/react"; -import { fetchAvailableModelsForTeamOrKey } from "../key_team_helpers/fetch_available_models_team_key"; -import { Modal, Form, InputNumber } from "antd"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import useKeyList from "../key_team_helpers/key_list"; -import { KeyResponse } from "../key_team_helpers/key_list"; -import { AllKeysTable } from "../all_keys_table"; -import { Team } from "../key_team_helpers/key_list"; import { Setter } from "@/types"; +import { Button, Col, Grid, Text, TextInput, Title } from "@tremor/react"; +import { Form, InputNumber, Modal } from "antd"; +import { add } from "date-fns"; +import React, { useEffect, useState } from "react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { AllKeysTable } from "../all_keys_table"; +import { fetchAvailableModelsForTeamOrKey } from "../key_team_helpers/fetch_available_models_team_key"; +import useKeyList, { KeyResponse, Team } from "../key_team_helpers/key_list"; +import { keyDeleteCall, Organization, regenerateKeyCall } from "../networking"; import NotificationManager from "../molecules/notifications_manager"; @@ -125,6 +122,7 @@ const ViewKeyTable: React.FC = ({ selectedKeyAlias, accessToken: accessToken || "", createClicked, + expand: ["user"], }); const handlePageChange = (newPage: number) => { From b21b156e5567ee2eb943f408b6f496e874397720 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 29 Dec 2025 15:21:19 -0800 Subject: [PATCH 2/2] adding tests --- .../src/components/all_keys_table.test.tsx | 36 +++++++++++++++++++ .../components/key_team_helpers/key_list.tsx | 4 +++ 2 files changed, 40 insertions(+) diff --git a/ui/litellm-dashboard/src/components/all_keys_table.test.tsx b/ui/litellm-dashboard/src/components/all_keys_table.test.tsx index 201a4be75bd..1458143d77e 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.test.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.test.tsx @@ -93,6 +93,10 @@ const mockKey: KeyResponse = { user_tpm_limit: 1000, user_rpm_limit: 100, user_email: "user@example.com", + user: { + user_email: "user@example.com", + user_id: "user-1", + }, }; const mockTeam: Team = { @@ -190,3 +194,35 @@ it("should display key information correctly", async () => { expect(screen.getByText("5.5000")).toBeInTheDocument(); }); }); + +it("should display user email correctly", async () => { + const mockProps = { + keys: [mockKey], + setKeys: vi.fn(), + isLoading: false, + pagination: { + currentPage: 1, + totalPages: 1, + totalCount: 1, + }, + onPageChange: vi.fn(), + pageSize: 50, + teams: [mockTeam], + selectedTeam: null, + setSelectedTeam: vi.fn(), + selectedKeyAlias: null, + setSelectedKeyAlias: vi.fn(), + accessToken: "test-token", + userID: "user-1", + userRole: "admin", + organizations: [mockOrganization], + setCurrentOrg: vi.fn(), + premiumUser: false, + }; + + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByText("user@example.com")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 9f95ba46cc1..a04fbf3943d 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -91,6 +91,10 @@ export interface KeyResponse { last_rotation_at?: string; key_rotation_at?: string; next_rotation_at?: string; + user?: { + user_id: string; + user_email: string; + }; } interface KeyListResponse {