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