(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:
Ishaan Jaff 2025-02-13 21:51:54 -08:00 committed by GitHub
parent e33543ae4f
commit 1e7849cfb2
14 changed files with 1134 additions and 905 deletions

View file

@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation";
import { jwtDecode } from "jwt-decode";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { defaultOrg } from "@/components/common_components/default_org";
import { Team } from "@/components/key_team_helpers/key_list";
import Navbar from "@/components/navbar";
import UserDashboard from "@/components/user_dashboard";
import ModelDashboard from "@/components/model_dashboard";
@ -76,7 +77,7 @@ export default function CreateKeyPage() {
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] =
useState(false);
const [userEmail, setUserEmail] = useState<null | string>(null);
const [teams, setTeams] = useState<null | any[]>(null);
const [teams, setTeams] = useState<Team[] | null>(null);
const [keys, setKeys] = useState<null | any[]>(null);
const [currentOrg, setCurrentOrg] = useState<Organization>(defaultOrg);
const [organizations, setOrganizations] = useState<Organization[]>([]);

View file

@ -16,6 +16,7 @@ import {
} from "antd";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Select, SelectItem, Subtitle } from "@tremor/react";
import { Team } from "./key_team_helpers/key_list";
import {
Table,
TableBody,
@ -38,7 +39,7 @@ import { InvitationLink } from "./onboarding_link";
interface AdminPanelProps {
searchParams: any;
accessToken: string | null;
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
showSSOBanner: boolean;
premiumUser: boolean;
}

View 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>
);
}

View file

@ -30,6 +30,7 @@ import {
modelAvailableCall,
getGuardrailsList,
} from "./networking";
import { Team } from "./key_team_helpers/key_list";
import { InfoCircleOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
@ -37,11 +38,12 @@ const { Option } = Select;
interface CreateKeyProps {
userID: string;
team: any | null;
team: Team | null;
userRole: string | null;
accessToken: string;
data: any[] | null;
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
teams: Team[] | null;
}
const getPredefinedTags = (data: any[] | null) => {
@ -68,10 +70,34 @@ const getPredefinedTags = (data: any[] | null) => {
return uniqueTags;
}
export const getTeamModels = (team: Team | null, allAvailableModels: string[]): string[] => {
let tempModelsToPick = [];
if (team) {
if (team.models.length > 0) {
if (team.models.includes("all-proxy-models")) {
// if the team has all-proxy-models show all available models
tempModelsToPick = allAvailableModels;
} else {
// show team models
tempModelsToPick = team.models;
}
} else {
// show all available models if the team has no models set
tempModelsToPick = allAvailableModels;
}
} else {
// no team set, show all available models
tempModelsToPick = allAvailableModels;
}
return unfurlWildcardModelsInList(tempModelsToPick, allAvailableModels);
};
const CreateKey: React.FC<CreateKeyProps> = ({
userID,
team,
teams,
userRole,
accessToken,
data,
@ -86,6 +112,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
const [keyOwner, setKeyOwner] = useState("you");
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState<Team | null>(team);
const handleOk = () => {
setIsModalVisible(false);
@ -197,30 +224,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({
};
useEffect(() => {
let tempModelsToPick = [];
if (team) {
if (team.models.length > 0) {
if (team.models.includes("all-proxy-models")) {
// if the team has all-proxy-models show all available models
tempModelsToPick = userModels;
} else {
// show team models
tempModelsToPick = team.models;
}
} else {
// show all available models if the team has no models set
tempModelsToPick = userModels;
}
} else {
// no team set, show all available models
tempModelsToPick = userModels;
}
tempModelsToPick = unfurlWildcardModelsInList(tempModelsToPick, userModels);
setModelsToPick(tempModelsToPick);
}, [team, userModels]);
const models = getTeamModels(selectedCreateKeyTeam, userModels);
setModelsToPick(models);
form.setFieldValue('models', []);
}, [selectedCreateKeyTeam, userModels]);
return (
<div>
@ -278,18 +285,45 @@ const CreateKey: React.FC<CreateKeyProps> = ({
<TextInput placeholder="" />
</Form.Item>
<Form.Item
label="Team ID"
label="Team"
name="team_id"
hidden={keyOwner !== "another_user"}
initialValue={team ? team["team_id"] : null}
valuePropName="team_id"
initialValue={team ? team.team_id : null}
className="mt-8"
rules={[{ required: true, message: 'Please select a team' }]}
>
<TextInput defaultValue={team ? team["team_id"] : null} onChange={(e) => form.setFieldValue('team_id', e.target.value)}/>
<Select
showSearch
placeholder="Search or select a team"
onChange={(value) => {
form.setFieldValue('team_id', value);
const selectedTeam = teams?.find(team => team.team_id === value);
setSelectedCreateKeyTeam(selectedTeam || null);
}}
filterOption={(input, option) => {
if (!option) return false;
const optionValue = option.children?.toString() || '';
return optionValue.toLowerCase().includes(input.toLowerCase());
}}
optionFilterProp="children"
>
{teams?.map((team) => (
<Select.Option 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>
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label="Models"
label={
<span>
Models{' '}
<Tooltip title="These are the models that your selected team has access to">
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
</Tooltip>
</span>
}
name="models"
rules={[{ required: true, message: "Please select a model" }]}
help="required"
@ -299,15 +333,8 @@ const CreateKey: React.FC<CreateKeyProps> = ({
placeholder="Select models"
style={{ width: "100%" }}
onChange={(values) => {
// Check if "All Team Models" is selected
const isAllTeamModelsSelected =
values.includes("all-team-models");
// If "All Team Models" is selected, deselect all other models
if (isAllTeamModelsSelected) {
const newValues = ["all-team-models"];
// You can call the form's setFieldsValue method to update the value
form.setFieldsValue({ models: newValues });
if (values.includes("all-team-models")) {
form.setFieldsValue({ models: ["all-team-models"] });
}
}}
>

View file

@ -0,0 +1,170 @@
import React, { useState, useEffect } from "react";
import { Form, Input, InputNumber, Select } from "antd";
import { Button, TextInput } from "@tremor/react";
import { KeyResponse } from "./key_team_helpers/key_list";
import { getTeamModels } from "../components/create_key_button";
import { modelAvailableCall } from "./networking";
interface KeyEditViewProps {
keyData: KeyResponse;
onCancel: () => void;
onSubmit: (values: any) => Promise<void>;
teams?: any[] | null;
accessToken: string | null;
userID: string | null;
userRole: string | null;
}
// Add this helper function
const getAvailableModelsForKey = (keyData: KeyResponse, teams: any[] | null): string[] => {
// If no teams data is available, return empty array
console.log("getAvailableModelsForKey:", teams);
if (!teams || !keyData.team_id) {
return [];
}
// Find the team that matches the key's team_id
const keyTeam = teams.find(team => team.team_id === keyData.team_id);
// If team found and has models, return those models
if (keyTeam?.models) {
return keyTeam.models;
}
return [];
};
export function KeyEditView({
keyData,
onCancel,
onSubmit,
teams,
accessToken,
userID,
userRole }: KeyEditViewProps) {
const [form] = Form.useForm();
const [userModels, setUserModels] = useState<string[]>([]);
const team = teams?.find(team => team.team_id === keyData.team_id);
const availableModels = getTeamModels(team, userModels);
useEffect(() => {
const fetchUserModels = async () => {
try {
if (accessToken && userID && userRole) {
const model_available = await modelAvailableCall(
accessToken,
userID,
userRole
);
let available_model_names = model_available["data"].map(
(element: { id: string }) => element.id
);
console.log("available_model_names:", available_model_names);
setUserModels(available_model_names);
}
} catch (error) {
console.error("Error fetching user models:", error);
}
};
fetchUserModels();
}, []);
// Convert API budget duration to form format
const getBudgetDuration = (duration: string | null) => {
if (!duration) return null;
const durationMap: Record<string, string> = {
"24h": "daily",
"7d": "weekly",
"30d": "monthly"
};
return durationMap[duration] || null;
};
// Set initial form values
const initialValues = {
...keyData,
budget_duration: getBudgetDuration(keyData.budget_duration),
metadata: keyData.metadata ? JSON.stringify(keyData.metadata, null, 2) : "",
guardrails: keyData.metadata?.guardrails || []
};
return (
<Form
form={form}
onFinish={onSubmit}
initialValues={initialValues}
layout="vertical"
>
<Form.Item label="Key Alias" name="key_alias">
<TextInput />
</Form.Item>
<Form.Item label="Models" name="models">
<Select
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
{/* Only show All Team Models if team has models */}
{availableModels.length > 0 && (
<Select.Option value="all-team-models">All Team Models</Select.Option>
)}
{/* Show available team models */}
{availableModels.map(model => (
<Select.Option key={model} value={model}>
{model}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<InputNumber step={0.01} precision={2} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Select placeholder="n/a">
<Select.Option value="daily">Daily</Select.Option>
<Select.Option value="weekly">Weekly</Select.Option>
<Select.Option value="monthly">Monthly</Select.Option>
</Select>
</Form.Item>
<Form.Item label="TPM Limit" name="tpm_limit">
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="RPM Limit" name="rpm_limit">
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Guardrails" name="guardrails">
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Select or enter guardrails"
/>
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={10} />
</Form.Item>
{/* Hidden form field for token */}
<Form.Item name="token" hidden>
<Input />
</Form.Item>
<div className="flex justify-end gap-2 mt-6">
<Button variant="light" onClick={onCancel}>
Cancel
</Button>
<Button>
Save Changes
</Button>
</div>
</Form>
);
}

View file

@ -0,0 +1,331 @@
import React, { useState } from "react";
import {
Card,
Text,
Button,
Grid,
Col,
Tab,
TabList,
TabGroup,
TabPanel,
TabPanels,
Title,
Badge,
TextInput,
Select as TremorSelect
} from "@tremor/react";
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
import { keyDeleteCall, keyUpdateCall } from "./networking";
import { KeyResponse } from "./key_team_helpers/key_list";
import { Form, Input, InputNumber, message, Select } from "antd";
import { KeyEditView } from "./key_edit_view";
interface KeyInfoViewProps {
keyId: string;
onClose: () => void;
keyData: KeyResponse | undefined;
accessToken: string | null;
userID: string | null;
userRole: string | null;
teams: any[] | null;
}
export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams }: KeyInfoViewProps) {
const [isEditing, setIsEditing] = useState(false);
const [form] = Form.useForm();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
if (!keyData) {
return (
<div className="p-4">
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
Back to Keys
</Button>
<Text>Key not found</Text>
</div>
);
}
const handleKeyUpdate = async (formValues: Record<string, any>) => {
try {
if (!accessToken) return;
const currentKey = formValues.token;
formValues.key = currentKey;
// Convert metadata back to an object if it exists and is a string
if (formValues.metadata && typeof formValues.metadata === "string") {
try {
const parsedMetadata = JSON.parse(formValues.metadata);
formValues.metadata = {
...parsedMetadata,
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}),
};
} catch (error) {
console.error("Error parsing metadata JSON:", error);
message.error("Invalid metadata JSON");
return;
}
} else {
formValues.metadata = {
...(formValues.metadata || {}),
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}),
};
}
// Convert budget_duration to API format
if (formValues.budget_duration) {
const durationMap: Record<string, string> = {
daily: "24h",
weekly: "7d",
monthly: "30d"
};
formValues.budget_duration = durationMap[formValues.budget_duration];
}
const newKeyValues = await keyUpdateCall(accessToken, formValues);
message.success("Key updated successfully");
setIsEditing(false);
// Refresh key data here if needed
} catch (error) {
message.error("Failed to update key");
console.error("Error updating key:", error);
}
};
const handleDelete = async () => {
try {
if (!accessToken) return;
await keyDeleteCall(accessToken as string, keyData.token);
message.success("Key deleted successfully");
onClose();
} catch (error) {
console.error("Error deleting the key:", error);
message.error("Failed to delete key");
}
};
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button
icon={ArrowLeftIcon}
variant="light"
onClick={onClose}
className="mb-4"
>
Back to Keys
</Button>
<Title>{keyData.key_alias || "API Key"}</Title>
<Text className="text-gray-500 font-mono">{keyData.token}</Text>
</div>
<Button
icon={TrashIcon}
variant="light"
color="red"
onClick={() => setIsDeleteModalOpen(true)}
>
Delete Key
</Button>
</div>
{/* Delete Confirmation Modal */}
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete Key
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this key?
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button
onClick={handleDelete}
color="red"
className="ml-2"
>
Delete
</Button>
<Button onClick={() => setIsDeleteModalOpen(false)}>
Cancel
</Button>
</div>
</div>
</div>
</div>
)}
<TabGroup>
<TabList className="mb-4">
<Tab>Overview</Tab>
<Tab>Settings</Tab>
</TabList>
<TabPanels>
{/* Overview Panel */}
<TabPanel>
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Spend</Text>
<div className="mt-2">
<Title>${Number(keyData.spend).toFixed(4)}</Title>
<Text>of {keyData.max_budget !== null ? `$${keyData.max_budget}` : "Unlimited"}</Text>
</div>
</Card>
<Card>
<Text>Rate Limits</Text>
<div className="mt-2">
<Text>TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"}</Text>
<Text>RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"}</Text>
</div>
</Card>
<Card>
<Text>Models</Text>
<div className="mt-2 flex flex-wrap gap-2">
{keyData.models && keyData.models.length > 0 ? (
keyData.models.map((model, index) => (
<Badge key={index} color="red">
{model}
</Badge>
))
) : (
<Text>No models specified</Text>
)}
</div>
</Card>
</Grid>
</TabPanel>
{/* Settings Panel */}
<TabPanel>
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Key Settings</Title>
{!isEditing && (
<Button variant="light" onClick={() => setIsEditing(true)}>
Edit Settings
</Button>
)}
</div>
{isEditing ? (
<KeyEditView
keyData={keyData}
onCancel={() => setIsEditing(false)}
onSubmit={handleKeyUpdate}
teams={teams}
accessToken={accessToken}
userID={userID}
userRole={userRole}
/>
) : (
<div className="space-y-4">
<div>
<Text className="font-medium">Key ID</Text>
<Text className="font-mono">{keyData.token}</Text>
</div>
<div>
<Text className="font-medium">Key Alias</Text>
<Text>{keyData.key_alias || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Secret Key</Text>
<Text className="font-mono">{keyData.key_name}</Text>
</div>
<div>
<Text className="font-medium">Team ID</Text>
<Text>{keyData.team_id || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Organization</Text>
<Text>{keyData.organization_id || "Not Set"}</Text>
</div>
<div>
<Text className="font-medium">Created</Text>
<Text>{new Date(keyData.created_at).toLocaleString()}</Text>
</div>
<div>
<Text className="font-medium">Expires</Text>
<Text>{keyData.expires ? new Date(keyData.expires).toLocaleString() : "Never"}</Text>
</div>
<div>
<Text className="font-medium">Spend</Text>
<Text>${Number(keyData.spend).toFixed(4)} USD</Text>
</div>
<div>
<Text className="font-medium">Budget</Text>
<Text>{keyData.max_budget !== null ? `$${keyData.max_budget} USD` : "Unlimited"}</Text>
</div>
<div>
<Text className="font-medium">Models</Text>
<div className="flex flex-wrap gap-2 mt-1">
{keyData.models && keyData.models.length > 0 ? (
keyData.models.map((model, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 rounded text-xs"
>
{model}
</span>
))
) : (
<Text>No models specified</Text>
)}
</div>
</div>
<div>
<Text className="font-medium">Rate Limits</Text>
<Text>TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"}</Text>
<Text>RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"}</Text>
</div>
<div>
<Text className="font-medium">Metadata</Text>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{JSON.stringify(keyData.metadata, null, 2)}
</pre>
</div>
</div>
)}
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
}

View file

@ -1,68 +1,76 @@
import { useState, useEffect } from 'react';
import { keyListCall, Organization } from '../networking';
interface Team {
team_id: string;
team_alias: string;
export interface Team {
team_id: string;
team_alias: string;
models: string[];
max_budget: number | null;
budget_duration: string | null;
tpm_limit: number | null;
rpm_limit: number | null;
organization_id: string;
created_at: string;
}
export interface KeyResponse {
token: string;
key_name: string;
key_alias: string;
spend: number;
max_budget: number;
expires: string;
models: string[];
aliases: Record<string, unknown>;
config: Record<string, unknown>;
user_id: string;
team_id: string | null;
max_parallel_requests: number;
metadata: Record<string, unknown>;
tpm_limit: number;
rpm_limit: number;
budget_duration: string;
budget_reset_at: string;
allowed_cache_controls: string[];
permissions: Record<string, unknown>;
model_spend: Record<string, number>;
model_max_budget: Record<string, number>;
soft_budget_cooldown: boolean;
blocked: boolean;
litellm_budget_table: Record<string, unknown>;
org_id: string | null;
created_at: string;
updated_at: string;
team_spend: number;
team_alias: string;
team_tpm_limit: number;
team_rpm_limit: number;
team_max_budget: number;
team_models: string[];
team_blocked: boolean;
soft_budget: number;
team_model_aliases: Record<string, string>;
team_member_spend: number;
team_member?: {
token: string;
key_name: string;
key_alias: string;
spend: number;
max_budget: number;
expires: string;
models: string[];
aliases: Record<string, unknown>;
config: Record<string, unknown>;
user_id: string;
team_id: string | null;
max_parallel_requests: number;
metadata: Record<string, unknown>;
tpm_limit: number;
rpm_limit: number;
budget_duration: string;
budget_reset_at: string;
allowed_cache_controls: string[];
permissions: Record<string, unknown>;
model_spend: Record<string, number>;
model_max_budget: Record<string, number>;
soft_budget_cooldown: boolean;
blocked: boolean;
litellm_budget_table: Record<string, unknown>;
organization_id: string | null;
created_at: string;
updated_at: string;
team_spend: number;
team_alias: string;
team_tpm_limit: number;
team_rpm_limit: number;
team_max_budget: number;
team_models: string[];
team_blocked: boolean;
soft_budget: number;
team_model_aliases: Record<string, string>;
team_member_spend: number;
team_member?: {
user_id: string;
user_email: string;
role: 'admin' | 'user';
};
team_metadata: Record<string, unknown>;
end_user_id: string;
end_user_tpm_limit: number;
end_user_rpm_limit: number;
end_user_max_budget: number;
last_refreshed_at: number;
api_key: string;
user_role: 'proxy_admin' | 'user';
allowed_model_region?: 'eu' | 'us' | string;
parent_otel_span?: string;
rpm_limit_per_model: Record<string, number>;
tpm_limit_per_model: Record<string, number>;
user_tpm_limit: number;
user_rpm_limit: number;
user_email: string;
};
team_metadata: Record<string, unknown>;
end_user_id: string;
end_user_tpm_limit: number;
end_user_rpm_limit: number;
end_user_max_budget: number;
last_refreshed_at: number;
api_key: string;
user_role: 'proxy_admin' | 'user';
allowed_model_region?: 'eu' | 'us' | string;
parent_otel_span?: string;
rpm_limit_per_model: Record<string, number>;
tpm_limit_per_model: Record<string, number>;
user_tpm_limit: number;
user_rpm_limit: number;
user_email: string;
}
interface KeyListResponse {
@ -76,6 +84,7 @@ interface UseKeyListProps {
selectedTeam?: Team;
currentOrg: Organization | null;
accessToken: string;
currentPage?: number;
}
interface PaginationData {
@ -92,7 +101,13 @@ pagination: PaginationData;
refresh: (params?: Record<string, unknown>) => Promise<void>;
}
const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps): UseKeyListReturn => {
const useKeyList = ({
selectedTeam,
currentOrg,
accessToken,
currentPage = 1,
}: UseKeyListProps): UseKeyListReturn => {
const [keyData, setKeyData] = useState<KeyListResponse>({
keys: [],
total_count: 0,
@ -105,20 +120,25 @@ const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps):
const fetchKeys = async (params: Record<string, unknown> = {}): Promise<void> => {
try {
console.log("calling fetchKeys");
if (!currentOrg || !selectedTeam || !accessToken) {
if (!currentOrg || !accessToken) {
console.log("currentOrg", currentOrg);
console.log("selectedTeam", selectedTeam);
console.log("accessToken", accessToken);
return
return;
}
setIsLoading(true);
const data = await keyListCall(accessToken, currentOrg.organization_id, selectedTeam.team_id);
const data = await keyListCall(
accessToken,
currentOrg.organization_id,
selectedTeam?.team_id || "",
params.page as number || 1,
50
);
console.log("data", data);
setKeyData(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error('An error occurred'));
setError(err instanceof Error ? err : new Error("An error occurred"));
} finally {
setIsLoading(false);
}
@ -126,7 +146,8 @@ const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps):
useEffect(() => {
fetchKeys();
}, [selectedTeam, currentOrg]);
console.log("selectedTeam", selectedTeam, "currentOrg", currentOrg, "accessToken", accessToken);
}, [selectedTeam, currentOrg, accessToken]);
return {
keys: keyData.keys,

View file

@ -2079,6 +2079,8 @@ export const keyListCall = async (
accessToken: String,
organizationID: string | null,
teamID: string | null,
page: number,
pageSize: number
) => {
/**
* Get all available teams on proxy
@ -2096,6 +2098,14 @@ export const keyListCall = async (
queryParams.append('organization_id', organizationID.toString());
}
if (page) {
queryParams.append('page', page.toString());
}
if (pageSize) {
queryParams.append('size', pageSize.toString());
}
queryParams.append('return_full_object', 'true');
const queryString = queryParams.toString();

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react";
import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking";
import { Team } from "./key_team_helpers/key_list";
import {
Col,
Grid,
@ -15,10 +16,10 @@ if (isLocal != true) {
console.log = function() {};
}
interface TeamProps {
teams: any[] | null;
teams: Team[] | null;
searchParams: any;
accessToken: string | null;
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
userID: string | null;
userRole: string | null;
premiumUser: boolean;

View file

@ -53,16 +53,17 @@ import {
} from "@tremor/react";
import { CogIcon } from "@heroicons/react/outline";
import AvailableTeamsPanel from "@/components/team/available_teams";
import type { Team } from "./key_team_helpers/key_list";
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
if (isLocal != true) {
console.log = function() {};
}
interface TeamProps {
teams: any[] | null;
teams: Team[] | null;
searchParams: any;
accessToken: string | null;
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
userID: string | null;
userRole: string | null;
currentOrg: Organization | null;

View file

@ -19,6 +19,7 @@ import ViewUserTeam from "./view_user_team";
import DashboardTeam from "./dashboard_default_team";
import Onboarding from "../app/onboarding/page";
import { useSearchParams, useRouter } from "next/navigation";
import { Team } from "./key_team_helpers/key_list";
import { jwtDecode } from "jwt-decode";
import { Typography } from "antd";
const isLocal = process.env.NODE_ENV === "development";
@ -56,11 +57,11 @@ interface UserDashboardProps {
userID: string | null;
userRole: string | null;
userEmail: string | null;
teams: any[] | null;
teams: Team[] | null;
keys: any[] | null;
setUserRole: React.Dispatch<React.SetStateAction<string>>;
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
setOrganizations: React.Dispatch<React.SetStateAction<Organization[]>>;
premiumUser: boolean;
@ -108,9 +109,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
team_alias: "Default Team",
team_id: null,
};
const [selectedTeam, setSelectedTeam] = useState<any | null>(
teams ? teams[0] : defaultTeam
);
const [selectedTeam, setSelectedTeam] = useState<any | null>(null);
// check if window is not undefined
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", function () {
@ -213,16 +212,6 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
}
const teamsArray = [...response["teams"]];
if (teamsArray.length > 0) {
console.log(`response['teams']: ${JSON.stringify(teamsArray)}`);
setSelectedTeam(teamsArray[0]);
} else {
setSelectedTeam(defaultTeam);
}
sessionStorage.setItem(
"userData" + userID,
JSON.stringify(response["keys"])
@ -348,20 +337,16 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
return (
<div className="w-full mx-4">
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
<Col numColSpan={1}>
<ViewUserTeam
<Col numColSpan={1} className="flex flex-col gap-2">
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
userID={userID}
team={selectedTeam as Team | null}
teams={teams as Team[]}
userRole={userRole}
selectedTeam={selectedTeam ? selectedTeam : null}
accessToken={accessToken}
/>
<ViewUserSpend
userID={userID}
userRole={userRole}
userMaxBudget={userSpendData?.max_budget || null}
accessToken={accessToken}
userSpend={teamSpend}
selectedTeam={selectedTeam ? selectedTeam : null}
data={keys}
setData={setKeys}
/>
<ViewKeyTable
@ -369,31 +354,13 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
userRole={userRole}
accessToken={accessToken}
selectedTeam={selectedTeam ? selectedTeam : null}
setSelectedTeam={setSelectedTeam}
data={keys}
setData={setKeys}
premiumUser={premiumUser}
teams={teams}
currentOrg={currentOrg}
/>
<CreateKey
key={selectedTeam ? selectedTeam.team_id : null}
userID={userID}
team={selectedTeam ? selectedTeam : null}
userRole={userRole}
accessToken={accessToken}
data={keys}
setData={setKeys}
/>
<DashboardTeam
teams={teams}
setSelectedTeam={setSelectedTeam}
userRole={userRole}
proxySettings={proxySettings}
setProxySettings={setProxySettings}
userInfo={userSpendData}
accessToken={accessToken}
setKeys={setKeys}
/>
</Col>
</Grid>
</div>

View file

@ -1,5 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import {
keyDeleteCall,
modelAvailableCall,
@ -42,6 +42,8 @@ import {
BarChart,
TextInput,
Textarea,
Select,
SelectItem,
} from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import {
@ -49,8 +51,6 @@ import {
getModelDisplayName,
} from "./key_team_helpers/fetch_available_models_team_key";
import {
Select as Select3,
SelectItem,
MultiSelect,
MultiSelectItem,
} from "@tremor/react";
@ -62,16 +62,15 @@ import {
Select as Select2,
InputNumber,
message,
Select,
Tooltip,
DatePicker,
} from "antd";
import { CopyToClipboard } from "react-copy-to-clipboard";
import TextArea from "antd/es/input/TextArea";
import useKeyList from "./key_team_helpers/key_list";
import { KeyResponse } from "./key_team_helpers/key_list";
const { Option } = Select;
import { AllKeysTable } from "./all_keys_table";
import { Team } from "./key_team_helpers/key_list";
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
@ -100,9 +99,10 @@ interface ViewKeyTableProps {
userRole: string | null;
accessToken: string;
selectedTeam: any | null;
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
data: any[] | null;
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
teams: any[] | null;
teams: Team[] | null;
premiumUser: boolean;
currentOrg: Organization | null;
}
@ -145,6 +145,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
userRole,
accessToken,
selectedTeam,
setSelectedTeam,
data,
setData,
teams,
@ -159,13 +160,43 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
{ day: string; spend: number }[] | null
>(null);
const { keys, isLoading, error, refresh } = useKeyList({
// NEW: Declare filter states for team and key alias.
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "");
const [keyAliasFilter, setKeyAliasFilter] = useState<string>("");
// Keep the team filter in sync with the incoming prop.
useEffect(() => {
setTeamFilter(selectedTeam?.team_id || "");
}, [selectedTeam]);
// Build a memoized filters object for the backend call.
const filters = useMemo(
() => {
const f: { team_id?: string; key_alias?: string } = {};
if (teamFilter) {
f.team_id = teamFilter;
}
if (keyAliasFilter) {
f.key_alias = keyAliasFilter;
}
return f;
},
[teamFilter, keyAliasFilter]
);
// Pass filters into the hook so the API call includes these query parameters.
const { keys, isLoading, error, pagination, refresh } = useKeyList({
selectedTeam,
currentOrg,
accessToken
accessToken,
});
console.log("keys", keys);
const handlePageChange = (newPage: number) => {
refresh({ page: newPage });
};
const [editModalVisible, setEditModalVisible] = useState(false);
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
@ -298,311 +329,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
setKnownTeamIDs(teamIDSet);
}
}, [teams]);
const EditKeyModal: React.FC<EditKeyModalProps> = ({
visible,
onCancel,
token,
onSubmit,
}) => {
const [form] = Form.useForm();
const [keyTeam, setKeyTeam] = useState(selectedTeam);
const [errorModels, setErrorModels] = useState<string[]>([]);
const [errorBudget, setErrorBudget] = useState<boolean>(false);
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
useEffect(() => {
const fetchGuardrails = async () => {
try {
const response = await getGuardrailsList(accessToken);
const guardrailNames = response.guardrails.map(
(g: { guardrail_name: string }) => g.guardrail_name
);
setGuardrailsList(guardrailNames);
} catch (error) {
console.error("Failed to fetch guardrails:", error);
}
};
fetchGuardrails();
}, [accessToken]);
let metadataString = "";
try {
// Create a copy of metadata without guardrails for display
const displayMetadata = { ...token.metadata };
delete displayMetadata.guardrails;
metadataString = JSON.stringify(displayMetadata, null, 2);
} catch (error) {
console.error("Error stringifying metadata:", error);
metadataString = "";
}
// Extract existing guardrails from metadata
let existingGuardrails: string[] = [];
try {
existingGuardrails = token.metadata?.guardrails || [];
} catch (error) {
console.error("Error extracting guardrails:", error);
}
const initialValues =
token ?
{
...token,
budget_duration: token.budget_duration,
metadata: metadataString,
guardrails: existingGuardrails,
}
: { metadata: metadataString, guardrails: [] };
const handleOk = () => {
form
.validateFields()
.then((values) => {
// const updatedValues = {...values, team_id: team.team_id};
// onSubmit(updatedValues);
form.resetFields();
})
.catch((error) => {
console.error("Validation failed:", error);
});
};
return (
<Modal
title="Edit Key"
visible={visible}
width={800}
footer={null}
onOk={handleOk}
onCancel={onCancel}
>
<Form
form={form}
onFinish={handleEditSubmit}
initialValues={initialValues}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<>
<Form.Item name="key_alias" label="Key Alias">
<TextInput />
</Form.Item>
<Form.Item
label="Models"
name="models"
rules={[
{
validator: (rule, value) => {
if (keyTeam.team_alias === "Default Team") {
return Promise.resolve();
}
const errorModels = value.filter(
(model: string) =>
!keyTeam.models.includes(model) &&
model !== "all-team-models" &&
model !== "all-proxy-models" &&
!keyTeam.models.includes("all-proxy-models")
);
console.log(`errorModels: ${errorModels}`);
if (errorModels.length > 0) {
return Promise.reject(
`Some models are not part of the new team's models - ${errorModels} Team models: ${keyTeam.models}`
);
} else {
return Promise.resolve();
}
},
},
]}
>
<Select
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
<Option key="all-team-models" value="all-team-models">
All Team Models
</Option>
{keyTeam.team_alias === "Default Team" ?
userModels
.filter((model) => model !== "all-proxy-models")
.map((model: string) => (
<Option key={model} value={model}>
{getModelDisplayName(model)}
</Option>
))
: keyTeam.models.map((model: string) => (
<Option key={model} value={model}>
{getModelDisplayName(model)}
</Option>
))
}
</Select>
</Form.Item>
<Form.Item
className="mt-8"
label="Max Budget (USD)"
name="max_budget"
help={`Budget cannot exceed team max budget: ${keyTeam?.max_budget !== null && keyTeam?.max_budget !== undefined ? keyTeam?.max_budget : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
keyTeam &&
keyTeam.max_budget !== null &&
value > keyTeam.max_budget
) {
console.log(`keyTeam.max_budget: ${keyTeam.max_budget}`);
throw new Error(
`Budget cannot exceed team max budget: $${keyTeam.max_budget}`
);
}
},
},
]}
>
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="Reset Budget"
name="budget_duration"
help={`Current Reset Budget: ${
token.budget_duration
}, budget will be reset: ${token.budget_reset_at ? new Date(token.budget_reset_at).toLocaleString() : "Never"}`}
>
<Select placeholder="n/a">
<Select.Option value="daily">daily</Select.Option>
<Select.Option value="weekly">weekly</Select.Option>
<Select.Option value="monthly">monthly</Select.Option>
</Select>
</Form.Item>
<Form.Item label="token" name="token" hidden={true}></Form.Item>
<Form.Item
label="Team"
name="team_id"
className="mt-8"
help="the team this key belongs to"
>
<Select3 value={token.team_alias}>
{teams?.map((team_obj, index) => (
<SelectItem
key={index}
value={team_obj.team_id}
onClick={() => setKeyTeam(team_obj)}
>
{team_obj.team_alias}
</SelectItem>
))}
</Select3>
</Form.Item>
<Form.Item
className="mt-8"
label="TPM Limit (tokens per minute)"
name="tpm_limit"
help={`tpm_limit cannot exceed team tpm_limit ${keyTeam?.tpm_limit !== null && keyTeam?.tpm_limit !== undefined ? keyTeam?.tpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
keyTeam &&
keyTeam.tpm_limit !== null &&
value > keyTeam.tpm_limit
) {
console.log(`keyTeam.tpm_limit: ${keyTeam.tpm_limit}`);
throw new Error(
`tpm_limit cannot exceed team max tpm_limit: $${keyTeam.tpm_limit}`
);
}
},
},
]}
>
<InputNumber step={1} precision={1} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="RPM Limit (requests per minute)"
name="rpm_limit"
help={`rpm_limit cannot exceed team max rpm_limit: ${keyTeam?.rpm_limit !== null && keyTeam?.rpm_limit !== undefined ? keyTeam?.rpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
keyTeam &&
keyTeam.rpm_limit !== null &&
value > keyTeam.rpm_limit
) {
console.log(`keyTeam.rpm_limit: ${keyTeam.rpm_limit}`);
throw new Error(
`rpm_limit cannot exceed team max rpm_limit: $${keyTeam.rpm_limit}`
);
}
},
},
]}
>
<InputNumber step={1} precision={1} width={200} />
</Form.Item>
<Form.Item
label={
<span>
Guardrails{" "}
<Tooltip title="Setup your first guardrail">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="guardrails"
className="mt-8"
help="Select existing guardrails or enter new ones"
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Select or enter guardrails"
options={guardrailsList.map((name) => ({
value: name,
label: name,
}))}
/>
</Form.Item>
<Form.Item
label="Metadata (ensure this is valid JSON)"
name="metadata"
>
<TextArea
rows={10}
onChange={(e) => {
form.setFieldsValue({ metadata: e.target.value });
}}
/>
</Form.Item>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Edit Key</Button2>
</div>
</Form>
</Modal>
);
};
const ModelLimitModal: React.FC<ModelLimitModalProps> = ({
visible,
@ -753,7 +480,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
{newModelRow !== null && (
<TableRow>
<TableCell>
<Select
<Select2
style={{ width: 200 }}
placeholder="Select a model"
onChange={handleModelSelect}
@ -762,11 +489,11 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
{availableModels
.filter((m) => !modelLimits.hasOwnProperty(m))
.map((m) => (
<Option key={m} value={m}>
<Select2.Option key={m} value={m}>
{m}
</Option>
</Select2.Option>
))}
</Select>
</Select2>
</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
@ -1013,377 +740,28 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
}
};
return (
<div>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4 mt-2">
<Table className="mt-5 max-h-[300px] min-h-[300px]">
<TableHead>
<TableRow>
<TableHeaderCell>Key Alias</TableHeaderCell>
<TableHeaderCell>Secret Key</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Expires</TableHeaderCell>
<TableHeaderCell>Spend (USD)</TableHeaderCell>
<TableHeaderCell>Budget (USD)</TableHeaderCell>
<TableHeaderCell>Budget Reset</TableHeaderCell>
<TableHeaderCell>Models</TableHeaderCell>
<TableHeaderCell>Rate Limits</TableHeaderCell>
<TableHeaderCell>Rate Limits per model</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{keys &&
keys.map((item) => {
console.log(item);
// skip item if item.team_id == "litellm-dashboard"
if (item.team_id === "litellm-dashboard") {
return null;
}
if (selectedTeam) {
/**
* if selected team id is null -> show the keys with no team id or team id's that don't exist in db
*/
if (
selectedTeam.team_id == null &&
item.team_id !== null &&
!knownTeamIDs.has(item.team_id)
) {
// do nothing -> returns a row with this key
} else if (item.team_id != selectedTeam.team_id) {
return null;
}
console.log(`item team id: ${item.team_id}, is returned`);
}
return (
<TableRow key={item.token}>
<TableCell
style={{
maxWidth: "2px",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
{item.key_alias != null ?
<Text>{item.key_alias}</Text>
: <Text>Not Set</Text>}
</TableCell>
<TableCell>
<Text>{item.key_name}</Text>
</TableCell>
<TableCell>
{item.created_at != null ?
<div>
<p style={{ fontSize: "0.70rem" }}>
{new Date(item.created_at).toLocaleDateString()}
</p>
</div>
: <p style={{ fontSize: "0.70rem" }}>Not available</p>}
</TableCell>
<TableCell>
{item.expires != null ?
<div>
<p style={{ fontSize: "0.70rem" }}>
{new Date(item.expires).toLocaleDateString()}
</p>
</div>
: <p style={{ fontSize: "0.70rem" }}>Never</p>}
</TableCell>
<TableCell>
<Text>
{(() => {
try {
return item.spend.toFixed(4);
} catch (error) {
return item.spend;
}
})()}
</Text>
</TableCell>
<TableCell>
{item.max_budget != null ?
<Text>{item.max_budget}</Text>
: <Text>Unlimited</Text>}
</TableCell>
<TableCell>
{item.budget_reset_at != null ?
<div>
<p style={{ fontSize: "0.70rem" }}>
{new Date(item.budget_reset_at).toLocaleString()}
</p>
</div>
: <p style={{ fontSize: "0.70rem" }}>Never</p>}
</TableCell>
{/* <TableCell style={{ maxWidth: '2px' }}>
<ViewKeySpendReport
token={item.token}
accessToken={accessToken}
keySpend={item.spend}
keyBudget={item.max_budget}
keyName={item.key_name}
/>
</TableCell> */}
{/* <TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}>
<Text>{item.team_alias && item.team_alias != "None" ? item.team_alias : item.team_id}</Text>
</TableCell> */}
{/* <TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}>
<Text>{JSON.stringify(item.metadata).slice(0, 400)}</Text>
</TableCell> */}
<TableCell>
{Array.isArray(item.models) ?
<div
style={{ display: "flex", flexDirection: "column" }}
>
{item.models.length === 0 ?
<>
{
(
selectedTeam &&
selectedTeam.models &&
selectedTeam.models.length > 0
) ?
selectedTeam.models.map(
(model: string, index: number) =>
model === "all-proxy-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
: model === "all-team-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Team Models</Text>
</Badge>
: <Badge
key={index}
size={"xs"}
className="mb-1"
color="blue"
>
<Text>
{model.length > 30 ?
`${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
// If selected team is None or selected team's models are empty, show all models
: <Badge
size={"xs"}
className="mb-1"
color="blue"
>
<Text>all-proxy-models</Text>
</Badge>
}
</>
: item.models.map((model: string, index: number) =>
model === "all-proxy-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
: model === "all-team-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Team Models</Text>
</Badge>
: <Badge
key={index}
size={"xs"}
className="mb-1"
color="blue"
>
<Text>
{model.length > 30 ?
`${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
}
</div>
: null}
</TableCell>
<TableCell>
<Text>
TPM: {item.tpm_limit ? item.tpm_limit : "Unlimited"}{" "}
<br></br> RPM:{" "}
{item.rpm_limit ? item.rpm_limit : "Unlimited"}
</Text>
</TableCell>
<TableCell>
<Button
size="xs"
onClick={() => handleModelLimitClick(item)}
>
Edit Limits
</Button>
</TableCell>
<TableCell>
<Icon
onClick={() => {
setSelectedToken(item);
setInfoDialogVisible(true);
}}
icon={InformationCircleIcon}
size="sm"
/>
<Modal
open={infoDialogVisible}
onCancel={() => {
setInfoDialogVisible(false);
setSelectedToken(null);
}}
footer={null}
width={800}
>
{selectedToken && (
<>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-8">
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Spend
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{(() => {
try {
return selectedToken.spend.toFixed(4);
} catch (error) {
return selectedToken.spend;
}
})()}
</p>
</div>
</Card>
<Card key={item.key_name}>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Budget
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{selectedToken.max_budget != null ?
<>
{selectedToken.max_budget}
{selectedToken.budget_duration && (
<>
<br />
Budget will be reset at{" "}
{selectedToken.budget_reset_at ?
new Date(
selectedToken.budget_reset_at
).toLocaleString()
: "Never"}
</>
)}
</>
: <>Unlimited</>}
</p>
</div>
</Card>
<Card key={item.key_name}>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Expires
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-default font-small text-tremor-content-strong dark:text-dark-tremor-content-strong">
{selectedToken.expires != null ?
<>
{new Date(
selectedToken.expires
).toLocaleString(undefined, {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
})}
</>
: <>Never</>}
</p>
</div>
</Card>
</div>
<Card className="my-4">
<Title>Token Name</Title>
<Text className="my-1">
{selectedToken.key_alias ?
selectedToken.key_alias
: selectedToken.key_name}
</Text>
<Title>Token ID</Title>
<Text className="my-1 text-[12px]">
{selectedToken.token}
</Text>
<Title>User ID</Title>
<Text className="my-1 text-[12px]">
{selectedToken.user_id}
</Text>
<Title>Metadata</Title>
<Text className="my-1">
<pre>
{JSON.stringify(selectedToken.metadata)}{" "}
</pre>
</Text>
</Card>
<Button
className="mx-auto flex items-center"
onClick={() => {
setInfoDialogVisible(false);
setSelectedToken(null);
}}
>
Close
</Button>
</>
)}
</Modal>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => handleEditClick(item)}
/>
<Icon
onClick={() => handleRegenerateClick(item)}
icon={RefreshIcon}
size="sm"
/>
<Icon
onClick={() => handleDelete(item)}
icon={TrashIcon}
size="sm"
/>
</TableCell>
</TableRow>
// New filter UI rendered above the table.
// For the team filter we use the teams prop, and for key alias we compute unique aliases from the keys.
const uniqueKeyAliases = Array.from(
new Set(keys.map((k) => (k.key_alias ? k.key_alias : "Not Set")))
);
})}
</TableBody>
</Table>
return (
<div>
<AllKeysTable
keys={keys}
isLoading={isLoading}
pagination={pagination}
onPageChange={handlePageChange}
pageSize={50}
teams={teams}
selectedTeam={selectedTeam}
setSelectedTeam={setSelectedTeam}
accessToken={accessToken}
userID={userID}
userRole={userRole}
/>
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
@ -1428,26 +806,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
</div>
</div>
)}
</Card>
{selectedToken && (
<EditKeyModal
visible={editModalVisible}
onCancel={handleEditCancel}
token={selectedToken}
onSubmit={handleEditSubmit}
/>
)}
{selectedToken && (
<ModelLimitModal
visible={modelLimitModalVisible}
onCancel={() => setModelLimitModalVisible(false)}
token={selectedToken}
onSubmit={handleModelLimitSubmit}
accessToken={accessToken}
/>
)}
{/* Regenerate Key Form Modal */}
<Modal

View file

@ -67,6 +67,13 @@ export const columns: ColumnDef<LogEntry>[] = [
</Tooltip>
),
},
{
header: "Cost",
accessorKey: "spend",
cell: (info: any) => (
<span>${Number(info.getValue() || 0).toFixed(6)}</span>
),
},
{
header: "Country",
accessorKey: "requester_ip_address",
@ -149,13 +156,7 @@ export const columns: ColumnDef<LogEntry>[] = [
accessorKey: "end_user",
cell: (info: any) => <span>{String(info.getValue() || "-")}</span>,
},
{
header: "Cost",
accessorKey: "spend",
cell: (info: any) => (
<span>${Number(info.getValue() || 0).toFixed(6)}</span>
),
},
{
header: "Tags",
accessorKey: "request_tags",

View file

@ -41,7 +41,7 @@ export function DataTable<TData, TValue>({
});
return (
<div className="rounded-lg custom-border table-wrapper">
<div className="rounded-lg custom-border">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (