mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 19:54:13 +00:00
(UI) - Refactor View Key Table (#8526)
* new key table * clean up * fix view all team keys * fixed create key button * show all keys * ui select team flow * pagination on keys * fix aligning of team and pagination * show key hash * allow clicking into key * click into a key * add current status * fix key alias edit * delete key * fix(create_key_button.tsx): allow user to select team when creating key * working edit key * feat(create_key_button.tsx): switch available models based on selected team enables user to create a key for a specific team * fix(create_key_button.tsx): improve type safety of component * fix(create_key_button.tsx): style cleanup * pass team all the way thru * refactor getTeamModels * fix(columns.tsx): make cost easier to see * ui fix edit key ui * cleanup * fix linting error * fix filter * fix linting * ui fix all keys * fix linting * fix linting * fix org id * fix linting * fix linting * fix linting * fix linting * fix linting * fix linting --------- Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>
This commit is contained in:
parent
e33543ae4f
commit
1e7849cfb2
14 changed files with 1134 additions and 905 deletions
340
ui/litellm-dashboard/src/components/all_keys_table.tsx
Normal file
340
ui/litellm-dashboard/src/components/all_keys_table.tsx
Normal file
|
@ -0,0 +1,340 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
import { DataTable } from "./view_logs/table";
|
||||
import { Select, SelectItem } from "@tremor/react"
|
||||
import { Button } from "@tremor/react"
|
||||
import KeyInfoView from "./key_info_view";
|
||||
import { Tooltip } from "antd";
|
||||
import { Team, KeyResponse } from "./key_team_helpers/key_list";
|
||||
|
||||
interface AllKeysTableProps {
|
||||
keys: KeyResponse[];
|
||||
isLoading?: boolean;
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
};
|
||||
onPageChange: (page: number) => void;
|
||||
pageSize?: number;
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
}
|
||||
|
||||
// Define columns similar to our logs table
|
||||
|
||||
function KeyViewer({ row }: { row: Row<KeyResponse> }) {
|
||||
return (
|
||||
<div className="p-4 bg-gray-50">
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-lg font-medium">Key Details</h3>
|
||||
<div className="mt-2 space-y-1">
|
||||
<p>
|
||||
<strong>Key Alias:</strong> {row.original.key_alias || "Not Set"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Secret Key:</strong> {row.original.key_name}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Created:</strong>{" "}
|
||||
{new Date(row.original.created_at).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Expires:</strong>{" "}
|
||||
{row.original.expires
|
||||
? new Date(row.original.expires).toLocaleString()
|
||||
: "Never"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Spend:</strong> {Number(row.original.spend).toFixed(4)}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Budget:</strong>{" "}
|
||||
{row.original.max_budget !== null
|
||||
? row.original.max_budget
|
||||
: "Unlimited"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Budget Reset:</strong>{" "}
|
||||
{row.original.budget_reset_at
|
||||
? new Date(row.original.budget_reset_at).toLocaleString()
|
||||
: "Never"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Models:</strong>{" "}
|
||||
{row.original.models && row.original.models.length > 0
|
||||
? row.original.models.join(", ")
|
||||
: "-"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Rate Limits:</strong> TPM:{" "}
|
||||
{row.original.tpm_limit !== null
|
||||
? row.original.tpm_limit
|
||||
: "Unlimited"}
|
||||
, RPM:{" "}
|
||||
{row.original.rpm_limit !== null
|
||||
? row.original.rpm_limit
|
||||
: "Unlimited"}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Metadata:</strong>
|
||||
</p>
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto">
|
||||
{JSON.stringify(row.original.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TeamFilter = ({
|
||||
teams,
|
||||
selectedTeam,
|
||||
setSelectedTeam
|
||||
}: {
|
||||
teams: Team[] | null;
|
||||
selectedTeam: Team | null;
|
||||
setSelectedTeam: (team: Team | null) => void;
|
||||
}) => {
|
||||
const handleTeamChange = (value: string) => {
|
||||
const team = teams?.find(t => t.team_id === value);
|
||||
setSelectedTeam(team || null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600">Where Team is</span>
|
||||
<Select
|
||||
value={selectedTeam?.team_id || ""}
|
||||
onValueChange={handleTeamChange}
|
||||
placeholder="Team ID"
|
||||
className="w-[400px]"
|
||||
>
|
||||
<SelectItem value="team_id">Team ID</SelectItem>
|
||||
{teams?.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
<span className="font-medium">{team.team_alias}</span>{" "}
|
||||
<span className="text-gray-500">({team.team_id})</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* AllKeysTable – a new table for keys that mimics the table styling used in view_logs.
|
||||
* The team selector and filtering have been removed so that all keys are shown.
|
||||
*/
|
||||
export function AllKeysTable({
|
||||
keys,
|
||||
isLoading = false,
|
||||
pagination,
|
||||
onPageChange,
|
||||
pageSize = 50,
|
||||
teams,
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
accessToken,
|
||||
userID,
|
||||
userRole
|
||||
}: AllKeysTableProps) {
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
||||
|
||||
const columns: ColumnDef<KeyResponse>[] = [
|
||||
{
|
||||
id: "expander",
|
||||
header: () => null,
|
||||
cell: ({ row }) =>
|
||||
row.getCanExpand() ? (
|
||||
<button
|
||||
onClick={row.getToggleExpandedHandler()}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{row.getIsExpanded() ? "▼" : "▶"}
|
||||
</button>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
header: "Key ID",
|
||||
accessorKey: "token",
|
||||
cell: (info) => (
|
||||
<div className="overflow-hidden">
|
||||
<Tooltip title={info.getValue() as string}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => setSelectedKeyId(info.getValue() as string)}
|
||||
>
|
||||
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Organization",
|
||||
accessorKey: "organization_id",
|
||||
cell: (info) => info.getValue() ? info.renderValue() : "Not Set",
|
||||
},
|
||||
{
|
||||
header: "Team ID",
|
||||
accessorKey: "team_id",
|
||||
cell: (info) => info.getValue() ? info.renderValue() : "Not Set",
|
||||
},
|
||||
{
|
||||
header: "Key Alias",
|
||||
accessorKey: "key_alias",
|
||||
cell: (info) => info.getValue() ? info.renderValue() : "Not Set",
|
||||
},
|
||||
{
|
||||
header: "Secret Key",
|
||||
accessorKey: "key_name",
|
||||
cell: (info) => <span className="font-mono text-xs">{info.getValue() as string}</span>,
|
||||
},
|
||||
{
|
||||
header: "Created",
|
||||
accessorKey: "created_at",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "-";
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Expires",
|
||||
accessorKey: "expires",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Spend (USD)",
|
||||
accessorKey: "spend",
|
||||
cell: (info) => Number(info.getValue()).toFixed(4),
|
||||
},
|
||||
{
|
||||
header: "Budget (USD)",
|
||||
accessorKey: "max_budget",
|
||||
cell: (info) =>
|
||||
info.getValue() !== null && info.getValue() !== undefined
|
||||
? info.getValue()
|
||||
: "Unlimited",
|
||||
},
|
||||
{
|
||||
header: "Budget Reset",
|
||||
accessorKey: "budget_reset_at",
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleString() : "Never";
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Models",
|
||||
accessorKey: "models",
|
||||
cell: (info) => {
|
||||
const models = info.getValue() as string[];
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models && models.length > 0 ? (
|
||||
models.map((model, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-2 py-1 bg-blue-100 rounded text-xs"
|
||||
>
|
||||
{model}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Rate Limits",
|
||||
cell: ({ row }) => {
|
||||
const key = row.original;
|
||||
return (
|
||||
<div>
|
||||
<div>TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}</div>
|
||||
<div>RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{selectedKeyId ? (
|
||||
<KeyInfoView
|
||||
keyId={selectedKeyId}
|
||||
onClose={() => setSelectedKeyId(null)}
|
||||
keyData={keys.find(k => k.token === selectedKeyId)}
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
teams={teams}
|
||||
/>
|
||||
) : (
|
||||
<div className="border-b py-4">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<TeamFilter
|
||||
teams={teams}
|
||||
selectedTeam={selectedTeam}
|
||||
setSelectedTeam={setSelectedTeam}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing {isLoading ? "..." : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`} of {isLoading ? "..." : pagination.totalCount} results
|
||||
</span>
|
||||
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage - 1)}
|
||||
disabled={isLoading || pagination.currentPage === 1}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage + 1)}
|
||||
disabled={isLoading || pagination.currentPage === pagination.totalPages}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={columns.filter(col => col.id !== 'expander')}
|
||||
data={keys}
|
||||
isLoading={isLoading}
|
||||
getRowCanExpand={() => false}
|
||||
renderSubComponent={() => <></>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue