Merge pull request #18306 from BerriAI/litellm_ui_playground_ta

[Feature] Change Delete Model and Key Modals
This commit is contained in:
yuneng-jiang 2025-12-20 17:20:23 -08:00 committed by GitHub
commit 7d8475153b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 82 additions and 125 deletions

View file

@ -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">
&#8203;
</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

View file

@ -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

View file

@ -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">