mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate key_value_input + email_settings to shadcn
- key_value_input: antd Button/Space + @tremor TextInput + @ant-design/icons MinusCircle/Plus \u2192 shadcn Input/Button + lucide MinusCircle/Plus. Plain Tailwind flex layout replaces antd Space. - email_settings: @tremor Card/Text/Grid/Button/TextInput/TableCell + antd Typography \u2192 shadcn Card/Input/Label/Button + plain Tailwind grid. Inline help text uses semantic muted-foreground / destructive tokens. Inputs preserve type=password for sensitive SMTP fields. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
6c315e702f
commit
d553000e66
2 changed files with 162 additions and 111 deletions
|
|
@ -1,44 +1,44 @@
|
|||
import React from "react";
|
||||
import { Card, Text, Grid, Button, TextInput, TableCell } from "@tremor/react";
|
||||
import { Typography } from "antd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import NotificationManager from "./molecules/notifications_manager";
|
||||
import { serviceHealthCheck, setCallbacksCall } from "./networking";
|
||||
import { EmailEventSettings } from "./email_events";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
interface EmailSettingsProps {
|
||||
accessToken: string | null;
|
||||
premiumUser: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
alerts: any[];
|
||||
}
|
||||
|
||||
const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser, alerts }) => {
|
||||
const EmailSettings: React.FC<EmailSettingsProps> = ({
|
||||
accessToken,
|
||||
premiumUser,
|
||||
alerts,
|
||||
}) => {
|
||||
const handleSaveEmailSettings = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
if (!accessToken) return;
|
||||
|
||||
let updatedVariables: Record<string, string> = {};
|
||||
const updatedVariables: Record<string, string> = {};
|
||||
|
||||
alerts
|
||||
.filter((alert) => alert.name === "email")
|
||||
.forEach((alert) => {
|
||||
Object.entries(alert.variables ?? {}).forEach(([key, value]) => {
|
||||
const inputElement = document.querySelector(`input[name="${key}"]`) as HTMLInputElement;
|
||||
Object.entries(alert.variables ?? {}).forEach(([key]) => {
|
||||
const inputElement = document.querySelector(
|
||||
`input[name="${key}"]`,
|
||||
) as HTMLInputElement;
|
||||
if (inputElement && inputElement.value) {
|
||||
updatedVariables[key] = inputElement?.value;
|
||||
updatedVariables[key] = inputElement.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("updatedVariables", updatedVariables);
|
||||
//filter out null / undefined values for updatedVariables
|
||||
|
||||
const payload = {
|
||||
general_settings: {
|
||||
alerting: ["email"],
|
||||
},
|
||||
general_settings: { alerting: ["email"] },
|
||||
environment_variables: updatedVariables,
|
||||
};
|
||||
try {
|
||||
|
|
@ -54,125 +54,154 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
|
|||
<div className="mt-6 mb-6">
|
||||
<EmailEventSettings accessToken={accessToken} />
|
||||
</div>
|
||||
<Card>
|
||||
<Title level={4}>Email Server Settings</Title>
|
||||
<Text>
|
||||
<a href="https://docs.litellm.ai/docs/proxy/email" target="_blank" style={{ color: "blue" }}>
|
||||
{" "}
|
||||
<Card className="p-6">
|
||||
<h4 className="text-lg font-semibold m-0">Email Server Settings</h4>
|
||||
<p className="text-sm">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/email"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
LiteLLM Docs: email alerts
|
||||
</a>{" "}
|
||||
<br />
|
||||
</Text>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div className="flex w-full">
|
||||
<div className="flex w-full mt-4">
|
||||
{alerts
|
||||
.filter((alert) => alert.name === "email")
|
||||
.map((alert, index) => (
|
||||
<TableCell key={index}>
|
||||
<ul>
|
||||
<Grid numItems={2}>
|
||||
{Object.entries(alert.variables ?? {}).map(([key, value]) => (
|
||||
<li key={key} className="mx-2 my-2">
|
||||
{premiumUser != true && (key === "EMAIL_LOGO_URL" || key === "EMAIL_SUPPORT_CONTACT") ? (
|
||||
<div key={index} className="w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4">
|
||||
{Object.entries(alert.variables ?? {}).map(
|
||||
([key, value]) => (
|
||||
<div key={key} className="mx-2 my-2 space-y-1">
|
||||
{premiumUser !== true &&
|
||||
(key === "EMAIL_LOGO_URL" ||
|
||||
key === "EMAIL_SUPPORT_CONTACT") ? (
|
||||
<div>
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
<Text className="mt-2"> ✨ {key}</Text>
|
||||
<a
|
||||
href="https://forms.gle/W3U4PZpJGFHWtHyA9"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Label className="mt-2">✨ {key}</Label>
|
||||
</a>
|
||||
<TextInput
|
||||
<Input
|
||||
name={key}
|
||||
defaultValue={value as string}
|
||||
type="password"
|
||||
disabled={true}
|
||||
style={{ width: "400px" }}
|
||||
disabled
|
||||
className="w-full max-w-[400px]"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Text className="mt-2">{key}</Text>
|
||||
<TextInput
|
||||
<Label className="mt-2">{key}</Label>
|
||||
<Input
|
||||
name={key}
|
||||
defaultValue={value as string}
|
||||
type="password"
|
||||
style={{ width: "400px" }}
|
||||
className="w-full max-w-[400px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Added descriptions for input fields */}
|
||||
<p style={{ fontSize: "small", fontStyle: "italic" }}>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
{key === "SMTP_HOST" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP host address, e.g. `smtp.resend.com`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
<span>
|
||||
Enter the SMTP host address, e.g.{" "}
|
||||
<code>smtp.resend.com</code>
|
||||
<span className="text-destructive">
|
||||
{" "}
|
||||
Required *
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{key === "SMTP_PORT" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP port number, e.g. `587`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
<span>
|
||||
Enter the SMTP port number, e.g. <code>587</code>
|
||||
<span className="text-destructive">
|
||||
{" "}
|
||||
Required *
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{key === "SMTP_USERNAME" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP username, e.g. `username`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
<span>
|
||||
Enter the SMTP username, e.g. <code>username</code>
|
||||
<span className="text-destructive">
|
||||
{" "}
|
||||
Required *
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{key === "SMTP_PASSWORD" && (
|
||||
<span className="text-destructive">
|
||||
{" "}
|
||||
Required *
|
||||
</span>
|
||||
)}
|
||||
|
||||
{key === "SMTP_PASSWORD" && <span style={{ color: "red" }}> Required * </span>}
|
||||
|
||||
{key === "SMTP_SENDER_EMAIL" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the sender email address, e.g. `sender@berri.ai`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
<span>
|
||||
Enter the sender email address, e.g.{" "}
|
||||
<code>sender@berri.ai</code>
|
||||
<span className="text-destructive">
|
||||
{" "}
|
||||
Required *
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{key === "TEST_EMAIL_ADDRESS" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Email Address to send `Test Email Alert` to. example: `info@berri.ai`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
<span>
|
||||
Email Address to send <code>Test Email Alert</code>{" "}
|
||||
to. example: <code>info@berri.ai</code>
|
||||
<span className="text-destructive">
|
||||
{" "}
|
||||
Required *
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{key === "EMAIL_LOGO_URL" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
(Optional) Customize the Logo that appears in the email, pass a url to your logo
|
||||
</div>
|
||||
<span>
|
||||
(Optional) Customize the Logo that appears in the
|
||||
email, pass a url to your logo
|
||||
</span>
|
||||
)}
|
||||
{key === "EMAIL_SUPPORT_CONTACT" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
(Optional) Customize the support email address that appears in the email. Default is
|
||||
support@berri.ai
|
||||
</div>
|
||||
<span>
|
||||
(Optional) Customize the support email address that
|
||||
appears in the email. Default is support@berri.ai
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</Grid>
|
||||
</ul>
|
||||
</TableCell>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button className="mt-2" onClick={() => handleSaveEmailSettings()}>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, "email");
|
||||
NotificationManager.success("Email test triggered. Check your configured email inbox/logs.");
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend(error);
|
||||
}
|
||||
}}
|
||||
className="mx-2"
|
||||
>
|
||||
Test Email Alerts
|
||||
</Button>
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button onClick={() => handleSaveEmailSettings()}>Save Changes</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, "email");
|
||||
NotificationManager.success(
|
||||
"Email test triggered. Check your configured email inbox/logs.",
|
||||
);
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test Email Alerts
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button, Space } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { MinusCircle, Plus } from "lucide-react";
|
||||
|
||||
interface KeyValueInputProps {
|
||||
value?: Record<string, string>;
|
||||
onChange?: (value: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) => {
|
||||
const [pairs, setPairs] = useState<[string, string][]>(Object.entries(value));
|
||||
const KeyValueInput: React.FC<KeyValueInputProps> = ({
|
||||
value = {},
|
||||
onChange,
|
||||
}) => {
|
||||
const [pairs, setPairs] = useState<[string, string][]>(
|
||||
Object.entries(value),
|
||||
);
|
||||
|
||||
const handleAdd = () => {
|
||||
setPairs([...pairs, ["", ""]]);
|
||||
|
|
@ -31,19 +36,36 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
|
|||
return (
|
||||
<div>
|
||||
{pairs.map(([key, val], index) => (
|
||||
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="center">
|
||||
<TextInput placeholder="Header Name" value={key} onChange={(e) => handleChange(index, e.target.value, val)} />
|
||||
<TextInput
|
||||
<div key={index} className="flex items-center gap-2 mb-2">
|
||||
<Input
|
||||
placeholder="Header Name"
|
||||
value={key}
|
||||
onChange={(e) => handleChange(index, e.target.value, val)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Header Value"
|
||||
value={val}
|
||||
onChange={(e) => handleChange(index, key, e.target.value)}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }}>
|
||||
<MinusCircleOutlined onClick={() => handleRemove(index)} style={{ cursor: "pointer" }} />
|
||||
</div>
|
||||
</Space>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemove(index)}
|
||||
aria-label="Remove header"
|
||||
className="h-8 w-8 shrink-0"
|
||||
>
|
||||
<MinusCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleAdd}
|
||||
className="border-dashed"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Header
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue