diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 37aa68bcc73..f66fd005ae1 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -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(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({ - {/* Delete Confirmation Modal */} - {isDeleteModalOpen && ( -
-
- - - - -
-
-
-
-

Delete Model

-
-

Are you sure you want to delete this model?

-
-
-
-
-
- - -
-
-
-
- )} + setIsDeleteModalOpen(false)} + onOk={handleDelete} + confirmLoading={deleteLoading} + /> {isCredentialModalOpen && !usingExistingCredential ? ( { @@ -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(); + 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 diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index d3028802ad5..47abda58c29 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -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>({}); @@ -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 ( -
-
-
-
-

Delete Key

- -
-
-
-
- - - -
-
-

- Warning: You are about to delete this Virtual Key. -

-

- This action is irreversible and will immediately revoke access for any applications using this - key. -

-
-
-

Are you sure you want to delete this Virtual Key?

-
- - 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 - /> -
-
-
-
- - -
-
-
- ); - })()} + { + setIsDeleteModalOpen(false); + setDeleteConfirmInput(""); + }} + onOk={handleDelete} + confirmLoading={deleteLoading} + requiredConfirmation={currentKeyData?.key_alias} + />