(UI) - Refactor View Key Table (#8526)

* new key table

* clean up

* fix view all team keys

* fixed create key button

* show all keys

* ui select team flow

* pagination on keys

* fix aligning of team and pagination

* show key hash

* allow clicking into key

* click into a key

* add current status

* fix key alias edit

* delete key

* fix(create_key_button.tsx): allow user to select team when creating key

* working edit key

* feat(create_key_button.tsx): switch available models based on selected team

enables user to create a key for a specific team

* fix(create_key_button.tsx): improve type safety of component

* fix(create_key_button.tsx): style cleanup

* pass team all the way thru

* refactor getTeamModels

* fix(columns.tsx): make cost easier to see

* ui fix edit key ui

* cleanup

* fix linting error

* fix filter

* fix linting

* ui fix all keys

* fix linting

* fix linting

* fix org id

* fix linting

* fix linting

* fix linting

* fix linting

* fix linting

* fix linting

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com>
This commit is contained in:
Ishaan Jaff 2025-02-13 21:51:54 -08:00 committed by GitHub
parent 58141df65d
commit 3cfb44ba88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1134 additions and 905 deletions

View file

@ -1,5 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import {
keyDeleteCall,
modelAvailableCall,
@ -42,6 +42,8 @@ import {
BarChart,
TextInput,
Textarea,
Select,
SelectItem,
} from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import {
@ -49,8 +51,6 @@ import {
getModelDisplayName,
} from "./key_team_helpers/fetch_available_models_team_key";
import {
Select as Select3,
SelectItem,
MultiSelect,
MultiSelectItem,
} from "@tremor/react";
@ -62,16 +62,15 @@ import {
Select as Select2,
InputNumber,
message,
Select,
Tooltip,
DatePicker,
} from "antd";
import { CopyToClipboard } from "react-copy-to-clipboard";
import TextArea from "antd/es/input/TextArea";
import useKeyList from "./key_team_helpers/key_list";
import { KeyResponse } from "./key_team_helpers/key_list";
const { Option } = Select;
import { AllKeysTable } from "./all_keys_table";
import { Team } from "./key_team_helpers/key_list";
const isLocal = process.env.NODE_ENV === "development";
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
@ -100,9 +99,10 @@ interface ViewKeyTableProps {
userRole: string | null;
accessToken: string;
selectedTeam: any | null;
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
data: any[] | null;
setData: React.Dispatch<React.SetStateAction<any[] | null>>;
teams: any[] | null;
teams: Team[] | null;
premiumUser: boolean;
currentOrg: Organization | null;
}
@ -145,6 +145,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
userRole,
accessToken,
selectedTeam,
setSelectedTeam,
data,
setData,
teams,
@ -159,13 +160,43 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
{ day: string; spend: number }[] | null
>(null);
const { keys, isLoading, error, refresh } = useKeyList({
// NEW: Declare filter states for team and key alias.
const [teamFilter, setTeamFilter] = useState<string>(selectedTeam?.team_id || "");
const [keyAliasFilter, setKeyAliasFilter] = useState<string>("");
// Keep the team filter in sync with the incoming prop.
useEffect(() => {
setTeamFilter(selectedTeam?.team_id || "");
}, [selectedTeam]);
// Build a memoized filters object for the backend call.
const filters = useMemo(
() => {
const f: { team_id?: string; key_alias?: string } = {};
if (teamFilter) {
f.team_id = teamFilter;
}
if (keyAliasFilter) {
f.key_alias = keyAliasFilter;
}
return f;
},
[teamFilter, keyAliasFilter]
);
// Pass filters into the hook so the API call includes these query parameters.
const { keys, isLoading, error, pagination, refresh } = useKeyList({
selectedTeam,
currentOrg,
accessToken
accessToken,
});
console.log("keys", keys);
const handlePageChange = (newPage: number) => {
refresh({ page: newPage });
};
const [editModalVisible, setEditModalVisible] = useState(false);
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
@ -298,311 +329,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
setKnownTeamIDs(teamIDSet);
}
}, [teams]);
const EditKeyModal: React.FC<EditKeyModalProps> = ({
visible,
onCancel,
token,
onSubmit,
}) => {
const [form] = Form.useForm();
const [keyTeam, setKeyTeam] = useState(selectedTeam);
const [errorModels, setErrorModels] = useState<string[]>([]);
const [errorBudget, setErrorBudget] = useState<boolean>(false);
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
useEffect(() => {
const fetchGuardrails = async () => {
try {
const response = await getGuardrailsList(accessToken);
const guardrailNames = response.guardrails.map(
(g: { guardrail_name: string }) => g.guardrail_name
);
setGuardrailsList(guardrailNames);
} catch (error) {
console.error("Failed to fetch guardrails:", error);
}
};
fetchGuardrails();
}, [accessToken]);
let metadataString = "";
try {
// Create a copy of metadata without guardrails for display
const displayMetadata = { ...token.metadata };
delete displayMetadata.guardrails;
metadataString = JSON.stringify(displayMetadata, null, 2);
} catch (error) {
console.error("Error stringifying metadata:", error);
metadataString = "";
}
// Extract existing guardrails from metadata
let existingGuardrails: string[] = [];
try {
existingGuardrails = token.metadata?.guardrails || [];
} catch (error) {
console.error("Error extracting guardrails:", error);
}
const initialValues =
token ?
{
...token,
budget_duration: token.budget_duration,
metadata: metadataString,
guardrails: existingGuardrails,
}
: { metadata: metadataString, guardrails: [] };
const handleOk = () => {
form
.validateFields()
.then((values) => {
// const updatedValues = {...values, team_id: team.team_id};
// onSubmit(updatedValues);
form.resetFields();
})
.catch((error) => {
console.error("Validation failed:", error);
});
};
return (
<Modal
title="Edit Key"
visible={visible}
width={800}
footer={null}
onOk={handleOk}
onCancel={onCancel}
>
<Form
form={form}
onFinish={handleEditSubmit}
initialValues={initialValues}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<>
<Form.Item name="key_alias" label="Key Alias">
<TextInput />
</Form.Item>
<Form.Item
label="Models"
name="models"
rules={[
{
validator: (rule, value) => {
if (keyTeam.team_alias === "Default Team") {
return Promise.resolve();
}
const errorModels = value.filter(
(model: string) =>
!keyTeam.models.includes(model) &&
model !== "all-team-models" &&
model !== "all-proxy-models" &&
!keyTeam.models.includes("all-proxy-models")
);
console.log(`errorModels: ${errorModels}`);
if (errorModels.length > 0) {
return Promise.reject(
`Some models are not part of the new team's models - ${errorModels} Team models: ${keyTeam.models}`
);
} else {
return Promise.resolve();
}
},
},
]}
>
<Select
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
<Option key="all-team-models" value="all-team-models">
All Team Models
</Option>
{keyTeam.team_alias === "Default Team" ?
userModels
.filter((model) => model !== "all-proxy-models")
.map((model: string) => (
<Option key={model} value={model}>
{getModelDisplayName(model)}
</Option>
))
: keyTeam.models.map((model: string) => (
<Option key={model} value={model}>
{getModelDisplayName(model)}
</Option>
))
}
</Select>
</Form.Item>
<Form.Item
className="mt-8"
label="Max Budget (USD)"
name="max_budget"
help={`Budget cannot exceed team max budget: ${keyTeam?.max_budget !== null && keyTeam?.max_budget !== undefined ? keyTeam?.max_budget : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
keyTeam &&
keyTeam.max_budget !== null &&
value > keyTeam.max_budget
) {
console.log(`keyTeam.max_budget: ${keyTeam.max_budget}`);
throw new Error(
`Budget cannot exceed team max budget: $${keyTeam.max_budget}`
);
}
},
},
]}
>
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="Reset Budget"
name="budget_duration"
help={`Current Reset Budget: ${
token.budget_duration
}, budget will be reset: ${token.budget_reset_at ? new Date(token.budget_reset_at).toLocaleString() : "Never"}`}
>
<Select placeholder="n/a">
<Select.Option value="daily">daily</Select.Option>
<Select.Option value="weekly">weekly</Select.Option>
<Select.Option value="monthly">monthly</Select.Option>
</Select>
</Form.Item>
<Form.Item label="token" name="token" hidden={true}></Form.Item>
<Form.Item
label="Team"
name="team_id"
className="mt-8"
help="the team this key belongs to"
>
<Select3 value={token.team_alias}>
{teams?.map((team_obj, index) => (
<SelectItem
key={index}
value={team_obj.team_id}
onClick={() => setKeyTeam(team_obj)}
>
{team_obj.team_alias}
</SelectItem>
))}
</Select3>
</Form.Item>
<Form.Item
className="mt-8"
label="TPM Limit (tokens per minute)"
name="tpm_limit"
help={`tpm_limit cannot exceed team tpm_limit ${keyTeam?.tpm_limit !== null && keyTeam?.tpm_limit !== undefined ? keyTeam?.tpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
keyTeam &&
keyTeam.tpm_limit !== null &&
value > keyTeam.tpm_limit
) {
console.log(`keyTeam.tpm_limit: ${keyTeam.tpm_limit}`);
throw new Error(
`tpm_limit cannot exceed team max tpm_limit: $${keyTeam.tpm_limit}`
);
}
},
},
]}
>
<InputNumber step={1} precision={1} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="RPM Limit (requests per minute)"
name="rpm_limit"
help={`rpm_limit cannot exceed team max rpm_limit: ${keyTeam?.rpm_limit !== null && keyTeam?.rpm_limit !== undefined ? keyTeam?.rpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
if (
value &&
keyTeam &&
keyTeam.rpm_limit !== null &&
value > keyTeam.rpm_limit
) {
console.log(`keyTeam.rpm_limit: ${keyTeam.rpm_limit}`);
throw new Error(
`rpm_limit cannot exceed team max rpm_limit: $${keyTeam.rpm_limit}`
);
}
},
},
]}
>
<InputNumber step={1} precision={1} width={200} />
</Form.Item>
<Form.Item
label={
<span>
Guardrails{" "}
<Tooltip title="Setup your first guardrail">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="guardrails"
className="mt-8"
help="Select existing guardrails or enter new ones"
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Select or enter guardrails"
options={guardrailsList.map((name) => ({
value: name,
label: name,
}))}
/>
</Form.Item>
<Form.Item
label="Metadata (ensure this is valid JSON)"
name="metadata"
>
<TextArea
rows={10}
onChange={(e) => {
form.setFieldsValue({ metadata: e.target.value });
}}
/>
</Form.Item>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Edit Key</Button2>
</div>
</Form>
</Modal>
);
};
const ModelLimitModal: React.FC<ModelLimitModalProps> = ({
visible,
@ -753,7 +480,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
{newModelRow !== null && (
<TableRow>
<TableCell>
<Select
<Select2
style={{ width: 200 }}
placeholder="Select a model"
onChange={handleModelSelect}
@ -762,11 +489,11 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
{availableModels
.filter((m) => !modelLimits.hasOwnProperty(m))
.map((m) => (
<Option key={m} value={m}>
<Select2.Option key={m} value={m}>
{m}
</Option>
</Select2.Option>
))}
</Select>
</Select2>
</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
@ -1013,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"
>
&#8203;
</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"
>
&#8203;
</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 */}