mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +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
|
@ -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[]>([]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
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>
|
||||
);
|
||||
}
|
|
@ -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"] });
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
170
ui/litellm-dashboard/src/components/key_edit_view.tsx
Normal file
170
ui/litellm-dashboard/src/components/key_edit_view.tsx
Normal 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>
|
||||
);
|
||||
}
|
331
ui/litellm-dashboard/src/components/key_info_view.tsx
Normal file
331
ui/litellm-dashboard/src/components/key_info_view.tsx
Normal 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">​</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>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,16 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { keyListCall, Organization } from '../networking';
|
||||
interface Team {
|
||||
|
||||
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 {
|
||||
|
@ -30,7 +38,7 @@ model_max_budget: Record<string, number>;
|
|||
soft_budget_cooldown: boolean;
|
||||
blocked: boolean;
|
||||
litellm_budget_table: Record<string, unknown>;
|
||||
org_id: string | null;
|
||||
organization_id: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
team_spend: number;
|
||||
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) => (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue