mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate HashicorpVault admin-settings subtree to shadcn
- HashicorpVaultEmptyPlaceholder: antd Empty/Typography/Button \u2192 plain Tailwind empty-state with shadcn Button + lucide Inbox icon. - EditHashicorpVaultModal: antd Modal/Form/Input/Input.Password/Divider/ Typography \u2192 shadcn Dialog + react-hook-form (mode='onSubmit') + Input/Label/Separator + lucide Eye/EyeOff for sensitive-field toggles. Sensitive-field placeholders preserve the existing-value hint pattern. - HashicorpVault: antd Alert/Card/Descriptions/Flex/Skeleton/Space/ Typography \u2192 shadcn Alert/Card/Skeleton + plain Tailwind <dl> grid + lucide icons. Two DeleteResourceModal call sites preserved (whole-config delete and per-field clear). Auth-method detection helper unchanged. Gates: TS \u2713 | Vitest 3/3 \u2713 (HashicorpVaultEmptyPlaceholder.test.tsx existing) | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
4d82b4a84a
commit
de34bbda12
3 changed files with 291 additions and 180 deletions
|
|
@ -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<string, any>;
|
||||
|
||||
const EditHashicorpVaultModal: React.FC<EditHashicorpVaultModalProps> = ({
|
||||
isVisible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const { accessToken } = useAuthorized();
|
||||
const { data } = useHashicorpVaultConfig();
|
||||
const { mutate, isPending } = useUpdateHashicorpVaultConfig(accessToken);
|
||||
const form = useForm<FormValues>({ mode: "onSubmit" });
|
||||
const [showSensitive, setShowSensitive] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
|
||||
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<string, any> = schema?.properties ?? {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rawValues: Record<string, any> = data?.values ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && data) {
|
||||
form.resetFields();
|
||||
// Only set non-sensitive fields — sensitive ones show as placeholders
|
||||
const formValues: Record<string, any> = {};
|
||||
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<string, any>) => {
|
||||
const config: Record<string, any> = {};
|
||||
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<EditHashicorpVaultModalProps> = ({
|
|||
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 (
|
||||
<Form.Item
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
label={FIELD_LABELS[fieldName] ?? fieldName}
|
||||
rules={rules}
|
||||
>
|
||||
<div key={fieldName} className="space-y-2 mb-4">
|
||||
<Label htmlFor={fieldName}>
|
||||
{FIELD_LABELS[fieldName] ?? fieldName}
|
||||
</Label>
|
||||
{isSensitive ? (
|
||||
<Input.Password placeholder={placeholder} />
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={fieldName}
|
||||
type={showSensitive[fieldName] ? "text" : "password"}
|
||||
placeholder={placeholder}
|
||||
{...form.register(fieldName, validators)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setShowSensitive((prev) => ({
|
||||
...prev,
|
||||
[fieldName]: !prev[fieldName],
|
||||
}))
|
||||
}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
aria-label={
|
||||
showSensitive[fieldName] ? "Hide value" : "Show value"
|
||||
}
|
||||
>
|
||||
{showSensitive[fieldName] ? (
|
||||
<EyeOff size={14} />
|
||||
) : (
|
||||
<Eye size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Input placeholder={fieldSchema?.description} />
|
||||
<Input
|
||||
id={fieldName}
|
||||
placeholder={fieldSchema?.description}
|
||||
{...form.register(fieldName, validators)}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
{form.formState.errors[fieldName] && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors[fieldName]?.message as string}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Edit Hashicorp Vault Configuration"
|
||||
<Dialog
|
||||
open={isVisible}
|
||||
width={700}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={handleCancel} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" loading={isPending} onClick={() => form.submit()}>
|
||||
{isPending ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(o) => (!o ? handleCancel() : undefined)}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
{FIELD_GROUPS.map((group, index) => (
|
||||
<div key={group.title}>
|
||||
{index > 0 && <Divider />}
|
||||
<Typography.Title level={5} style={{ marginBottom: 4 }}>
|
||||
{group.title}
|
||||
</Typography.Title>
|
||||
{group.subtitle && (
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 16 }}>
|
||||
{group.subtitle}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
{group.fields.map(renderField)}
|
||||
</div>
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Hashicorp Vault Configuration</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Configure Hashicorp Vault credentials and connection details.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
{FIELD_GROUPS.map((group, index) => (
|
||||
<div key={group.title}>
|
||||
{index > 0 && <Separator className="my-4" />}
|
||||
<h5 className="text-base font-semibold mb-1">
|
||||
{group.title}
|
||||
</h5>
|
||||
{group.subtitle && (
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{group.subtitle}
|
||||
</p>
|
||||
)}
|
||||
{group.fields.map(renderField)}
|
||||
</div>
|
||||
))}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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, any>): 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, any>): 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<string | null>(null);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
|
||||
const rawValues = data?.values ?? {};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rawValues: Record<string, any> = 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 <span className="text-gray-400 italic">Not configured</span>;
|
||||
return (
|
||||
<span className="text-muted-foreground italic">Not configured</span>
|
||||
);
|
||||
}
|
||||
if (SENSITIVE_FIELDS.has(key)) {
|
||||
return (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text className="font-mono text-gray-600">{value}</Text>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-mono text-muted-foreground">{value}</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<Trash2 className="w-3.5 h-3.5" />}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:text-destructive"
|
||||
onClick={() => setClearingField(key)}
|
||||
/>
|
||||
</Flex>
|
||||
aria-label={`Clear ${FIELD_LABELS[key] ?? key}`}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Text className="font-mono text-gray-600">{value}</Text>;
|
||||
return <span className="font-mono text-muted-foreground">{value}</span>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Descriptions bordered {...descriptionsConfig}>
|
||||
<Descriptions.Item label="Auth Method">
|
||||
<Text>{detectAuthMethod(rawValues)}</Text>
|
||||
</Descriptions.Item>
|
||||
{fieldsToShow.map(([key]) => (
|
||||
<Descriptions.Item key={key} label={FIELD_LABELS[key] ?? key}>
|
||||
{renderValue(key)}
|
||||
</Descriptions.Item>
|
||||
<dl className="border border-border rounded-md overflow-hidden text-sm">
|
||||
<div className="grid grid-cols-[200px_1fr] border-b border-border">
|
||||
<dt className="bg-muted px-4 py-3 font-medium">Auth Method</dt>
|
||||
<dd className="px-4 py-3">{detectAuthMethod(rawValues)}</dd>
|
||||
</div>
|
||||
{fieldsToShow.map(([key], i) => (
|
||||
<div
|
||||
key={key}
|
||||
className={`grid grid-cols-[200px_1fr] ${
|
||||
i < fieldsToShow.length - 1 ? "border-b border-border" : ""
|
||||
}`}
|
||||
>
|
||||
<dt className="bg-muted px-4 py-3 font-medium">
|
||||
{FIELD_LABELS[key] ?? key}
|
||||
</dt>
|
||||
<dd className="px-4 py-3">{renderValue(key)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</Descriptions>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<Skeleton active />
|
||||
<Card className="p-6">
|
||||
<Skeleton className="h-32 w-full" />
|
||||
</Card>
|
||||
) : isError ? (
|
||||
<Card>
|
||||
<Alert
|
||||
type="error"
|
||||
message="Could not load Hashicorp Vault configuration"
|
||||
description={error instanceof Error ? error.message : undefined}
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Could not load Hashicorp Vault configuration</AlertTitle>
|
||||
{error instanceof Error && (
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
)}
|
||||
</Alert>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Space direction="vertical" size="large" className="w-full">
|
||||
{/* Header */}
|
||||
<Flex justify="space-between" align="center">
|
||||
<Flex align="center" gap={12}>
|
||||
<KeyRound className="w-6 h-6 text-gray-400" />
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6 w-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<KeyRound className="w-6 h-6 text-muted-foreground" />
|
||||
<div>
|
||||
<Title level={3} style={{ marginBottom: 0 }}>Hashicorp Vault</Title>
|
||||
<Text type="secondary">Manage secret manager configuration</Text>
|
||||
<h3 className="text-lg font-semibold m-0">
|
||||
Hashicorp Vault
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage secret manager configuration
|
||||
</p>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
<div className="flex gap-2">
|
||||
{isConfigured && (
|
||||
<>
|
||||
<Button
|
||||
icon={<PlugZap className="w-4 h-4" />}
|
||||
loading={isTesting}
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={isTesting}
|
||||
>
|
||||
Test Connection
|
||||
<PlugZap className="w-4 h-4" />
|
||||
{isTesting ? "Testing…" : "Test Connection"}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Edit className="w-4 h-4" />}
|
||||
variant="outline"
|
||||
onClick={() => setIsEditModalVisible(true)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
Edit Configuration
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
variant="destructive"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Delete Configuration
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isConfigured && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={'Secrets must be stored with the field name "key"'}
|
||||
description={
|
||||
<>
|
||||
<Text code>vault kv put secret/SECRET_NAME key=secret_value</Text>
|
||||
<br />
|
||||
<Typography.Link
|
||||
href="https://docs.litellm.ai/docs/secret_managers/hashicorp_vault"
|
||||
target="_blank"
|
||||
>
|
||||
View documentation
|
||||
</Typography.Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Alert>
|
||||
<AlertTitle>
|
||||
Secrets must be stored with the field name "key"
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-xs">
|
||||
vault kv put secret/SECRET_NAME key=secret_value
|
||||
</code>
|
||||
<br />
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/secret_managers/hashicorp_vault"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
View documentation
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isConfigured ? (
|
||||
renderSettings()
|
||||
) : (
|
||||
<HashicorpVaultEmptyPlaceholder onAdd={() => setIsEditModalVisible(true)} />
|
||||
<HashicorpVaultEmptyPlaceholder
|
||||
onAdd={() => setIsEditModalVisible(true)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="bg-white p-12 rounded-lg border border-dashed border-gray-300 text-center w-full">
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description={
|
||||
<div className="space-y-2">
|
||||
<Title level={4}>No Vault Configuration Found</Title>
|
||||
<Paragraph type="secondary" className="max-w-md mx-auto">
|
||||
Configure Hashicorp Vault to securely manage provider API keys and secrets
|
||||
for your LiteLLM deployment.
|
||||
</Paragraph>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button type="primary" size="large" onClick={onAdd} className="flex items-center gap-2 mx-auto mt-4">
|
||||
<div className="bg-background p-12 rounded-lg border border-dashed border-border text-center w-full">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Inbox className="h-12 w-12 text-muted-foreground" />
|
||||
<h4 className="text-lg font-semibold">
|
||||
No Vault Configuration Found
|
||||
</h4>
|
||||
<p className="text-sm text-muted-foreground max-w-md mx-auto">
|
||||
Configure Hashicorp Vault to securely manage provider API keys and
|
||||
secrets for your LiteLLM deployment.
|
||||
</p>
|
||||
<Button onClick={onAdd} className="mt-4">
|
||||
Configure Vault
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue