diff --git a/ui/litellm-dashboard/docs/CYCLES.md b/ui/litellm-dashboard/docs/CYCLES.md index 5bc5b0f01ae..06ddb77cf82 100644 --- a/ui/litellm-dashboard/docs/CYCLES.md +++ b/ui/litellm-dashboard/docs/CYCLES.md @@ -52,3 +52,21 @@ Layer abbreviations: **TS** (tsc --noEmit), **Lint** (eslint), **Vitest** - Scope: PolicySelect.tsx only (the single banned-import file under ToolPolicies/). The ToolPoliciesView.tsx top-level wrapper has no antd imports already. Top-level ToolPolicies.tsx legacy wrapper is not in scope. - Section decision: keep amber/emerald/red categorical colors for policy badges (not theme-semantic). Documented in DEVIATIONS.md. +## 21. Logging & Alerts / 32-a: Alerting `DynamicForm` + +- Cycles used: 1 / 7 +- Layer outcomes per cycle: + - cycle 1: TS ✓ | Lint ✓ | Vitest n/a (no colocated tests) | Build ✓ | Parity ⏭ | Snap ⏭ +- Final status: **done (partial — single file within the Logging & Alerts section)** +- Scope: `src/components/alerting/dynamic_form.tsx` only. The broader + Logging & Alerts section (alerting_settings.tsx, email_events/*) still + has files that will be picked up by subsequent runs. + +## 21-b. Email Event Settings + +- Cycles used: 1 / 7 +- Layer outcomes per cycle: + - cycle 1: TS ✓ | Lint ✓ | Vitest n/a | Build ✓ | Parity ⏭ | Snap ⏭ +- Final status: **done (partial)** +- Scope: `src/components/email_events/email_event_settings.tsx` only. + diff --git a/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx b/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx index a0c7586ce4f..92b995392a7 100644 --- a/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx +++ b/ui/litellm-dashboard/src/components/alerting/dynamic_form.tsx @@ -1,11 +1,16 @@ -import React from "react"; -import { Form, Input, InputNumber, Button as Button2 } from "antd"; -import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline"; -import { Button, Badge, Icon, Text, TableRow, TableCell, Switch } from "@tremor/react"; +import React, { useState } from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { TableCell, TableRow } from "@/components/ui/table"; +import { CheckCircle, Trash2 } from "lucide-react"; + interface AlertingSetting { field_name: string; field_description: string; field_type: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any field_value: any; stored_in_db: boolean | null; premium_field: boolean; @@ -13,12 +18,23 @@ interface AlertingSetting { interface DynamicFormProps { alertingSettings: AlertingSetting[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any handleInputChange: (fieldName: string, newValue: any) => void; handleResetField: (fieldName: string, index: number) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any handleSubmit: (formValues: Record) => void; premiumUser: boolean; } +/** + * Alerting-settings dynamic form. Settings are rendered row-by-row into an + * existing table context (this component emits `` children that + * must be placed inside a ``/`` parent). + * + * Post phase-1 migration: antd Form replaced with a native form element + * driving a local state bag; no rhf/zod here because the field set is + * dynamic (driven by `alertingSettings`). + */ const DynamicForm: React.FC = ({ alertingSettings, handleInputChange, @@ -26,121 +42,121 @@ const DynamicForm: React.FC = ({ handleSubmit, premiumUser, }) => { - const [form] = Form.useForm(); + // Mirror the form state locally so we can submit a consolidated snapshot + // without depending on antd's Form.getFieldsValue(). + const [formValues, setFormValues] = useState>({}); - const onFinish = () => { - console.log(`INSIDE ONFINISH`); - const formData = form.getFieldsValue(); - const isEmpty = Object.entries(formData).every(([key, value]) => { - if (typeof value === "boolean") { - return false; // Boolean values are never considered empty - } + const updateLocal = (name: string, value: unknown) => { + setFormValues((prev) => ({ ...prev, [name]: value })); + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const isEmpty = Object.entries(formValues).every(([, value]) => { + if (typeof value === "boolean") return false; return value === "" || value === null || value === undefined; }); - console.log(`formData: ${JSON.stringify(formData)}, isEmpty: ${isEmpty}`); if (!isEmpty) { - handleSubmit(formData); + handleSubmit(formValues); } else { console.log("Some form fields are empty."); } }; return ( -
- {alertingSettings.map((value, index) => ( - - - {value.field_name} -

+ {alertingSettings.map((value, index) => { + const renderInput = () => { + if (value.field_type === "Integer") { + return ( + { + const v = e.target.value === "" ? null : Number(e.target.value); + handleInputChange(value.field_name, v); + updateLocal(value.field_name, v); + }} + /> + ); + } + if (value.field_type === "Boolean") { + return ( + { + handleInputChange(value.field_name, checked); + updateLocal(value.field_name, checked); + }} + /> + ); + } + return ( + { + handleInputChange(value.field_name, e.target.value); + updateLocal(value.field_name, e.target.value); }} - className="mt-1" - > - {value.field_description} -

-
- {value.premium_field ? ( - premiumUser ? ( - - - {value.field_type === "Integer" ? ( - handleInputChange(value.field_name, e)} - /> - ) : value.field_type === "Boolean" ? ( - handleInputChange(value.field_name, checked)} - /> - ) : ( - handleInputChange(value.field_name, e)} /> - )} - - - ) : ( - - - - ) - ) : ( - - - {value.field_type === "Integer" ? ( - handleInputChange(value.field_name, e)} - className="p-0" - /> - ) : value.field_type === "Boolean" ? ( - { - handleInputChange(value.field_name, checked); - form.setFieldsValue({ [value.field_name]: checked }); - }} - /> - ) : ( - handleInputChange(value.field_name, e)} /> - )} - - - )} - - {value.stored_in_db == true ? ( - - In DB - - ) : value.stored_in_db == false ? ( - In Config - ) : ( - Not Set - )} - - - handleResetField(value.field_name, index)}> - Reset - - -
- ))} + ) : ( + renderInput() + )} + + + {value.stored_in_db === true ? ( + + + In DB + + ) : value.stored_in_db === false ? ( + In Config + ) : ( + Not Set + )} + + + + + + ); + })}
- Update Settings +
- + ); }; diff --git a/ui/litellm-dashboard/src/components/email_events/email_event_settings.tsx b/ui/litellm-dashboard/src/components/email_events/email_event_settings.tsx index 8d0555f17bb..4b28209a765 100644 --- a/ui/litellm-dashboard/src/components/email_events/email_event_settings.tsx +++ b/ui/litellm-dashboard/src/components/email_events/email_event_settings.tsx @@ -1,41 +1,45 @@ import React, { useState, useEffect } from "react"; -import { Card, Text, Button } from "@tremor/react"; -import { Typography, Divider, Spin, Checkbox } from "antd"; +import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; import NotificationsManager from "../molecules/notifications_manager"; -import { getEmailEventSettings, updateEmailEventSettings, resetEmailEventSettings } from "../networking"; +import { + getEmailEventSettings, + updateEmailEventSettings, + resetEmailEventSettings, +} from "../networking"; import { EmailEvent } from "../../types"; import { EmailEventSetting } from "./types"; -const { Title } = Typography; - interface EmailEventSettingsProps { accessToken: string | null; } -const EmailEventSettings: React.FC = ({ accessToken }) => { +const EmailEventSettings: React.FC = ({ + accessToken, +}) => { const [loading, setLoading] = useState(true); const [eventSettings, setEventSettings] = useState([]); - // Fetch email event settings on component mount useEffect(() => { + const fetchEventSettings = async () => { + if (!accessToken) return; + setLoading(true); + try { + const response = await getEmailEventSettings(accessToken); + setEventSettings(response.settings); + } catch (error) { + console.error("Failed to fetch email event settings:", error); + NotificationsManager.fromBackend(error); + } finally { + setLoading(false); + } + }; fetchEventSettings(); }, [accessToken]); - const fetchEventSettings = async () => { - if (!accessToken) return; - - setLoading(true); - try { - const response = await getEmailEventSettings(accessToken); - setEventSettings(response.settings); - } catch (error) { - console.error("Failed to fetch email event settings:", error); - NotificationsManager.fromBackend(error); - } finally { - setLoading(false); - } - }; - const handleCheckboxChange = (event: EmailEvent, checked: boolean) => { const updatedSettings = eventSettings.map((setting) => setting.event === event ? { ...setting, enabled: checked } : setting, @@ -45,7 +49,6 @@ const EmailEventSettings: React.FC = ({ accessToken }) const handleSaveSettings = async () => { if (!accessToken) return; - try { await updateEmailEventSettings(accessToken, { settings: eventSettings }); NotificationsManager.success("Email event settings updated successfully"); @@ -57,27 +60,26 @@ const EmailEventSettings: React.FC = ({ accessToken }) const handleResetSettings = async () => { if (!accessToken) return; - try { await resetEmailEventSettings(accessToken); NotificationsManager.success("Email event settings reset to defaults"); - // Refresh settings after reset - fetchEventSettings(); + // Refresh + setLoading(true); + const response = await getEmailEventSettings(accessToken); + setEventSettings(response.settings); + setLoading(false); } catch (error) { console.error("Failed to reset email event settings:", error); NotificationsManager.fromBackend(error); } }; - // Helper function to get a description for each event type const getEventDescription = (event: EmailEvent): string => { - // Convert event name to a sentence with more context if (event.includes("Virtual Key Created")) { return "An email will be sent to the user when a new virtual key is created with their user ID"; } else if (event.includes("New User Invitation")) { return "An email will be sent to the email address of the user when a new user is created"; } else { - // Handle any other event type from the API const words = event .split(/(?=[A-Z])/) .join(" ") @@ -87,37 +89,54 @@ const EmailEventSettings: React.FC = ({ accessToken }) }; return ( - - Email Notifications - Select which events should trigger email notifications. - + +

Email Notifications

+

+ Select which events should trigger email notifications. +

+ {loading ? ( -
- +
+
) : (
{eventSettings.map((setting) => ( -
+
handleCheckboxChange(setting.event, e.target.checked)} + onCheckedChange={(checked) => + handleCheckboxChange(setting.event, !!checked) + } + className="mt-1" /> -
- {setting.event} -
{getEventDescription(setting.event)}
+
+ +
+ {getEventDescription(setting.event)} +
))}
)} -
+
-