diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx index 58be384a667..04c5e824163 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/EditHashicorpVaultModal.tsx @@ -4,8 +4,21 @@ import { useHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides import { useUpdateHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import NotificationManager from "@/components/molecules/notifications_manager"; -import { Button, Divider, Form, Input, Modal, Space, Typography } from "antd"; -import React, { useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Eye, EyeOff } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; import { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants"; interface FieldGroup { @@ -17,16 +30,23 @@ interface FieldGroup { const FIELD_GROUPS: FieldGroup[] = [ { title: "Connection", - fields: ["vault_addr", "vault_namespace", "vault_mount_name", "vault_path_prefix"], + fields: [ + "vault_addr", + "vault_namespace", + "vault_mount_name", + "vault_path_prefix", + ], }, { title: "Token Authentication", - subtitle: "Use a Vault token to authenticate. Only one auth method is required.", + subtitle: + "Use a Vault token to authenticate. Only one auth method is required.", fields: ["vault_token"], }, { title: "AppRole Authentication", - subtitle: "Use AppRole credentials to authenticate. Only one auth method is required.", + subtitle: + "Use AppRole credentials to authenticate. Only one auth method is required.", fields: ["approle_role_id", "approle_secret_id", "approle_mount_path"], }, { @@ -42,60 +62,64 @@ interface EditHashicorpVaultModalProps { onSuccess: () => void; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type FormValues = Record; + const EditHashicorpVaultModal: React.FC = ({ isVisible, onCancel, onSuccess, }) => { - const [form] = Form.useForm(); const { accessToken } = useAuthorized(); const { data } = useHashicorpVaultConfig(); const { mutate, isPending } = useUpdateHashicorpVaultConfig(accessToken); + const form = useForm({ mode: "onSubmit" }); + const [showSensitive, setShowSensitive] = useState>( + {}, + ); - const schema = data?.field_schema; - const properties = schema?.properties ?? {}; - const rawValues = data?.values ?? {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const schema: any = data?.field_schema; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const properties: Record = schema?.properties ?? {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rawValues: Record = data?.values ?? {}; useEffect(() => { if (isVisible && data) { - form.resetFields(); - // Only set non-sensitive fields — sensitive ones show as placeholders - const formValues: Record = {}; + const formValues: FormValues = {}; for (const [key, value] of Object.entries(rawValues)) { - if (!SENSITIVE_FIELDS.has(key)) { - formValues[key] = value; - } + if (!SENSITIVE_FIELDS.has(key)) formValues[key] = value; } - form.setFieldsValue(formValues); + form.reset(formValues); } - }, [isVisible, data, form]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible, data]); - const handleSubmit = (formValues: Record) => { - const config: Record = {}; + const handleSubmit = form.handleSubmit((formValues) => { + const config: FormValues = {}; for (const [key, value] of Object.entries(formValues)) { if (value !== undefined && value !== null && value !== "") { - // Non-empty value → update config[key] = value; } else if (!SENSITIVE_FIELDS.has(key)) { - // Non-sensitive field cleared → send "" to clear it on the backend config[key] = ""; } - // Sensitive field left blank → omit from payload (keep existing) } - mutate(config, { onSuccess: () => { - NotificationManager.success("Hashicorp Vault configuration updated successfully"); + NotificationManager.success( + "Hashicorp Vault configuration updated successfully", + ); onSuccess(); }, onError: (err) => { NotificationManager.fromBackend(err); }, }); - }; + }); const handleCancel = () => { - form.resetFields(); + form.reset(); onCancel(); }; @@ -103,68 +127,118 @@ const EditHashicorpVaultModal: React.FC = ({ const fieldSchema = properties[fieldName]; if (!fieldSchema) return null; - const rules = - fieldName === "vault_addr" - ? [{ pattern: /^https?:\/\/.+/, message: "Must start with http:// or https://" }] - : undefined; - const isSensitive = SENSITIVE_FIELDS.has(fieldName); const existingValue = rawValues[fieldName]; - const hasExistingValue = isSensitive && existingValue != null && existingValue !== ""; + const hasExistingValue = + isSensitive && existingValue != null && existingValue !== ""; const placeholder = hasExistingValue ? `Leave blank to keep existing (${existingValue})` : fieldSchema?.description; + const validators = + fieldName === "vault_addr" + ? { + pattern: { + value: /^https?:\/\/.+/, + message: "Must start with http:// or https://", + }, + } + : {}; + return ( - +
+ {isSensitive ? ( - +
+ + +
) : ( - + )} - + {form.formState.errors[fieldName] && ( +

+ {form.formState.errors[fieldName]?.message as string} +

+ )} +
); }; return ( - - - - - } - onCancel={handleCancel} + onOpenChange={(o) => (!o ? handleCancel() : undefined)} > -
- {FIELD_GROUPS.map((group, index) => ( -
- {index > 0 && } - - {group.title} - - {group.subtitle && ( - - {group.subtitle} - - )} - {group.fields.map(renderField)} -
- ))} -
-
+ + + Edit Hashicorp Vault Configuration + + Configure Hashicorp Vault credentials and connection details. + + + +
+ {FIELD_GROUPS.map((group, index) => ( +
+ {index > 0 && } +
+ {group.title} +
+ {group.subtitle && ( +

+ {group.subtitle} +

+ )} + {group.fields.map(renderField)} +
+ ))} + + + + +
+
+
+ ); }; diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx index 33a2c9e22b5..c431c7db9bb 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVault.tsx @@ -8,14 +8,20 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import NotificationManager from "@/components/molecules/notifications_manager"; import { testHashicorpVaultConnection } from "@/app/(dashboard)/hooks/configOverrides/hashicorpVaultApi"; -import { Alert, Button, Card, Descriptions, Flex, Skeleton, Space, Typography } from "antd"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; import { Edit, KeyRound, PlugZap, Trash2 } from "lucide-react"; import { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants"; import EditHashicorpVaultModal from "./EditHashicorpVaultModal"; import HashicorpVaultEmptyPlaceholder from "./HashicorpVaultEmptyPlaceholder"; -const { Title, Text } = Typography; - +// eslint-disable-next-line @typescript-eslint/no-explicit-any function detectAuthMethod(values: Record): string { if (values.approle_role_id || values.approle_secret_id) return "AppRole"; if (values.client_cert && values.client_key) return "TLS Certificate"; @@ -23,22 +29,21 @@ function detectAuthMethod(values: Record): string { return "None"; } -const descriptionsConfig = { - column: { xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }, -}; - export default function HashicorpVault() { const { accessToken } = useAuthorized(); const { data, isLoading, isError, error } = useHashicorpVaultConfig(); - const { mutate: deleteConfig, isPending: isDeleting } = useDeleteHashicorpVaultConfig(accessToken); - const { mutate: updateConfig, isPending: isClearingField } = useUpdateHashicorpVaultConfig(accessToken); + const { mutate: deleteConfig, isPending: isDeleting } = + useDeleteHashicorpVaultConfig(accessToken); + const { mutate: updateConfig, isPending: isClearingField } = + useUpdateHashicorpVaultConfig(accessToken); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [clearingField, setClearingField] = useState(null); const [isTesting, setIsTesting] = useState(false); - const rawValues = data?.values ?? {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rawValues: Record = data?.values ?? {}; const isConfigured = Boolean(rawValues.vault_addr); const handleTestConnection = async () => { @@ -46,7 +51,9 @@ export default function HashicorpVault() { setIsTesting(true); try { const result = await testHashicorpVaultConnection(accessToken); - NotificationManager.success(result.message || "Connection to Vault successful!"); + NotificationManager.success( + result.message || "Connection to Vault successful!", + ); } catch (err) { NotificationManager.fromBackend(err); } finally { @@ -57,7 +64,9 @@ export default function HashicorpVault() { const handleDelete = () => { deleteConfig(undefined, { onSuccess: () => { - NotificationManager.success("Hashicorp Vault configuration deleted"); + NotificationManager.success( + "Hashicorp Vault configuration deleted", + ); setIsDeleteModalOpen(false); }, onError: (err) => { @@ -68,142 +77,167 @@ export default function HashicorpVault() { const handleClearField = () => { if (!clearingField) return; - updateConfig({ [clearingField]: "" }, { - onSuccess: () => { - NotificationManager.success(`${FIELD_LABELS[clearingField] ?? clearingField} cleared`); - setClearingField(null); + updateConfig( + { [clearingField]: "" }, + { + onSuccess: () => { + NotificationManager.success( + `${FIELD_LABELS[clearingField] ?? clearingField} cleared`, + ); + setClearingField(null); + }, + onError: (err) => { + NotificationManager.fromBackend(err); + }, }, - onError: (err) => { - NotificationManager.fromBackend(err); - }, - }); + ); }; const renderValue = (key: string) => { const value = rawValues[key]; if (!value) { - return Not configured; + return ( + Not configured + ); } if (SENSITIVE_FIELDS.has(key)) { return ( - - {value} +
+ {value} +
); } - return {value}; + return {value}; }; const renderSettings = () => { - // Only show fields that have values, plus auth method const fieldsToShow = Object.entries(rawValues).filter( - ([_, value]) => value != null && value !== "" + ([, value]) => value != null && value !== "", ); - if (fieldsToShow.length === 0) return null; - return ( - - - {detectAuthMethod(rawValues)} - - {fieldsToShow.map(([key]) => ( - - {renderValue(key)} - +
+
+
Auth Method
+
{detectAuthMethod(rawValues)}
+
+ {fieldsToShow.map(([key], i) => ( +
+
+ {FIELD_LABELS[key] ?? key} +
+
{renderValue(key)}
+
))} - +
); }; return ( <> {isLoading ? ( - - + + ) : isError ? ( - - + + + Could not load Hashicorp Vault configuration + {error instanceof Error && ( + {error.message} + )} + ) : ( - - - {/* Header */} - - - + +
+
+
+
- Hashicorp Vault - Manage secret manager configuration +

+ Hashicorp Vault +

+

+ Manage secret manager configuration +

- +
- +
{isConfigured && ( <> )} - - +
+
{isConfigured && ( - - vault kv put secret/SECRET_NAME key=secret_value -
- - View documentation - - - } - /> + + + Secrets must be stored with the field name "key" + + + + vault kv put secret/SECRET_NAME key=secret_value + +
+ + View documentation + +
+
)} {isConfigured ? ( renderSettings() ) : ( - setIsEditModalVisible(true)} /> + setIsEditModalVisible(true)} + /> )} - +
)} @@ -232,7 +266,12 @@ export default function HashicorpVault() { message="This will remove the stored value." resourceInformationTitle="Field" resourceInformation={[ - { label: "Field", value: clearingField ? (FIELD_LABELS[clearingField] ?? clearingField) : "" }, + { + label: "Field", + value: clearingField + ? (FIELD_LABELS[clearingField] ?? clearingField) + : "", + }, ]} onCancel={() => setClearingField(null)} onOk={handleClearField} diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx index 49860fc7617..6a2f53f6bcd 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/HashicorpVault/HashicorpVaultEmptyPlaceholder.tsx @@ -1,30 +1,28 @@ -import { Empty, Typography, Button } from "antd"; - -const { Title, Paragraph } = Typography; +import { Button } from "@/components/ui/button"; +import { Inbox } from "lucide-react"; interface HashicorpVaultEmptyPlaceholderProps { onAdd: () => void; } -export default function HashicorpVaultEmptyPlaceholder({ onAdd }: HashicorpVaultEmptyPlaceholderProps) { +export default function HashicorpVaultEmptyPlaceholder({ + onAdd, +}: HashicorpVaultEmptyPlaceholderProps) { return ( -
- - No Vault Configuration Found - - Configure Hashicorp Vault to securely manage provider API keys and secrets - for your LiteLLM deployment. - -
- } - > - - + ); }