From 13aebafff42cf6403e33885a9d139ad86c3c36e2 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 16 Jan 2026 18:13:14 -0800 Subject: [PATCH] temp commit for branch switching --- .../src/app/(dashboard)/hooks/keys/useKeys.ts | 28 ++ .../DeletedKeysPage/DeletedKeysPage.tsx | 27 ++ .../DeletedKeysTable/DeletedKeysTable.tsx | 397 ++++++++++++++++++ .../src/components/networking.tsx | 7 +- .../src/components/view_logs/index.tsx | 26 +- 5 files changed, 471 insertions(+), 14 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx create mode 100644 ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts index cd3df1a3820..1f6eb8eeb68 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeys.ts @@ -13,6 +13,20 @@ export interface KeysResponse { total_pages: number; } +export interface DeletedKeyResponse { + token: string; + token_id: string; + key_name: string; + key_alias: string; +} + +export interface DeletedKeysResponse { + keys: DeletedKeyResponse[]; + total_count: number; + current_page: number; + total_pages: number; +} + export const useKeys = (page: number, pageSize: number): UseQueryResult => { const { accessToken } = useAuthorized(); @@ -34,3 +48,17 @@ export const useKeys = (page: number, pageSize: number): UseQueryResult => { + const { accessToken } = useAuthorized(); + + return useQuery({ + queryKey: deletedKeyKeys.list({ page, limit: pageSize }), + queryFn: async () => + await keyListCall(accessToken!, null, null, null, null, null, page, pageSize, null, null, null, "deleted"), + enabled: Boolean(accessToken), + staleTime: 30000, // 30 seconds + placeholderData: keepPreviousData, + }); +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx new file mode 100644 index 00000000000..0ca6438a095 --- /dev/null +++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx @@ -0,0 +1,27 @@ +"use client"; +import { useState } from "react"; +import { useDeletedKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; +import { DeletedKeysTable } from "./DeletedKeysTable/DeletedKeysTable"; + +export default function DeletedKeysPage() { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize] = useState(50); + + const { + data: keysData, + isPending: isLoading, + isFetching, + } = useDeletedKeys(pageIndex + 1, pageSize); + + return ( + + ); +} diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx new file mode 100644 index 00000000000..6a39109e555 --- /dev/null +++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTable.tsx @@ -0,0 +1,397 @@ +"use client"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { ChevronDownIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + PaginationState, + SortingState, + useReactTable, +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, +} from "@tremor/react"; +import { Tooltip } from "antd"; +import React, { useState } from "react"; +import { KeyResponse } from "../../key_team_helpers/key_list"; + +interface DeletedKeysTableProps { + keys: KeyResponse[]; + totalCount: number; + isLoading: boolean; + isFetching: boolean; + pageIndex: number; + pageSize: number; + onPageChange: (pageIndex: number) => void; +} + +export function DeletedKeysTable({ + keys, + totalCount, + isLoading, + isFetching, + pageIndex, + pageSize, + onPageChange, +}: DeletedKeysTableProps) { + const [sorting, setSorting] = useState([ + { + id: "deleted_at", + desc: true, + }, + ]); + + const [tablePagination, setTablePagination] = useState({ + pageIndex, + pageSize, + }); + + // Sync pagination state when prop changes + React.useEffect(() => { + setTablePagination({ pageIndex, pageSize }); + }, [pageIndex, pageSize]); + + const columns: ColumnDef[] = [ + { + id: "token", + accessorKey: "token", + header: "Key ID", + size: 150, + cell: (info) => { + const value = info.getValue() as string; + return ( + + + {value || "-"} + + + ); + }, + }, + { + id: "key_alias", + accessorKey: "key_alias", + header: "Key Alias", + size: 150, + cell: (info) => { + const value = info.getValue() as string; + return ( + + + {value ?? "-"} + + + ); + }, + }, + { + id: "team_alias", + accessorKey: "team_alias", + header: "Team Alias", + size: 120, + cell: (info) => { + const value = info.getValue() as string; + return ( + + {value || "-"} + + ); + }, + }, + { + id: "spend", + accessorKey: "spend", + header: "Spend (USD)", + size: 100, + cell: (info) => formatNumberWithCommas(info.getValue() as number, 4), + }, + { + id: "max_budget", + accessorKey: "max_budget", + header: "Budget (USD)", + size: 110, + cell: (info) => { + const maxBudget = info.getValue() as number | null; + if (maxBudget === null) { + return "Unlimited"; + } + return `$${formatNumberWithCommas(maxBudget)}`; + }, + }, + { + id: "user_email", + accessorKey: "user_email", + header: "User Email", + size: 160, + cell: (info) => { + const value = info.getValue() as string; + return ( + + + {value ?? "-"} + + + ); + }, + }, + { + id: "user_id", + accessorKey: "user_id", + header: "User ID", + size: 120, + cell: (info) => { + const userId = info.getValue() as string | null; + return ( + + + {userId || "-"} + + + ); + }, + }, + { + id: "created_at", + accessorKey: "created_at", + header: "Created At", + size: 120, + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "-"; + }, + }, + { + id: "created_by", + accessorKey: "created_by", + header: "Created By", + size: 120, + cell: (info) => { + const value = (info.row.original as any).created_by as string | null | undefined; + return ( + + + {value || "-"} + + + ); + }, + }, + { + id: "deleted_at", + accessorKey: "deleted_at", + header: "Deleted At", + size: 120, + cell: (info) => { + const value = (info.row.original as any).deleted_at as string | null | undefined; + return value ? new Date(value).toLocaleDateString() : "-"; + }, + }, + { + id: "deleted_by", + accessorKey: "deleted_by", + header: "Deleted By", + size: 120, + cell: (info) => { + const value = (info.row.original as any).deleted_by as string | null | undefined; + return ( + + + {value || "-"} + + + ); + }, + }, + ]; + + const table = useReactTable({ + data: keys, + columns, + columnResizeMode: "onChange", + columnResizeDirection: "ltr", + state: { + sorting, + pagination: tablePagination, + }, + onSortingChange: setSorting, + onPaginationChange: (updater) => { + const newPagination = typeof updater === "function" ? updater(tablePagination) : updater; + setTablePagination(newPagination); + onPageChange(newPagination.pageIndex); + }, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + enableSorting: true, + manualSorting: false, + manualPagination: true, + pageCount: Math.ceil(totalCount / pageSize), + }); + + const { pageIndex: currentPageIndex } = table.getState().pagination; + const start = currentPageIndex * pageSize + 1; + const end = Math.min((currentPageIndex + 1) * pageSize, totalCount); + const rangeLabel = `${start} - ${end}`; + + return ( +
+
+
+ {isLoading || isFetching ? ( + Loading... + ) : ( + + Showing {rangeLabel} of {totalCount} results + + )} + +
+ {isLoading || isFetching ? ( + Loading... + ) : ( + + Page {currentPageIndex + 1} of {table.getPageCount()} + + )} + + + + +
+
+
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + { + const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`); + if (resizer) { + (resizer as HTMLElement).style.opacity = "0.5"; + } + }} + onMouseLeave={() => { + const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`); + if (resizer && !header.column.getIsResizing()) { + (resizer as HTMLElement).style.opacity = "0"; + } + }} + onClick={header.column.getToggleSortingHandler()} + > +
+
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: , + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${table.options.columnResizeDirection} ${header.column.getIsResizing() ? "isResizing" : ""}`} + style={{ + position: "absolute", + right: 0, + top: 0, + height: "100%", + width: "5px", + background: header.column.getIsResizing() ? "#3b82f6" : "transparent", + cursor: "col-resize", + userSelect: "none", + touchAction: "none", + opacity: header.column.getIsResizing() ? 1 : 0, + }} + /> +
+ + ))} + + ))} + + + {isLoading || isFetching ? ( + + +
+

🚅 Loading keys...

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

No deleted keys found

+
+
+
+ )} +
+
+
+
+
+
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 920448e8ac1..2fdac26fafa 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -53,7 +53,7 @@ const defaultServerRootPath = "/"; export let serverRootPath = defaultServerRootPath; export let proxyBaseUrl = defaultProxyBaseUrl; if (isLocal != true) { - console.log = function () {}; + console.log = function () { }; } const getWindowLocation = () => { @@ -3270,6 +3270,7 @@ export const keyListCall = async ( sortBy: string | null = null, sortOrder: string | null = null, expand: string | null = null, + status: string | null = null, ) => { /** * Get all available teams on proxy @@ -3319,6 +3320,10 @@ export const keyListCall = async ( queryParams.append("expand", expand); } + if (status) { + queryParams.append("status", status); + } + 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/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 2a94284fca5..a7017886267 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -28,6 +28,7 @@ import AuditLogs from "./audit_logs"; import { getTimeRangeDisplay } from "./logs_utils"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { truncateString } from "@/utils/textUtils"; +import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage"; interface SpendLogsTableProps { accessToken: string | null; @@ -355,7 +356,7 @@ export default function SpendLogsTable({ sessionLogs.data?.data?.map((log) => ({ ...log, onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), - onSessionClick: (sessionId: string) => {}, + onSessionClick: (sessionId: string) => { }, })) || []; // Add this function to handle manual refresh @@ -502,6 +503,7 @@ export default function SpendLogsTable({ Request Logs Audit Logs + Deleted Keys @@ -537,7 +539,7 @@ export default function SpendLogsTable({ data={sessionData} renderSubComponent={RequestViewer} getRowCanExpand={() => true} - // Optionally: add session-specific row expansion state + // Optionally: add session-specific row expansion state /> ) : ( @@ -597,9 +599,8 @@ export default function SpendLogsTable({ {quickSelectOptions.map((option) => (