mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Merge pull request #23036 from BerriAI/litellm_hashicorp_vault_frontend
Litellm hashicorp vault frontend
This commit is contained in:
commit
fe1ee4b039
10 changed files with 617 additions and 0 deletions
|
|
@ -0,0 +1,86 @@
|
|||
import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage } from "@/components/networking";
|
||||
|
||||
export const getHashicorpVaultConfig = async (accessToken: string) => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/config_overrides/hashicorp_vault`
|
||||
: `/config_overrides/hashicorp_vault`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateHashicorpVaultConfig = async (
|
||||
accessToken: string,
|
||||
config: Record<string, any>,
|
||||
) => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/config_overrides/hashicorp_vault`
|
||||
: `/config_overrides/hashicorp_vault`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteHashicorpVaultConfig = async (accessToken: string) => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/config_overrides/hashicorp_vault`
|
||||
: `/config_overrides/hashicorp_vault`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
export const testHashicorpVaultConnection = async (accessToken: string) => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/config_overrides/hashicorp_vault/test_connection`
|
||||
: `/config_overrides/hashicorp_vault/test_connection`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { deleteHashicorpVaultConfig } from "./hashicorpVaultApi";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { hashicorpVaultKeys } from "./useHashicorpVaultConfig";
|
||||
|
||||
export const useDeleteHashicorpVaultConfig = (accessToken: string | null) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return deleteHashicorpVaultConfig(accessToken);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: hashicorpVaultKeys.all });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { getHashicorpVaultConfig } from "./hashicorpVaultApi";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import useAuthorized from "../useAuthorized";
|
||||
import { createQueryKeys } from "../common/queryKeysFactory";
|
||||
|
||||
export const hashicorpVaultKeys = createQueryKeys("hashicorpVaultConfig");
|
||||
|
||||
export const useHashicorpVaultConfig = () => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useQuery<Record<string, any>>({
|
||||
queryKey: hashicorpVaultKeys.list({}),
|
||||
queryFn: async () => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return getHashicorpVaultConfig(accessToken);
|
||||
},
|
||||
enabled: !!accessToken,
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: 60 * 60 * 1000,
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { updateHashicorpVaultConfig } from "./hashicorpVaultApi";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { hashicorpVaultKeys } from "./useHashicorpVaultConfig";
|
||||
|
||||
export const useUpdateHashicorpVaultConfig = (accessToken: string | null) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (config: Record<string, any>) => {
|
||||
if (!accessToken) {
|
||||
throw new Error("Access token is required");
|
||||
}
|
||||
return updateHashicorpVaultConfig(accessToken, config);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: hashicorpVaultKeys.all });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -23,6 +23,7 @@ import { addAllowedIP, deleteAllowedIP, getAllowedIPs, getSSOSettings } from "./
|
|||
import SCIMConfig from "./SCIM";
|
||||
import SSOSettings from "./Settings/AdminSettings/SSOSettings/SSOSettings";
|
||||
import UISettings from "./Settings/AdminSettings/UISettings/UISettings";
|
||||
import HashicorpVault from "./Settings/AdminSettings/HashicorpVault/HashicorpVault";
|
||||
import SSOModals from "./SSOModals";
|
||||
import UIAccessControlForm from "./UIAccessControlForm";
|
||||
|
||||
|
|
@ -361,6 +362,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
|
|||
),
|
||||
children: <UISettings />,
|
||||
},
|
||||
{
|
||||
key: "hashicorp-vault",
|
||||
label: "Hashicorp Vault",
|
||||
children: <HashicorpVault />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
"use client";
|
||||
|
||||
import { useHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig";
|
||||
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 { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants";
|
||||
|
||||
interface FieldGroup {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
fields: string[];
|
||||
}
|
||||
|
||||
const FIELD_GROUPS: FieldGroup[] = [
|
||||
{
|
||||
title: "Connection",
|
||||
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.",
|
||||
fields: ["vault_token"],
|
||||
},
|
||||
{
|
||||
title: "AppRole Authentication",
|
||||
subtitle: "Use AppRole credentials to authenticate. Only one auth method is required.",
|
||||
fields: ["approle_role_id", "approle_secret_id", "approle_mount_path"],
|
||||
},
|
||||
{
|
||||
title: "TLS",
|
||||
subtitle: "Optional client certificate for mTLS.",
|
||||
fields: ["client_cert", "client_key", "vault_cert_role"],
|
||||
},
|
||||
];
|
||||
|
||||
interface EditHashicorpVaultModalProps {
|
||||
isVisible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const EditHashicorpVaultModal: React.FC<EditHashicorpVaultModalProps> = ({
|
||||
isVisible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const { accessToken } = useAuthorized();
|
||||
const { data } = useHashicorpVaultConfig();
|
||||
const { mutate, isPending } = useUpdateHashicorpVaultConfig(accessToken);
|
||||
|
||||
const schema = data?.field_schema;
|
||||
const properties = schema?.properties ?? {};
|
||||
const rawValues = data?.values ?? {};
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && data) {
|
||||
form.resetFields();
|
||||
// Only set non-sensitive fields — sensitive ones show as placeholders
|
||||
const formValues: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(rawValues)) {
|
||||
if (!SENSITIVE_FIELDS.has(key)) {
|
||||
formValues[key] = value;
|
||||
}
|
||||
}
|
||||
form.setFieldsValue(formValues);
|
||||
}
|
||||
}, [isVisible, data, form]);
|
||||
|
||||
const handleSubmit = (formValues: Record<string, any>) => {
|
||||
const config: Record<string, any> = {};
|
||||
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");
|
||||
onSuccess();
|
||||
},
|
||||
onError: (err) => {
|
||||
NotificationManager.fromBackend(err);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const renderField = (fieldName: string) => {
|
||||
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 placeholder = hasExistingValue
|
||||
? `Leave blank to keep existing (${existingValue})`
|
||||
: fieldSchema?.description;
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={fieldName}
|
||||
name={fieldName}
|
||||
label={FIELD_LABELS[fieldName] ?? fieldName}
|
||||
rules={rules}
|
||||
>
|
||||
{isSensitive ? (
|
||||
<Input.Password placeholder={placeholder} />
|
||||
) : (
|
||||
<Input placeholder={fieldSchema?.description} />
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Edit Hashicorp Vault Configuration"
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditHashicorpVaultModal;
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useHashicorpVaultConfig";
|
||||
import { useDeleteHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useDeleteHashicorpVaultConfig";
|
||||
import { useUpdateHashicorpVaultConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateHashicorpVaultConfig";
|
||||
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 { 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;
|
||||
|
||||
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";
|
||||
if (values.vault_token) return "Token";
|
||||
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 [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 ?? {};
|
||||
const isConfigured = Boolean(rawValues.vault_addr);
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!accessToken) return;
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const result = await testHashicorpVaultConnection(accessToken);
|
||||
NotificationManager.success(result.message || "Connection to Vault successful!");
|
||||
} catch (err) {
|
||||
NotificationManager.fromBackend(err);
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteConfig(undefined, {
|
||||
onSuccess: () => {
|
||||
NotificationManager.success("Hashicorp Vault configuration deleted");
|
||||
setIsDeleteModalOpen(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
NotificationManager.fromBackend(err);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearField = () => {
|
||||
if (!clearingField) return;
|
||||
updateConfig({ [clearingField]: "" }, {
|
||||
onSuccess: () => {
|
||||
NotificationManager.success(`${FIELD_LABELS[clearingField] ?? clearingField} cleared`);
|
||||
setClearingField(null);
|
||||
},
|
||||
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>;
|
||||
}
|
||||
if (SENSITIVE_FIELDS.has(key)) {
|
||||
return (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text className="font-mono text-gray-600">{value}</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<Trash2 className="w-3.5 h-3.5" />}
|
||||
onClick={() => setClearingField(key)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <Text className="font-mono text-gray-600">{value}</Text>;
|
||||
};
|
||||
|
||||
const renderSettings = () => {
|
||||
// Only show fields that have values, plus auth method
|
||||
const fieldsToShow = Object.entries(rawValues).filter(
|
||||
([_, 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>
|
||||
))}
|
||||
</Descriptions>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Card>
|
||||
<Skeleton active />
|
||||
</Card>
|
||||
) : isError ? (
|
||||
<Card>
|
||||
<Alert
|
||||
type="error"
|
||||
message="Could not load Hashicorp Vault configuration"
|
||||
description={error instanceof Error ? error.message : undefined}
|
||||
/>
|
||||
</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" />
|
||||
<div>
|
||||
<Title level={3} style={{ marginBottom: 0 }}>Hashicorp Vault</Title>
|
||||
<Text type="secondary">Manage secret manager configuration</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<Space>
|
||||
{isConfigured && (
|
||||
<>
|
||||
<Button
|
||||
icon={<PlugZap className="w-4 h-4" />}
|
||||
loading={isTesting}
|
||||
onClick={handleTestConnection}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button
|
||||
icon={<Edit className="w-4 h-4" />}
|
||||
onClick={() => setIsEditModalVisible(true)}
|
||||
>
|
||||
Edit Configuration
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
>
|
||||
Delete Configuration
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
{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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isConfigured ? (
|
||||
renderSettings()
|
||||
) : (
|
||||
<HashicorpVaultEmptyPlaceholder onAdd={() => setIsEditModalVisible(true)} />
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<EditHashicorpVaultModal
|
||||
isVisible={isEditModalVisible}
|
||||
onCancel={() => setIsEditModalVisible(false)}
|
||||
onSuccess={() => setIsEditModalVisible(false)}
|
||||
/>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Hashicorp Vault Configuration?"
|
||||
message="Models using Vault secrets will lose access to their API keys until a new configuration is saved."
|
||||
resourceInformationTitle="Vault Configuration"
|
||||
resourceInformation={[
|
||||
{ label: "Vault Address", value: rawValues.vault_addr },
|
||||
]}
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
onOk={handleDelete}
|
||||
confirmLoading={isDeleting}
|
||||
/>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={clearingField !== null}
|
||||
title={`Clear ${clearingField ? (FIELD_LABELS[clearingField] ?? clearingField) : ""}?`}
|
||||
message="This will remove the stored value."
|
||||
resourceInformationTitle="Field"
|
||||
resourceInformation={[
|
||||
{ label: "Field", value: clearingField ? (FIELD_LABELS[clearingField] ?? clearingField) : "" },
|
||||
]}
|
||||
onCancel={() => setClearingField(null)}
|
||||
onOk={handleClearField}
|
||||
confirmLoading={isClearingField}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Empty, Typography, Button } from "antd";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
interface HashicorpVaultEmptyPlaceholderProps {
|
||||
onAdd: () => void;
|
||||
}
|
||||
|
||||
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">
|
||||
Configure Vault
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
export const SENSITIVE_FIELDS = new Set([
|
||||
"vault_token",
|
||||
"approle_secret_id",
|
||||
"client_key",
|
||||
]);
|
||||
|
||||
export const FIELD_LABELS: Record<string, string> = {
|
||||
vault_addr: "Vault Address",
|
||||
vault_namespace: "Namespace",
|
||||
vault_mount_name: "KV Mount Name",
|
||||
vault_path_prefix: "Path Prefix",
|
||||
vault_token: "Token",
|
||||
approle_role_id: "Role ID",
|
||||
approle_secret_id: "Secret ID",
|
||||
approle_mount_path: "Mount Path",
|
||||
client_cert: "Client Certificate",
|
||||
client_key: "Client Key",
|
||||
vault_cert_role: "Certificate Role",
|
||||
};
|
||||
|
|
@ -8880,6 +8880,7 @@ export const updateUiSettings = async (accessToken: string, settings: Record<str
|
|||
return data;
|
||||
};
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Claude Code Marketplace Networking Functions
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue