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 { jwtDecode } from "jwt-decode";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { defaultOrg } from "@/components/common_components/default_org";
|
import { defaultOrg } from "@/components/common_components/default_org";
|
||||||
|
import { Team } from "@/components/key_team_helpers/key_list";
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
import UserDashboard from "@/components/user_dashboard";
|
import UserDashboard from "@/components/user_dashboard";
|
||||||
import ModelDashboard from "@/components/model_dashboard";
|
import ModelDashboard from "@/components/model_dashboard";
|
||||||
|
@ -76,7 +77,7 @@ export default function CreateKeyPage() {
|
||||||
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] =
|
const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
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 [keys, setKeys] = useState<null | any[]>(null);
|
||||||
const [currentOrg, setCurrentOrg] = useState<Organization>(defaultOrg);
|
const [currentOrg, setCurrentOrg] = useState<Organization>(defaultOrg);
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
import { Select, SelectItem, Subtitle } from "@tremor/react";
|
import { Select, SelectItem, Subtitle } from "@tremor/react";
|
||||||
|
import { Team } from "./key_team_helpers/key_list";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
@ -38,7 +39,7 @@ import { InvitationLink } from "./onboarding_link";
|
||||||
interface AdminPanelProps {
|
interface AdminPanelProps {
|
||||||
searchParams: any;
|
searchParams: any;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
|
||||||
showSSOBanner: boolean;
|
showSSOBanner: boolean;
|
||||||
premiumUser: 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,
|
modelAvailableCall,
|
||||||
getGuardrailsList,
|
getGuardrailsList,
|
||||||
} from "./networking";
|
} from "./networking";
|
||||||
|
import { Team } from "./key_team_helpers/key_list";
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Tooltip } from 'antd';
|
import { Tooltip } from 'antd';
|
||||||
|
|
||||||
|
@ -37,11 +38,12 @@ const { Option } = Select;
|
||||||
|
|
||||||
interface CreateKeyProps {
|
interface CreateKeyProps {
|
||||||
userID: string;
|
userID: string;
|
||||||
team: any | null;
|
team: Team | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
data: any[] | null;
|
data: any[] | null;
|
||||||
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
||||||
|
teams: Team[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPredefinedTags = (data: any[] | null) => {
|
const getPredefinedTags = (data: any[] | null) => {
|
||||||
|
@ -68,10 +70,34 @@ const getPredefinedTags = (data: any[] | null) => {
|
||||||
return uniqueTags;
|
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> = ({
|
const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
userID,
|
userID,
|
||||||
team,
|
team,
|
||||||
|
teams,
|
||||||
userRole,
|
userRole,
|
||||||
accessToken,
|
accessToken,
|
||||||
data,
|
data,
|
||||||
|
@ -86,6 +112,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
const [keyOwner, setKeyOwner] = useState("you");
|
const [keyOwner, setKeyOwner] = useState("you");
|
||||||
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
|
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
|
||||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||||
|
const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState<Team | null>(team);
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
|
@ -197,30 +224,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let tempModelsToPick = [];
|
const models = getTeamModels(selectedCreateKeyTeam, userModels);
|
||||||
|
setModelsToPick(models);
|
||||||
if (team) {
|
form.setFieldValue('models', []);
|
||||||
if (team.models.length > 0) {
|
}, [selectedCreateKeyTeam, userModels]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -278,18 +285,45 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
<TextInput placeholder="" />
|
<TextInput placeholder="" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Team ID"
|
label="Team"
|
||||||
name="team_id"
|
name="team_id"
|
||||||
hidden={keyOwner !== "another_user"}
|
initialValue={team ? team.team_id : null}
|
||||||
initialValue={team ? team["team_id"] : null}
|
|
||||||
valuePropName="team_id"
|
|
||||||
className="mt-8"
|
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>
|
||||||
|
|
||||||
<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"
|
name="models"
|
||||||
rules={[{ required: true, message: "Please select a model" }]}
|
rules={[{ required: true, message: "Please select a model" }]}
|
||||||
help="required"
|
help="required"
|
||||||
|
@ -299,15 +333,8 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
placeholder="Select models"
|
placeholder="Select models"
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
onChange={(values) => {
|
onChange={(values) => {
|
||||||
// Check if "All Team Models" is selected
|
if (values.includes("all-team-models")) {
|
||||||
const isAllTeamModelsSelected =
|
form.setFieldsValue({ models: ["all-team-models"] });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
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,68 +1,76 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { keyListCall, Organization } from '../networking';
|
import { keyListCall, Organization } from '../networking';
|
||||||
interface Team {
|
|
||||||
team_id: string;
|
export interface Team {
|
||||||
team_alias: string;
|
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 {
|
export interface KeyResponse {
|
||||||
token: string;
|
token: string;
|
||||||
key_name: string;
|
key_name: string;
|
||||||
key_alias: string;
|
key_alias: string;
|
||||||
spend: number;
|
spend: number;
|
||||||
max_budget: number;
|
max_budget: number;
|
||||||
expires: string;
|
expires: string;
|
||||||
models: string[];
|
models: string[];
|
||||||
aliases: Record<string, unknown>;
|
aliases: Record<string, unknown>;
|
||||||
config: 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?: {
|
|
||||||
user_id: string;
|
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;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyListResponse {
|
interface KeyListResponse {
|
||||||
|
@ -76,6 +84,7 @@ interface UseKeyListProps {
|
||||||
selectedTeam?: Team;
|
selectedTeam?: Team;
|
||||||
currentOrg: Organization | null;
|
currentOrg: Organization | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
currentPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaginationData {
|
interface PaginationData {
|
||||||
|
@ -92,7 +101,13 @@ pagination: PaginationData;
|
||||||
refresh: (params?: Record<string, unknown>) => Promise<void>;
|
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>({
|
const [keyData, setKeyData] = useState<KeyListResponse>({
|
||||||
keys: [],
|
keys: [],
|
||||||
total_count: 0,
|
total_count: 0,
|
||||||
|
@ -105,20 +120,25 @@ const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps):
|
||||||
const fetchKeys = async (params: Record<string, unknown> = {}): Promise<void> => {
|
const fetchKeys = async (params: Record<string, unknown> = {}): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
console.log("calling fetchKeys");
|
console.log("calling fetchKeys");
|
||||||
if (!currentOrg || !selectedTeam || !accessToken) {
|
if (!currentOrg || !accessToken) {
|
||||||
console.log("currentOrg", currentOrg);
|
console.log("currentOrg", currentOrg);
|
||||||
console.log("selectedTeam", selectedTeam);
|
|
||||||
console.log("accessToken", accessToken);
|
console.log("accessToken", accessToken);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
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);
|
console.log("data", data);
|
||||||
setKeyData(data);
|
setKeyData(data);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err : new Error('An error occurred'));
|
setError(err instanceof Error ? err : new Error("An error occurred"));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -126,7 +146,8 @@ const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps):
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchKeys();
|
fetchKeys();
|
||||||
}, [selectedTeam, currentOrg]);
|
console.log("selectedTeam", selectedTeam, "currentOrg", currentOrg, "accessToken", accessToken);
|
||||||
|
}, [selectedTeam, currentOrg, accessToken]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
keys: keyData.keys,
|
keys: keyData.keys,
|
||||||
|
|
|
@ -2079,6 +2079,8 @@ export const keyListCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
organizationID: string | null,
|
organizationID: string | null,
|
||||||
teamID: string | null,
|
teamID: string | null,
|
||||||
|
page: number,
|
||||||
|
pageSize: number
|
||||||
) => {
|
) => {
|
||||||
/**
|
/**
|
||||||
* Get all available teams on proxy
|
* Get all available teams on proxy
|
||||||
|
@ -2096,6 +2098,14 @@ export const keyListCall = async (
|
||||||
queryParams.append('organization_id', organizationID.toString());
|
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');
|
queryParams.append('return_full_object', 'true');
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking";
|
import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking";
|
||||||
|
import { Team } from "./key_team_helpers/key_list";
|
||||||
import {
|
import {
|
||||||
Col,
|
Col,
|
||||||
Grid,
|
Grid,
|
||||||
|
@ -15,10 +16,10 @@ if (isLocal != true) {
|
||||||
console.log = function() {};
|
console.log = function() {};
|
||||||
}
|
}
|
||||||
interface TeamProps {
|
interface TeamProps {
|
||||||
teams: any[] | null;
|
teams: Team[] | null;
|
||||||
searchParams: any;
|
searchParams: any;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
premiumUser: boolean;
|
premiumUser: boolean;
|
||||||
|
|
|
@ -53,16 +53,17 @@ import {
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { CogIcon } from "@heroicons/react/outline";
|
import { CogIcon } from "@heroicons/react/outline";
|
||||||
import AvailableTeamsPanel from "@/components/team/available_teams";
|
import AvailableTeamsPanel from "@/components/team/available_teams";
|
||||||
|
import type { Team } from "./key_team_helpers/key_list";
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||||
if (isLocal != true) {
|
if (isLocal != true) {
|
||||||
console.log = function() {};
|
console.log = function() {};
|
||||||
}
|
}
|
||||||
interface TeamProps {
|
interface TeamProps {
|
||||||
teams: any[] | null;
|
teams: Team[] | null;
|
||||||
searchParams: any;
|
searchParams: any;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
currentOrg: Organization | null;
|
currentOrg: Organization | null;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import ViewUserTeam from "./view_user_team";
|
||||||
import DashboardTeam from "./dashboard_default_team";
|
import DashboardTeam from "./dashboard_default_team";
|
||||||
import Onboarding from "../app/onboarding/page";
|
import Onboarding from "../app/onboarding/page";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
import { Team } from "./key_team_helpers/key_list";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import { Typography } from "antd";
|
import { Typography } from "antd";
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
|
@ -56,11 +57,11 @@ interface UserDashboardProps {
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
teams: any[] | null;
|
teams: Team[] | null;
|
||||||
keys: any[] | null;
|
keys: any[] | null;
|
||||||
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
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>>;
|
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
||||||
setOrganizations: React.Dispatch<React.SetStateAction<Organization[]>>;
|
setOrganizations: React.Dispatch<React.SetStateAction<Organization[]>>;
|
||||||
premiumUser: boolean;
|
premiumUser: boolean;
|
||||||
|
@ -108,9 +109,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
team_alias: "Default Team",
|
team_alias: "Default Team",
|
||||||
team_id: null,
|
team_id: null,
|
||||||
};
|
};
|
||||||
const [selectedTeam, setSelectedTeam] = useState<any | null>(
|
const [selectedTeam, setSelectedTeam] = useState<any | null>(null);
|
||||||
teams ? teams[0] : defaultTeam
|
|
||||||
);
|
|
||||||
// check if window is not undefined
|
// check if window is not undefined
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("beforeunload", function () {
|
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(
|
sessionStorage.setItem(
|
||||||
"userData" + userID,
|
"userData" + userID,
|
||||||
JSON.stringify(response["keys"])
|
JSON.stringify(response["keys"])
|
||||||
|
@ -348,20 +337,16 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-4">
|
<div className="w-full mx-4">
|
||||||
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
|
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
|
||||||
<Col numColSpan={1}>
|
<Col numColSpan={1} className="flex flex-col gap-2">
|
||||||
<ViewUserTeam
|
<CreateKey
|
||||||
|
key={selectedTeam ? selectedTeam.team_id : null}
|
||||||
userID={userID}
|
userID={userID}
|
||||||
|
team={selectedTeam as Team | null}
|
||||||
|
teams={teams as Team[]}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
selectedTeam={selectedTeam ? selectedTeam : null}
|
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
data={keys}
|
||||||
<ViewUserSpend
|
setData={setKeys}
|
||||||
userID={userID}
|
|
||||||
userRole={userRole}
|
|
||||||
userMaxBudget={userSpendData?.max_budget || null}
|
|
||||||
accessToken={accessToken}
|
|
||||||
userSpend={teamSpend}
|
|
||||||
selectedTeam={selectedTeam ? selectedTeam : null}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ViewKeyTable
|
<ViewKeyTable
|
||||||
|
@ -369,31 +354,13 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
selectedTeam={selectedTeam ? selectedTeam : null}
|
selectedTeam={selectedTeam ? selectedTeam : null}
|
||||||
|
setSelectedTeam={setSelectedTeam}
|
||||||
data={keys}
|
data={keys}
|
||||||
setData={setKeys}
|
setData={setKeys}
|
||||||
premiumUser={premiumUser}
|
premiumUser={premiumUser}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
currentOrg={currentOrg}
|
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>
|
</Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
keyDeleteCall,
|
keyDeleteCall,
|
||||||
modelAvailableCall,
|
modelAvailableCall,
|
||||||
|
@ -42,6 +42,8 @@ import {
|
||||||
BarChart,
|
BarChart,
|
||||||
TextInput,
|
TextInput,
|
||||||
Textarea,
|
Textarea,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||||
import {
|
import {
|
||||||
|
@ -49,8 +51,6 @@ import {
|
||||||
getModelDisplayName,
|
getModelDisplayName,
|
||||||
} from "./key_team_helpers/fetch_available_models_team_key";
|
} from "./key_team_helpers/fetch_available_models_team_key";
|
||||||
import {
|
import {
|
||||||
Select as Select3,
|
|
||||||
SelectItem,
|
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
MultiSelectItem,
|
MultiSelectItem,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
|
@ -62,16 +62,15 @@ import {
|
||||||
Select as Select2,
|
Select as Select2,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
message,
|
message,
|
||||||
Select,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
|
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
import TextArea from "antd/es/input/TextArea";
|
import TextArea from "antd/es/input/TextArea";
|
||||||
import useKeyList from "./key_team_helpers/key_list";
|
import useKeyList from "./key_team_helpers/key_list";
|
||||||
import { KeyResponse } 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 isLocal = process.env.NODE_ENV === "development";
|
||||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||||
|
@ -100,9 +99,10 @@ interface ViewKeyTableProps {
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
selectedTeam: any | null;
|
selectedTeam: any | null;
|
||||||
|
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
|
||||||
data: any[] | null;
|
data: any[] | null;
|
||||||
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
|
||||||
teams: any[] | null;
|
teams: Team[] | null;
|
||||||
premiumUser: boolean;
|
premiumUser: boolean;
|
||||||
currentOrg: Organization | null;
|
currentOrg: Organization | null;
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
userRole,
|
userRole,
|
||||||
accessToken,
|
accessToken,
|
||||||
selectedTeam,
|
selectedTeam,
|
||||||
|
setSelectedTeam,
|
||||||
data,
|
data,
|
||||||
setData,
|
setData,
|
||||||
teams,
|
teams,
|
||||||
|
@ -159,13 +160,43 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
{ day: string; spend: number }[] | null
|
{ day: string; spend: number }[] | null
|
||||||
>(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,
|
selectedTeam,
|
||||||
currentOrg,
|
currentOrg,
|
||||||
accessToken
|
accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("keys", keys);
|
const handlePageChange = (newPage: number) => {
|
||||||
|
refresh({ page: newPage });
|
||||||
|
};
|
||||||
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
||||||
|
@ -298,311 +329,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
setKnownTeamIDs(teamIDSet);
|
setKnownTeamIDs(teamIDSet);
|
||||||
}
|
}
|
||||||
}, [teams]);
|
}, [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> = ({
|
const ModelLimitModal: React.FC<ModelLimitModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
|
@ -753,7 +480,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
{newModelRow !== null && (
|
{newModelRow !== null && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Select
|
<Select2
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
placeholder="Select a model"
|
placeholder="Select a model"
|
||||||
onChange={handleModelSelect}
|
onChange={handleModelSelect}
|
||||||
|
@ -762,11 +489,11 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
{availableModels
|
{availableModels
|
||||||
.filter((m) => !modelLimits.hasOwnProperty(m))
|
.filter((m) => !modelLimits.hasOwnProperty(m))
|
||||||
.map((m) => (
|
.map((m) => (
|
||||||
<Option key={m} value={m}>
|
<Select2.Option key={m} value={m}>
|
||||||
{m}
|
{m}
|
||||||
</Option>
|
</Select2.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select2>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
<TableCell>-</TableCell>
|
<TableCell>-</TableCell>
|
||||||
|
@ -1013,440 +740,71 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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")))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4 mt-2">
|
<AllKeysTable
|
||||||
<Table className="mt-5 max-h-[300px] min-h-[300px]">
|
keys={keys}
|
||||||
<TableHead>
|
isLoading={isLoading}
|
||||||
<TableRow>
|
pagination={pagination}
|
||||||
<TableHeaderCell>Key Alias</TableHeaderCell>
|
onPageChange={handlePageChange}
|
||||||
<TableHeaderCell>Secret Key</TableHeaderCell>
|
pageSize={50}
|
||||||
<TableHeaderCell>Created</TableHeaderCell>
|
teams={teams}
|
||||||
<TableHeaderCell>Expires</TableHeaderCell>
|
selectedTeam={selectedTeam}
|
||||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
setSelectedTeam={setSelectedTeam}
|
||||||
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
accessToken={accessToken}
|
||||||
<TableHeaderCell>Budget Reset</TableHeaderCell>
|
userID={userID}
|
||||||
<TableHeaderCell>Models</TableHeaderCell>
|
userRole={userRole}
|
||||||
<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 (
|
{isDeleteModalOpen && (
|
||||||
selectedTeam.team_id == null &&
|
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||||
item.team_id !== null &&
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
!knownTeamIDs.has(item.team_id)
|
<div
|
||||||
) {
|
className="fixed inset-0 transition-opacity"
|
||||||
// do nothing -> returns a row with this key
|
aria-hidden="true"
|
||||||
} else if (item.team_id != selectedTeam.team_id) {
|
>
|
||||||
return null;
|
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||||
}
|
</div>
|
||||||
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>
|
{/* Modal Panel */}
|
||||||
{Array.isArray(item.models) ?
|
<span
|
||||||
<div
|
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
style={{ display: "flex", flexDirection: "column" }}
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{item.models.length === 0 ?
|
​
|
||||||
<>
|
</span>
|
||||||
{
|
|
||||||
(
|
|
||||||
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>
|
|
||||||
|
|
||||||
}
|
{/* Confirmation Modal Content */}
|
||||||
</>
|
<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">
|
||||||
: item.models.map((model: string, index: number) =>
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
model === "all-proxy-models" ?
|
<div className="sm:flex sm:items-start">
|
||||||
<Badge
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
key={index}
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
size={"xs"}
|
Delete Key
|
||||||
className="mb-1"
|
</h3>
|
||||||
color="red"
|
<div className="mt-2">
|
||||||
>
|
<p className="text-sm text-gray-500">
|
||||||
<Text>All Proxy Models</Text>
|
Are you sure you want to delete this key ?
|
||||||
</Badge>
|
</p>
|
||||||
: 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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
{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>
|
|
||||||
|
|
||||||
{/* Modal Panel */}
|
|
||||||
<span
|
|
||||||
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
​
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Confirmation Modal Content */}
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
</div>
|
||||||
<Button onClick={confirmDelete} color="red" className="ml-2">
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
Delete
|
<Button onClick={confirmDelete} color="red" className="ml-2">
|
||||||
</Button>
|
Delete
|
||||||
<Button onClick={cancelDelete}>Cancel</Button>
|
</Button>
|
||||||
</div>
|
<Button onClick={cancelDelete}>Cancel</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* Regenerate Key Form Modal */}
|
||||||
|
|
|
@ -67,6 +67,13 @@ export const columns: ColumnDef<LogEntry>[] = [
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: "Cost",
|
||||||
|
accessorKey: "spend",
|
||||||
|
cell: (info: any) => (
|
||||||
|
<span>${Number(info.getValue() || 0).toFixed(6)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: "Country",
|
header: "Country",
|
||||||
accessorKey: "requester_ip_address",
|
accessorKey: "requester_ip_address",
|
||||||
|
@ -149,13 +156,7 @@ export const columns: ColumnDef<LogEntry>[] = [
|
||||||
accessorKey: "end_user",
|
accessorKey: "end_user",
|
||||||
cell: (info: any) => <span>{String(info.getValue() || "-")}</span>,
|
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",
|
header: "Tags",
|
||||||
accessorKey: "request_tags",
|
accessorKey: "request_tags",
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function DataTable<TData, TValue>({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg custom-border table-wrapper">
|
<div className="rounded-lg custom-border">
|
||||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue