mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
Merge pull request #18306 from BerriAI/litellm_ui_playground_ta
[Feature] Change Delete Model and Key Modals
This commit is contained in:
commit
7d8475153b
3 changed files with 82 additions and 125 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { ArrowLeftIcon, KeyIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
|
|
@ -19,6 +20,7 @@ import { useEffect, useState } from "react";
|
|||
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
|
||||
import { formItemValidateJSON, truncateString } from "../utils/textUtils";
|
||||
import CacheControlSettings from "./add_model/cache_control_settings";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal";
|
||||
import ReuseCredentialsModal from "./model_add/reuse_credentials";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
|
@ -37,7 +39,6 @@ import { getProviderLogoAndName } from "./provider_info_helpers";
|
|||
import NumericalInput from "./shared/numerical_input";
|
||||
import { Tag } from "./tag_management/types";
|
||||
import { getDisplayModelName } from "./view_model/model_name_display";
|
||||
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
|
||||
|
||||
interface ModelInfoViewProps {
|
||||
modelId: string;
|
||||
|
|
@ -69,6 +70,7 @@ export default function ModelInfoView({
|
|||
const [form] = Form.useForm();
|
||||
const [localModelData, setLocalModelData] = useState<any>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [isCredentialModalOpen, setIsCredentialModalOpen] = useState(false);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
|
@ -318,6 +320,7 @@ export default function ModelInfoView({
|
|||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
if (!accessToken) return;
|
||||
await modelDeleteCall(accessToken, modelId);
|
||||
NotificationsManager.success("Model deleted successfully");
|
||||
|
|
@ -333,6 +336,9 @@ export default function ModelInfoView({
|
|||
} catch (error) {
|
||||
console.error("Error deleting the model:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete model");
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1046,39 +1052,34 @@ export default function ModelInfoView({
|
|||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
{/* 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 Model</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">Are you sure you want to delete this model?</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} className="ml-2" danger>
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={() => setIsDeleteModalOpen(false)}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Model"
|
||||
alertMessage="This action cannot be undone."
|
||||
message="Are you sure you want to delete this model?"
|
||||
resourceInformationTitle="Model Information"
|
||||
resourceInformation={[
|
||||
{
|
||||
label: "Model Name",
|
||||
value: modelData?.model_name || "Not Set",
|
||||
},
|
||||
{
|
||||
label: "LiteLLM Model Name",
|
||||
value: modelData?.litellm_model_name || "Not Set",
|
||||
},
|
||||
{
|
||||
label: "Provider",
|
||||
value: modelData?.provider || "Not Set",
|
||||
},
|
||||
{
|
||||
label: "Created By",
|
||||
value: modelData?.model_info?.created_by || "Not Set",
|
||||
},
|
||||
]}
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
onOk={handleDelete}
|
||||
confirmLoading={deleteLoading}
|
||||
/>
|
||||
|
||||
{isCredentialModalOpen && !usingExistingCredential ? (
|
||||
<ReuseCredentialsModal
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React from "react";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// ---- Hoisted shared mocks (safe to use inside vi.mock factories) ----
|
||||
const { keyUpdateCallMock, keyDeleteCallMock } = vi.hoisted(() => {
|
||||
|
|
@ -123,7 +122,9 @@ vi.mock("@tremor/react", async () => {
|
|||
});
|
||||
|
||||
// antd bits -> async factory & local React
|
||||
vi.mock("antd", async () => {
|
||||
vi.mock("antd", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("antd")>();
|
||||
|
||||
const React = await import("react");
|
||||
|
||||
const Form = { useForm: () => [{}] };
|
||||
|
|
@ -154,7 +155,7 @@ vi.mock("antd", async () => {
|
|||
}
|
||||
(Button as any).displayName = "AntdButton";
|
||||
|
||||
return { Form, Input, InputNumber, Select, Tooltip, Button };
|
||||
return { ...actual, Form, Input, InputNumber, Select, Tooltip, Button };
|
||||
});
|
||||
|
||||
// Icons -> async factory & local React
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
|
|||
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
|
||||
import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "../callback_info_helpers";
|
||||
import AutoRotationView from "../common_components/AutoRotationView";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata } from "../key_info_utils";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import LoggingSettingsView from "../logging_settings_view";
|
||||
|
|
@ -59,6 +60,7 @@ export default function KeyInfoView({
|
|||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
|
||||
const [isRegenerateModalOpen, setIsRegenerateModalOpen] = useState(false);
|
||||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
|
|
@ -237,6 +239,7 @@ export default function KeyInfoView({
|
|||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
if (!accessToken) return;
|
||||
await keyDeleteCall(accessToken as string, currentKeyData.token || currentKeyData.token_id);
|
||||
NotificationManager.success("Key deleted successfully");
|
||||
|
|
@ -247,9 +250,11 @@ export default function KeyInfoView({
|
|||
} catch (error) {
|
||||
console.error("Error deleting the key:", error);
|
||||
NotificationManager.fromBackend(error);
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
setDeleteConfirmInput("");
|
||||
}
|
||||
// Reset the confirmation input
|
||||
setDeleteConfirmInput("");
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string, key: string) => {
|
||||
|
|
@ -402,90 +407,40 @@ export default function KeyInfoView({
|
|||
/>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{isDeleteModalOpen &&
|
||||
(() => {
|
||||
const keyName = currentKeyData?.key_alias || currentKeyData?.token_id || "Virtual Key";
|
||||
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={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
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={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
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={handleDelete}
|
||||
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 Key
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Key"
|
||||
alertMessage="This action is irreversible and will immediately revoke access for any applications using this key."
|
||||
message="Are you sure you want to delete this Virtual Key?"
|
||||
resourceInformationTitle="Key Information"
|
||||
resourceInformation={[
|
||||
{
|
||||
label: "Key Alias",
|
||||
value: currentKeyData?.key_alias || "-",
|
||||
},
|
||||
{
|
||||
label: "Key ID",
|
||||
value: currentKeyData?.token_id || currentKeyData?.token || "-",
|
||||
code: true,
|
||||
},
|
||||
{
|
||||
label: "Team ID",
|
||||
value: currentKeyData?.team_id || "-",
|
||||
code: true,
|
||||
},
|
||||
{
|
||||
label: "Spend",
|
||||
value: currentKeyData?.spend ? `$${formatNumberWithCommas(currentKeyData.spend, 4)}` : "$0.0000",
|
||||
},
|
||||
]}
|
||||
onCancel={() => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setDeleteConfirmInput("");
|
||||
}}
|
||||
onOk={handleDelete}
|
||||
confirmLoading={deleteLoading}
|
||||
requiredConfirmation={currentKeyData?.key_alias}
|
||||
/>
|
||||
|
||||
<TabGroup>
|
||||
<TabList className="mb-4">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue