- {Array.isArray(models) ? (
-
- {models.length === 0 ? (
-
- All Proxy Models
-
- ) : (
- <>
-
- {models.length > 3 && (
-
- {
- setExpandedAccordions((prev) => ({
- ...prev,
- [info.row.id]: !prev[info.row.id],
- }));
- }}
- />
-
- )}
-
- {models.slice(0, 3).map((model, index) =>
- model === "all-proxy-models" ? (
-
- All Proxy Models
-
- ) : (
-
-
- {model.length > 30
- ? `${getModelDisplayName(model).slice(0, 30)}...`
- : getModelDisplayName(model)}
-
-
- ),
- )}
- {models.length > 3 && !expandedAccordions[info.row.id] && (
-
-
- +{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"}
-
-
- )}
- {expandedAccordions[info.row.id] && (
-
- {models.slice(3).map((model, index) =>
- model === "all-proxy-models" ? (
-
- All Proxy Models
-
- ) : (
-
-
- {model.length > 30
- ? `${getModelDisplayName(model).slice(0, 30)}...`
- : getModelDisplayName(model)}
-
-
- ),
- )}
-
- )}
-
-
- >
+ {
+ id: "last_active",
+ accessorKey: "last_active",
+ header: () => (
+
+ Last Active
+
+
+
+
+
+
+ This is a new field and is not backfilled. Only new key usage
+ will update this value.
+
+
+
+
+ ),
+ size: 130,
+ enableSorting: false,
+ cell: (info) => {
+ const value = info.getValue();
+ if (!value) return "Unknown";
+ const date = new Date(value as string);
+ return (
+
+
+
+ {date.toLocaleDateString()}
+
+
+ {date.toLocaleString(undefined, {
+ dateStyle: "medium",
+ timeStyle: "long",
+ })}
+
+
+
+ );
+ },
+ },
+ {
+ id: "expires",
+ accessorKey: "expires",
+ header: "Expires",
+ size: 120,
+ enableSorting: false,
+ cell: (info) => {
+ const value = info.getValue();
+ return value ? new Date(value as string).toLocaleDateString() : "Never";
+ },
+ },
+ {
+ id: "spend",
+ accessorKey: "spend",
+ header: "Spend (USD)",
+ size: 100,
+ enableSorting: true,
+ cell: (info) => formatNumberWithCommas(info.getValue() as number, 4),
+ },
+ {
+ id: "max_budget",
+ accessorKey: "max_budget",
+ header: "Budget (USD)",
+ size: 110,
+ enableSorting: true,
+ cell: (info) => {
+ const maxBudget = info.getValue() as number | null;
+ if (maxBudget === null) return "Unlimited";
+ return `$${formatNumberWithCommas(maxBudget)}`;
+ },
+ },
+ {
+ id: "budget_reset_at",
+ accessorKey: "budget_reset_at",
+ header: "Budget Reset",
+ size: 130,
+ enableSorting: false,
+ cell: (info) => {
+ const value = info.getValue();
+ return value ? new Date(value as string).toLocaleString() : "Never";
+ },
+ },
+ {
+ id: "models",
+ accessorKey: "models",
+ header: "Models",
+ size: 200,
+ enableSorting: false,
+ cell: (info) => {
+ const models = info.getValue() as string[];
+ if (!Array.isArray(models)) return null;
+ if (models.length === 0) {
+ return (
+
+ All Proxy Models
+
+ );
+ }
+ return (
+
+
+ {models.length > 3 && (
+
)}
+
+ {models.slice(0, 3).map((model, index) =>
+ model === "all-proxy-models" ? (
+
+ All Proxy Models
+
+ ) : (
+
+ {model.length > 30
+ ? `${getModelDisplayName(model).slice(0, 30)}...`
+ : getModelDisplayName(model)}
+
+ ),
+ )}
+ {models.length > 3 && !expandedAccordions[info.row.id] && (
+
+ +{models.length - 3}{" "}
+ {models.length - 3 === 1 ? "more model" : "more models"}
+
+ )}
+ {expandedAccordions[info.row.id] && (
+
+ {models.slice(3).map((model, index) =>
+ model === "all-proxy-models" ? (
+
+ All Proxy Models
+
+ ) : (
+
+ {model.length > 30
+ ? `${getModelDisplayName(model).slice(0, 30)}...`
+ : getModelDisplayName(model)}
+
+ ),
+ )}
+
+ )}
+
- ) : null}
-
- );
+
+ );
+ },
},
- },
- {
- id: "rate_limits",
- header: "Rate Limits",
- size: 140,
- enableSorting: false,
- cell: ({ row }) => {
- const key = row.original;
- return (
-
-
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
-
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
-
- );
+ {
+ id: "rate_limits",
+ header: "Rate Limits",
+ size: 140,
+ enableSorting: false,
+ cell: ({ row }) => {
+ const key = row.original;
+ return (
+
+
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
+
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
+
+ );
+ },
},
- },
- ], [teams, resolvedOrganizations]);
+ ],
+ [teams, resolvedOrganizations, expandedAccordions],
+ );
const filterOptions: FilterOption[] = [
{
@@ -568,13 +630,14 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
isSearchable: true,
searchFn: async (searchText: string) => {
if (!allTeams || allTeams.length === 0) return [];
-
const filteredTeams = allTeams.filter(
(team) =>
team.team_id.toLowerCase().includes(searchText.toLowerCase()) ||
- (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())),
+ (team.team_alias &&
+ team.team_alias
+ .toLowerCase()
+ .includes(searchText.toLowerCase())),
);
-
return filteredTeams.map((team) => ({
label: `${team.team_alias || team.team_id} (${team.team_id})`,
value: team.team_id,
@@ -587,13 +650,17 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
isSearchable: true,
searchFn: async (searchText: string) => {
if (!allOrganizations || allOrganizations.length === 0) return [];
-
const filteredOrgs = allOrganizations.filter(
- (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false,
+ (org) =>
+ org.organization_id
+ ?.toLowerCase()
+ .includes(searchText.toLowerCase()) ?? false,
);
-
return filteredOrgs
- .filter((org) => org.organization_id !== null && org.organization_id !== undefined)
+ .filter(
+ (org) =>
+ org.organization_id !== null && org.organization_id !== undefined,
+ )
.map((org) => ({
label: `${org.organization_id || "Unknown"} (${org.organization_id})`,
value: org.organization_id as string,
@@ -605,16 +672,8 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
label: "Key Alias",
customComponent: PaginatedKeyAliasSelect,
},
- {
- name: "User ID",
- label: "User ID",
- isSearchable: false,
- },
- {
- name: "Key Hash",
- label: "Key Hash",
- isSearchable: false,
- },
+ { name: "User ID", label: "User ID", isSearchable: false },
+ { name: "Key Hash", label: "Key Hash", isSearchable: false },
];
const table = useReactTable({
@@ -622,28 +681,22 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
columns: columns.filter((col) => col.id !== "expander"),
columnResizeMode: "onChange",
columnResizeDirection: "ltr",
- state: {
- sorting,
- pagination: tablePagination,
- },
+ state: { sorting, pagination: tablePagination },
onSortingChange: (updaterOrValue) => {
- const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue;
+ const newSorting =
+ typeof updaterOrValue === "function"
+ ? updaterOrValue(sorting)
+ : updaterOrValue;
setSorting(newSorting);
if (newSorting && newSorting.length > 0) {
const sortState = newSorting[0];
- const sortBy = sortState.id;
- const sortOrder = sortState.desc ? "desc" : "asc";
- // Update filters state without triggering debouncedSearch
- // The useKeys hook will automatically refetch with the new sort parameters
+ const sortByVal = sortState.id;
+ const sortOrderVal = sortState.desc ? "desc" : "asc";
handleFilterChange(
- {
- ...filters,
- "Sort By": sortBy,
- "Sort Order": sortOrder,
- },
- true, // skipDebounce - let useKeys handle the API call with correct page size
+ { ...filters, "Sort By": sortByVal, "Sort Order": sortOrderVal },
+ true,
);
- onSortChange?.(sortBy, sortOrder);
+ onSortChange?.(sortByVal, sortOrderVal as "asc" | "desc");
}
},
onPaginationChange: setTablePagination,
@@ -656,14 +709,10 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
pageCount: Math.ceil(totalCount / tablePagination.pageSize),
});
- // Update local sorting state when currentSort prop changes
React.useEffect(() => {
if (currentSort) {
setSorting([
- {
- id: currentSort.sortBy,
- desc: currentSort.sortOrder === "desc",
- },
+ { id: currentSort.sortBy, desc: currentSort.sortOrder === "desc" },
]);
}
}, [currentSort]);
@@ -672,6 +721,7 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
const start = pageIndex * pageSize + 1;
const end = Math.min((pageIndex + 1) * pageSize, totalCount);
const rangeLabel = `${start} - ${end}`;
+
return (
{selectedKey ? (
@@ -696,122 +746,158 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
{isLoading ? (
-
+
) : (
-
+
Showing {rangeLabel} of {totalCount} results
)}
- }
+
{isLoading ? (
-
+
) : (
-
+
Page {pageIndex + 1} of {table.getPageCount()}
)}
{isLoading ? (
-
+
) : (
-
+
)}
{isLoading ? (
-
+
) : (
-
+
)}
-
-
+
+
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => (
- {
- const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`);
+ 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()) {
+ const resizer = document.querySelector(
+ `[data-header-id="${header.id}"] .resizer`,
+ );
+ if (
+ resizer &&
+ !header.column.getIsResizing()
+ ) {
(resizer as HTMLElement).style.opacity = "0";
}
}}
- onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
+ onClick={
+ header.column.getCanSort()
+ ? header.column.getToggleSortingHandler()
+ : undefined
+ }
>
{header.isPlaceholder
? null
- : flexRender(header.column.columnDef.header, header.getContext())}
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
- {header.id !== "actions" && header.column.getCanSort() && (
-
- {header.column.getIsSorted() ? (
- {
- asc: ,
- desc: ,
- }[header.column.getIsSorted() as string]
- ) : (
-
- )}
-
- )}
+ {header.id !== "actions" &&
+ header.column.getCanSort() && (
+
+ {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" : ""}`}
+ 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",
+ background: header.column.getIsResizing()
+ ? "hsl(var(--primary))"
+ : "transparent",
cursor: "col-resize",
userSelect: "none",
touchAction: "none",
@@ -819,16 +905,19 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
}}
/>
-
+
))}
))}
-
+
{isLoading ? (
-
-
+
+
@@ -845,17 +934,29 @@ export function VirtualKeysTable({ teams, organizations, onSortChange, currentSo
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
- className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === "models" && Array.isArray(cell.getValue()) && (cell.getValue() as string[]).length > 3 ? "px-0" : ""}`}
+ className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
+ cell.column.id === "models" &&
+ Array.isArray(cell.getValue()) &&
+ (cell.getValue() as string[]).length > 3
+ ? "px-0"
+ : ""
+ }`}
>
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
))}
))
) : (
-
-
+
+
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx
index 45687461510..8ec8abfa6b3 100644
--- a/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/BudgetWindowsEditor.tsx
@@ -1,5 +1,14 @@
-import { Button, InputNumber, Select } from "antd";
import React from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { X } from "lucide-react";
export interface BudgetWindowEntry {
budget_duration: string;
@@ -7,10 +16,10 @@ export interface BudgetWindowEntry {
}
export const BUDGET_WINDOW_OPTIONS = [
- { value: "1h", label: "Hourly", resetHint: "Resets every hour" },
- { value: "24h", label: "Daily", resetHint: "Resets daily at midnight UTC" },
- { value: "7d", label: "Weekly", resetHint: "Resets every Sunday at midnight UTC" },
- { value: "30d", label: "Monthly", resetHint: "Resets on the 1st of every month at midnight UTC" },
+ { value: "1h", label: "Hourly", resetHint: "Resets every hour" },
+ { value: "24h", label: "Daily", resetHint: "Resets daily at midnight UTC" },
+ { value: "7d", label: "Weekly", resetHint: "Resets every Sunday at midnight UTC" },
+ { value: "30d", label: "Monthly", resetHint: "Resets on the 1st of every month at midnight UTC" },
];
interface BudgetWindowsEditorProps {
@@ -27,7 +36,11 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
onChange(value.filter((_, i) => i !== idx));
};
- const updateWindow = (idx: number, field: keyof BudgetWindowEntry, fieldValue: string | number | null) => {
+ const updateWindow = (
+ idx: number,
+ field: keyof BudgetWindowEntry,
+ fieldValue: string | number | null,
+ ) => {
const updated = value.map((w, i) => (i === idx ? { ...w, [field]: fieldValue } : w));
onChange(updated);
};
@@ -35,38 +48,57 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
return (
{value.map((window, idx) => {
- const hint = BUDGET_WINDOW_OPTIONS.find((o) => o.value === window.budget_duration)?.resetHint;
+ const hint = BUDGET_WINDOW_OPTIONS.find(
+ (o) => o.value === window.budget_duration,
+ )?.resetHint;
return (
-
-
+
+
+
+
+ $
+
+ {
+ const v = e.target.value;
+ updateWindow(idx, "max_budget", v === "" ? null : Number(v));
+ }}
+ placeholder="Max spend"
+ className="pl-6"
+ />
+
+
{hint && (
-
+
↻ {hint}
)}
@@ -74,8 +106,13 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
);
})}