mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-29 22:21:21 +00:00
keys table refactor pt2
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { keepPreviousData, useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { createQueryKeys } from "../common/queryKeysFactory";
|
||||
import { keyListCall } from "@/components/networking";
|
||||
import { KeyResponse } from "@/components/key_team_helpers/key_list";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
const keyKeys = createQueryKeys("keys");
|
||||
|
||||
export interface KeysResponse {
|
||||
keys: KeyResponse[];
|
||||
total_count: number;
|
||||
current_page: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export const useKeys = (page: number, pageSize: number): UseQueryResult<KeysResponse> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useQuery<KeysResponse>({
|
||||
queryKey: keyKeys.list({ page, limit: pageSize }),
|
||||
queryFn: async () =>
|
||||
await keyListCall(
|
||||
accessToken!,
|
||||
null, // organizationID
|
||||
null, // teamID
|
||||
null, // selectedKeyAlias
|
||||
null, // userID
|
||||
null, // keyHash
|
||||
page,
|
||||
pageSize,
|
||||
),
|
||||
enabled: Boolean(accessToken),
|
||||
staleTime: 30000, // 30 seconds
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
};
|
||||
@@ -136,13 +136,7 @@ it("should render VirtualKeysTable component", () => {
|
||||
keys: [mockKey],
|
||||
setKeys: vi.fn(),
|
||||
isLoading: false,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalCount: 1,
|
||||
},
|
||||
onPageChange: vi.fn(),
|
||||
pageSize: 50,
|
||||
totalCount: 1,
|
||||
teams: [mockTeam],
|
||||
selectedTeam: null,
|
||||
setSelectedTeam: vi.fn(),
|
||||
@@ -166,13 +160,7 @@ it("should display key information correctly", async () => {
|
||||
keys: [mockKey],
|
||||
setKeys: vi.fn(),
|
||||
isLoading: false,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalCount: 1,
|
||||
},
|
||||
onPageChange: vi.fn(),
|
||||
pageSize: 50,
|
||||
totalCount: 1,
|
||||
teams: [mockTeam],
|
||||
selectedTeam: null,
|
||||
setSelectedTeam: vi.fn(),
|
||||
@@ -200,13 +188,7 @@ it("should display user email correctly", async () => {
|
||||
keys: [mockKey],
|
||||
setKeys: vi.fn(),
|
||||
isLoading: false,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalCount: 1,
|
||||
},
|
||||
onPageChange: vi.fn(),
|
||||
pageSize: 50,
|
||||
totalCount: 1,
|
||||
teams: [mockTeam],
|
||||
selectedTeam: null,
|
||||
setSelectedTeam: vi.fn(),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
import { Setter } from "@/types";
|
||||
import { formatNumberWithCommas, updateExistingKeys } from "@/utils/dataUtils";
|
||||
import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
@@ -30,27 +32,10 @@ import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import FilterComponent, { FilterOption } from "../molecules/filter";
|
||||
import { Organization } from "../networking";
|
||||
import KeyInfoView from "../templates/key_info_view";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
interface VirtualKeysTableProps {
|
||||
keys: KeyResponse[];
|
||||
setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void;
|
||||
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;
|
||||
selectedKeyAlias: string | null;
|
||||
setSelectedKeyAlias: Setter<string | null>;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
refresh?: () => void;
|
||||
onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void;
|
||||
currentSort?: {
|
||||
sortBy: string;
|
||||
@@ -63,21 +48,8 @@ interface VirtualKeysTableProps {
|
||||
* The team selector and filtering have been removed so that all keys are shown.
|
||||
*/
|
||||
|
||||
export function VirtualKeysTable({
|
||||
keys,
|
||||
setKeys,
|
||||
isLoading = false,
|
||||
pagination,
|
||||
onPageChange,
|
||||
pageSize = 50,
|
||||
teams,
|
||||
organizations,
|
||||
refresh,
|
||||
onSortChange,
|
||||
currentSort,
|
||||
}: VirtualKeysTableProps) {
|
||||
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
|
||||
const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null);
|
||||
export function VirtualKeysTable({ teams, organizations, onSortChange, currentSort }: VirtualKeysTableProps) {
|
||||
const [selectedKey, setSelectedKey] = useState<KeyResponse | null>(null);
|
||||
const [sorting, setSorting] = React.useState<SortingState>(() => {
|
||||
if (currentSort) {
|
||||
return [
|
||||
@@ -94,22 +66,33 @@ export function VirtualKeysTable({
|
||||
},
|
||||
];
|
||||
});
|
||||
const [tablePagination, setTablePagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
|
||||
const {
|
||||
data: keys,
|
||||
isPending: isLoading,
|
||||
refetch,
|
||||
} = useKeys(tablePagination.pageIndex + 1, tablePagination.pageSize);
|
||||
const totalCount = keys?.total_count || 0;
|
||||
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
|
||||
|
||||
// Use the filter logic hook
|
||||
|
||||
const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } =
|
||||
useFilterLogic({
|
||||
keys,
|
||||
keys: keys?.keys || [],
|
||||
teams,
|
||||
organizations,
|
||||
});
|
||||
|
||||
// Add a useEffect to call refresh when a key is created
|
||||
useEffect(() => {
|
||||
if (refresh) {
|
||||
if (refetch) {
|
||||
const handleStorageChange = () => {
|
||||
refresh();
|
||||
refetch();
|
||||
};
|
||||
|
||||
// Listen for storage events that might indicate a key was created
|
||||
@@ -119,7 +102,7 @@ export function VirtualKeysTable({
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
};
|
||||
}
|
||||
}, [refresh]);
|
||||
}, [refetch]);
|
||||
|
||||
const columns: ColumnDef<KeyResponse>[] = [
|
||||
{
|
||||
@@ -145,7 +128,7 @@ export function VirtualKeysTable({
|
||||
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)}
|
||||
onClick={() => setSelectedKey(info.row.original)}
|
||||
>
|
||||
{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}
|
||||
</Button>
|
||||
@@ -496,6 +479,7 @@ export function VirtualKeysTable({
|
||||
columnResizeDirection: "ltr",
|
||||
state: {
|
||||
sorting,
|
||||
pagination: tablePagination,
|
||||
},
|
||||
onSortingChange: (updaterOrValue) => {
|
||||
const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue;
|
||||
@@ -514,10 +498,14 @@ export function VirtualKeysTable({
|
||||
onSortChange?.(sortBy, sortOrder);
|
||||
}
|
||||
},
|
||||
onPaginationChange: setTablePagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
enableSorting: true,
|
||||
manualSorting: false,
|
||||
manualPagination: true,
|
||||
pageCount: Math.ceil(totalCount / tablePagination.pageSize),
|
||||
});
|
||||
|
||||
// Update local sorting state when currentSort prop changes
|
||||
@@ -534,26 +522,11 @@ export function VirtualKeysTable({
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
{selectedKeyId ? (
|
||||
{selectedKey ? (
|
||||
<KeyInfoView
|
||||
keyId={selectedKeyId}
|
||||
onClose={() => setSelectedKeyId(null)}
|
||||
keyData={filteredKeys.find((k) => k.token === selectedKeyId)}
|
||||
onKeyDataUpdate={(updatedKeyData) => {
|
||||
setKeys((keys) =>
|
||||
keys.map((key) => {
|
||||
if (key.token === updatedKeyData.token) {
|
||||
return updateExistingKeys(key, updatedKeyData);
|
||||
}
|
||||
return key;
|
||||
}),
|
||||
);
|
||||
if (refresh) refresh(); // Minimal fix: refresh the full key list after an update
|
||||
}}
|
||||
onDelete={() => {
|
||||
setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId));
|
||||
if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete
|
||||
}}
|
||||
keyId={selectedKey.token}
|
||||
onClose={() => setSelectedKey(null)}
|
||||
keyData={selectedKey}
|
||||
teams={allTeams}
|
||||
/>
|
||||
) : (
|
||||
@@ -572,26 +545,30 @@ export function VirtualKeysTable({
|
||||
Showing{" "}
|
||||
{isLoading
|
||||
? "..."
|
||||
: `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "}
|
||||
of {isLoading ? "..." : pagination.totalCount} results
|
||||
: `${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} - ${Math.min(
|
||||
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
|
||||
totalCount,
|
||||
)}`}{" "}
|
||||
of {isLoading ? "..." : 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}
|
||||
Page {isLoading ? "..." : table.getState().pagination.pageIndex + 1} of{" "}
|
||||
{isLoading ? "..." : table.getPageCount()}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => onPageChange(pagination.currentPage - 1)}
|
||||
disabled={isLoading || pagination.currentPage === 1}
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={isLoading || !table.getCanPreviousPage()}
|
||||
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}
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={isLoading || !table.getCanNextPage()}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
"use client";
|
||||
import { Setter } from "@/types";
|
||||
import { Button, Col, Grid, Text, TextInput, Title } from "@tremor/react";
|
||||
import { Form, InputNumber, Modal } from "antd";
|
||||
import { Form } from "antd";
|
||||
import { add } from "date-fns";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { VirtualKeysTable } from "../VirtualKeysPage/VirtualKeysTable";
|
||||
import useKeyList, { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import NotificationManager from "../molecules/notifications_manager";
|
||||
import { keyDeleteCall, Organization, regenerateKeyCall } from "../networking";
|
||||
import { Organization } from "../networking";
|
||||
|
||||
// Define the props type
|
||||
interface ViewKeyTableProps {
|
||||
@@ -31,27 +28,17 @@ interface ViewKeyTableProps {
|
||||
}
|
||||
|
||||
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
userID,
|
||||
userRole,
|
||||
accessToken,
|
||||
selectedTeam,
|
||||
setSelectedTeam,
|
||||
data,
|
||||
setData,
|
||||
teams,
|
||||
premiumUser,
|
||||
currentOrg,
|
||||
organizations,
|
||||
setCurrentOrg,
|
||||
selectedKeyAlias,
|
||||
setSelectedKeyAlias,
|
||||
createClicked,
|
||||
setAccessToken,
|
||||
}) => {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
|
||||
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
|
||||
|
||||
const { keys, isLoading, error, pagination, refresh, setKeys } = useKeyList({
|
||||
selectedTeam: selectedTeam || undefined,
|
||||
currentOrg,
|
||||
@@ -65,369 +52,9 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
refresh({ page: newPage });
|
||||
};
|
||||
|
||||
const [selectedToken, setSelectedToken] = useState<KeyResponse | null>(null);
|
||||
const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false);
|
||||
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null);
|
||||
const [regenerateFormData, setRegenerateFormData] = useState<any>(null);
|
||||
const [regenerateForm] = Form.useForm();
|
||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateNewExpiryTime = (duration: string | undefined) => {
|
||||
if (!duration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
let newExpiry: Date;
|
||||
|
||||
if (duration.endsWith("s")) {
|
||||
newExpiry = add(now, { seconds: parseInt(duration) });
|
||||
} else if (duration.endsWith("h")) {
|
||||
newExpiry = add(now, { hours: parseInt(duration) });
|
||||
} else if (duration.endsWith("d")) {
|
||||
newExpiry = add(now, { days: parseInt(duration) });
|
||||
} else {
|
||||
throw new Error("Invalid duration format");
|
||||
}
|
||||
|
||||
return newExpiry.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
hour12: true,
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
console.log("in calculateNewExpiryTime for selectedToken", selectedToken);
|
||||
|
||||
// When a new duration is entered
|
||||
if (regenerateFormData?.duration) {
|
||||
setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration));
|
||||
} else {
|
||||
setNewExpiryTime(null);
|
||||
}
|
||||
|
||||
console.log("calculateNewExpiryTime:", newExpiryTime);
|
||||
}, [selectedToken, regenerateFormData?.duration]);
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (keyToDelete == null || keys == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
await keyDeleteCall(accessToken, keyToDelete);
|
||||
// Successfully completed the deletion. Update the state to trigger a rerender.
|
||||
const filteredKeys = keys.filter((item) => item.token !== keyToDelete);
|
||||
setKeys(filteredKeys);
|
||||
} catch (error) {
|
||||
NotificationManager.error({ description: "Error deleting the key" });
|
||||
}
|
||||
|
||||
// Close the confirmation modal and reset the keyToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setKeyToDelete(null);
|
||||
setDeleteConfirmInput("");
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
// Close the confirmation modal and reset the keyToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setKeyToDelete(null);
|
||||
setDeleteConfirmInput("");
|
||||
};
|
||||
|
||||
const handleRegenerateClick = (token: any) => {
|
||||
setSelectedToken(token);
|
||||
setNewExpiryTime(null);
|
||||
regenerateForm.setFieldsValue({
|
||||
key_alias: token.key_alias,
|
||||
max_budget: token.max_budget,
|
||||
tpm_limit: token.tpm_limit,
|
||||
rpm_limit: token.rpm_limit,
|
||||
duration: token.duration || "",
|
||||
});
|
||||
setRegenerateDialogVisible(true);
|
||||
};
|
||||
|
||||
const handleRegenerateFormChange = (field: string, value: any) => {
|
||||
setRegenerateFormData((prev: any) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRegenerateKey = async () => {
|
||||
if (!premiumUser) {
|
||||
NotificationManager.warning({
|
||||
description: "Regenerate Virtual Key is an Enterprise feature. Please upgrade to use this feature.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedToken == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formValues = await regenerateForm.validateFields();
|
||||
if (!accessToken) return;
|
||||
const response = await regenerateKeyCall(accessToken, selectedToken.token || selectedToken.token_id, formValues);
|
||||
setRegeneratedKey(response.key);
|
||||
|
||||
// Update the data state with the new key_name
|
||||
if (data) {
|
||||
const updatedData = data.map((item) =>
|
||||
item.token === selectedToken?.token ? { ...item, key_name: response.key_name, ...formValues } : item,
|
||||
);
|
||||
setData(updatedData);
|
||||
}
|
||||
|
||||
setRegenerateDialogVisible(false);
|
||||
regenerateForm.resetFields();
|
||||
NotificationManager.success({ description: "Virtual Key regenerated successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error regenerating key:", error);
|
||||
NotificationManager.error({ description: "Failed to regenerate Virtual Key" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VirtualKeysTable
|
||||
keys={keys}
|
||||
setKeys={setKeys}
|
||||
isLoading={isLoading}
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
pageSize={100}
|
||||
teams={teams}
|
||||
selectedTeam={selectedTeam}
|
||||
setSelectedTeam={setSelectedTeam}
|
||||
organizations={organizations}
|
||||
setCurrentOrg={setCurrentOrg}
|
||||
refresh={refresh}
|
||||
selectedKeyAlias={selectedKeyAlias}
|
||||
setSelectedKeyAlias={setSelectedKeyAlias}
|
||||
/>
|
||||
|
||||
{isDeleteModalOpen &&
|
||||
(() => {
|
||||
const keyData = keys?.find((k) => k.token === keyToDelete);
|
||||
const keyName = keyData?.key_alias || keyData?.token_id || keyToDelete;
|
||||
const isValid = deleteConfirmInput === keyName;
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl min-h-[380px] py-6 overflow-hidden transform transition-all flex flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Delete Key</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
cancelDelete();
|
||||
setDeleteConfirmInput("");
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-500 focus:outline-none"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-100 rounded-md mb-5">
|
||||
<div className="text-red-500 mt-0.5">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.082 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base font-medium text-red-600">
|
||||
Warning: You are about to delete this Virtual Key.
|
||||
</p>
|
||||
<p className="text-base text-red-600 mt-2">
|
||||
This action is irreversible and will immediately revoke access for any applications using this
|
||||
key.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-base text-gray-600 mb-5">Are you sure you want to delete this Virtual Key?</p>
|
||||
<div className="mb-5">
|
||||
<label className="block text-base font-medium text-gray-700 mb-2">
|
||||
{`Type `}
|
||||
<span className="underline">{keyName}</span>
|
||||
{` to confirm deletion:`}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirmInput}
|
||||
onChange={(e) => setDeleteConfirmInput(e.target.value)}
|
||||
placeholder="Enter key name exactly"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 bg-gray-50 flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
cancelDelete();
|
||||
setDeleteConfirmInput("");
|
||||
}}
|
||||
className="px-5 py-3 bg-white border border-gray-300 rounded-md text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmDelete}
|
||||
disabled={!isValid}
|
||||
className={`px-5 py-3 rounded-md text-base font-medium text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 ${isValid ? "bg-red-600 hover:bg-red-700" : "bg-red-300 cursor-not-allowed"}`}
|
||||
>
|
||||
Delete Virtual Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Regenerate Key Form Modal */}
|
||||
<Modal
|
||||
title="Regenerate Virtual Key"
|
||||
visible={regenerateDialogVisible}
|
||||
onCancel={() => {
|
||||
setRegenerateDialogVisible(false);
|
||||
regenerateForm.resetFields();
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="cancel"
|
||||
onClick={() => {
|
||||
setRegenerateDialogVisible(false);
|
||||
regenerateForm.resetFields();
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button key="regenerate" onClick={handleRegenerateKey} disabled={!premiumUser}>
|
||||
{premiumUser ? "Regenerate" : "Upgrade to Regenerate"}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{premiumUser ? (
|
||||
<Form
|
||||
form={regenerateForm}
|
||||
layout="vertical"
|
||||
onValuesChange={(changedValues, allValues) => {
|
||||
if ("duration" in changedValues) {
|
||||
handleRegenerateFormChange("duration", changedValues.duration);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item name="key_alias" label="Key Alias">
|
||||
<TextInput disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item name="max_budget" label="Max Budget (USD)">
|
||||
<InputNumber step={0.01} precision={2} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="tpm_limit" label="TPM Limit">
|
||||
<InputNumber style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="rpm_limit" label="RPM Limit">
|
||||
<InputNumber style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="duration" label="Expire Key (eg: 30s, 30h, 30d)" className="mt-8">
|
||||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
Current expiry:{" "}
|
||||
{selectedToken?.expires != null ? new Date(selectedToken.expires).toLocaleString() : "Never"}
|
||||
</div>
|
||||
{newExpiryTime && <div className="mt-2 text-sm text-green-600">New expiry: {newExpiryTime}</div>}
|
||||
</Form>
|
||||
) : (
|
||||
<div>
|
||||
<p className="mb-2 text-gray-500 italic text-[12px]">Upgrade to use this feature</p>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a href="https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* Regenerated Key Display Modal */}
|
||||
{regeneratedKey && (
|
||||
<Modal
|
||||
visible={!!regeneratedKey}
|
||||
onCancel={() => setRegeneratedKey(null)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setRegeneratedKey(null)}>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Grid numItems={1} className="gap-2 w-full">
|
||||
<Title>Regenerated Key</Title>
|
||||
<Col numColSpan={1}>
|
||||
<p>
|
||||
Please replace your old key with the new key generated. For security reasons,{" "}
|
||||
<b>you will not be able to view it again</b> through your LiteLLM account. If you lose this secret key,
|
||||
you will need to generate a new one.
|
||||
</p>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Text className="mt-3">Key Alias:</Text>
|
||||
<div
|
||||
style={{
|
||||
background: "#f8f8f8",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>
|
||||
{selectedToken?.key_alias || "No alias set"}
|
||||
</pre>
|
||||
</div>
|
||||
<Text className="mt-3">New Virtual Key:</Text>
|
||||
<div
|
||||
style={{
|
||||
background: "#f8f8f8",
|
||||
padding: "10px",
|
||||
borderRadius: "5px",
|
||||
marginBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>{regeneratedKey}</pre>
|
||||
</div>
|
||||
<CopyToClipboard
|
||||
text={regeneratedKey}
|
||||
onCopy={() => NotificationManager.success({ description: "Virtual Key copied to clipboard" })}
|
||||
>
|
||||
<Button className="mt-3">Copy Virtual Key</Button>
|
||||
</CopyToClipboard>
|
||||
</Col>
|
||||
</Grid>
|
||||
</Modal>
|
||||
)}
|
||||
<VirtualKeysTable teams={teams} organizations={organizations} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
userInfoCall,
|
||||
modelAvailableCall,
|
||||
getProxyUISettings,
|
||||
Organization,
|
||||
keyInfoCall,
|
||||
getProxyBaseUrl,
|
||||
} from "./networking";
|
||||
import { fetchTeams } from "./common_components/fetch_teams";
|
||||
import { Grid, Col } from "@tremor/react";
|
||||
import CreateKey from "./organisms/create_key_button";
|
||||
import ViewKeyTable from "./templates/view_key_table";
|
||||
import Onboarding from "../app/onboarding/page";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { Typography } from "antd";
|
||||
import { clearTokenCookies } from "@/utils/cookieUtils";
|
||||
import { Col, Grid } from "@tremor/react";
|
||||
import { Typography } from "antd";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Onboarding from "../app/onboarding/page";
|
||||
import { fetchTeams } from "./common_components/fetch_teams";
|
||||
import { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
import {
|
||||
getProxyBaseUrl,
|
||||
getProxyUISettings,
|
||||
keyInfoCall,
|
||||
modelAvailableCall,
|
||||
Organization,
|
||||
userInfoCall,
|
||||
} from "./networking";
|
||||
import CreateKey from "./organisms/create_key_button";
|
||||
import { VirtualKeysTable } from "./VirtualKeysPage/VirtualKeysTable";
|
||||
|
||||
export interface ProxySettings {
|
||||
PROXY_BASE_URL: string | null;
|
||||
@@ -361,25 +361,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||
addKey={addKey}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
|
||||
<ViewKeyTable
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
selectedTeam={selectedTeam ? selectedTeam : null}
|
||||
setSelectedTeam={setSelectedTeam}
|
||||
selectedKeyAlias={selectedKeyAlias}
|
||||
setSelectedKeyAlias={setSelectedKeyAlias}
|
||||
data={keys}
|
||||
setData={setKeys}
|
||||
premiumUser={premiumUser}
|
||||
teams={teams}
|
||||
currentOrg={currentOrg}
|
||||
setCurrentOrg={setCurrentOrg}
|
||||
organizations={organizations}
|
||||
createClicked={createClicked}
|
||||
setAccessToken={setAccessToken}
|
||||
/>
|
||||
<VirtualKeysTable teams={teams} organizations={organizations} />
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user