diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index c3b2bb03f11..e83c2d678ed 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -2497,16 +2497,6 @@ "count": 1 } }, - "src/components/molecules/message_manager.tsx": { - "local/filename-pascal-case": { - "count": 1 - } - }, - "src/components/molecules/notifications_manager.tsx": { - "local/filename-pascal-case": { - "count": 1 - } - }, "src/components/navbar.tsx": { "local/filename-pascal-case": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.tsx index ec557260aa5..c65f8bf951f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupEditModal.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from "react"; import { Modal, Form } from "antd"; -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { AccessGroupBaseForm, AccessGroupFormValues } from "./AccessGroupBaseForm"; import { useEditAccessGroup, AccessGroupUpdateParams } from "@/app/(dashboard)/hooks/accessGroups/useEditAccessGroup"; import { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups"; @@ -45,7 +45,7 @@ export function AccessGroupEditModal({ visible, accessGroup, onCancel, onSuccess { accessGroupId: accessGroup.access_group_id, params }, { onSuccess: () => { - MessageManager.success("Access group updated successfully"); + toast.success("Access group updated successfully"); onSuccess?.(); onCancel(); }, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/AccessGroupCreateDialog.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/AccessGroupCreateDialog.test.tsx index 8547610ee60..7a006efce1e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/AccessGroupCreateDialog.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/access-group-create/AccessGroupCreateDialog.test.tsx @@ -4,10 +4,6 @@ import userEvent from "@testing-library/user-event"; import React from "react"; import { describe, expect, it, vi } from "vitest"; -vi.mock("@/components/molecules/notifications_manager", () => ({ - __esModule: true, - default: { success: vi.fn(), fromBackend: vi.fn() }, -})); vi.mock("@/components/ModelSelect/ModelSelect", () => ({ ModelSelect: ({ onChange }: { onChange: (values: string[]) => void }) => ( diff --git a/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx b/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx index 1935c0f226a..ea092e5af14 100644 --- a/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/KeysPanel.tsx @@ -11,7 +11,7 @@ import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { keyListCall, regenerateKeyCall } from "../networking"; import { KeyResponse } from "../key_team_helpers/key_list"; import { formatExpiresUtc, isKeyExpired, calculateExpiryPreviewFromDuration } from "@/utils/keyExpiryUtils"; @@ -132,10 +132,10 @@ const KeysPanel: React.FC = ({ accessToken, userId, premiumUser }) => { const response = await regenerateKeyCall(accessToken, rotateTarget.token || rotateTarget.token_id, payload); setRegeneratedKey(response.key); - MessageManager.success("Key rotated successfully"); + toast.success("Key rotated successfully"); queryClient.invalidateQueries({ queryKey: [KEYS_QUERY_KEY] }); } catch { - MessageManager.error("Failed to rotate key"); + toast.error("Failed to rotate key"); } finally { setIsRegenerating(false); } diff --git a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx index 7dbfc058a77..52c067a0732 100644 --- a/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx +++ b/ui/litellm-dashboard/src/components/chat/MCPAppsPanel.tsx @@ -15,7 +15,7 @@ import { } from "../networking"; import { AUTH_TYPE, MCPServer, MCPTool, handleTransport, isUnsupportedOnGatewayConnect } from "../mcp_tools/types"; import { Logo } from "@/components/molecules/logo/Logo"; -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { useUserMcpOAuthFlow } from "@/hooks/useUserMcpOAuthFlow"; interface OAuth2ConnectButtonProps { @@ -249,7 +249,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange, try { const result = await listMCPTools(accessToken, server.server_id); if (result?.error) { - MessageManager.warning(`Could not load tools for ${serverName}`); + toast.warning(`Could not load tools for ${serverName}`); return; } if (connectableNow(server.server_id) === undefined) return; @@ -257,7 +257,7 @@ const MCPAppsPanel: React.FC = ({ accessToken, selectedServers, onChange, onChange([...selectedServersRef.current, serverName]); } } catch { - MessageManager.warning(`Could not load tools for ${serverName}`); + toast.warning(`Could not load tools for ${serverName}`); } finally { setTogglingOn((prev) => { const next = new Set(prev); diff --git a/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx b/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx index a353457946c..41c9327591a 100644 --- a/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx +++ b/ui/litellm-dashboard/src/components/chat/MCPConnectPicker.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Loader2 } from "lucide-react"; import { Switch } from "@/components/ui/switch"; import { Skeleton } from "@/components/ui/skeleton"; -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { Logo } from "@/components/molecules/logo/Logo"; import { fetchMCPServers, listMCPTools } from "../networking"; import { MCPServer } from "../mcp_tools/types"; @@ -56,12 +56,12 @@ const MCPConnectPicker: React.FC = ({ accessToken, selectedServers, onCha try { const result = await listMCPTools(accessToken, serverName); if (result?.error) { - MessageManager.warning(`Could not load tools for ${serverName} \u2014 it will be excluded from this message.`); + toast.warning(`Could not load tools for ${serverName} \u2014 it will be excluded from this message.`); return; } onChange([...selectedServers, serverName]); } catch { - MessageManager.warning(`Could not load tools for ${serverName} \u2014 it will be excluded from this message.`); + toast.warning(`Could not load tools for ${serverName} \u2014 it will be excluded from this message.`); } finally { setTogglingOn((prev) => { const next = new Set(prev); diff --git a/ui/litellm-dashboard/src/components/chat/MCPCredentialsTab.tsx b/ui/litellm-dashboard/src/components/chat/MCPCredentialsTab.tsx index 4f80a941511..33c153b3474 100644 --- a/ui/litellm-dashboard/src/components/chat/MCPCredentialsTab.tsx +++ b/ui/litellm-dashboard/src/components/chat/MCPCredentialsTab.tsx @@ -18,7 +18,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { deleteMCPOAuthUserCredential, listMCPUserCredentials, MCPUserCredentialListItem } from "../networking"; const MCP_CREDENTIALS_QUERY_KEY = "mcp-user-credentials"; @@ -83,7 +83,7 @@ const MCPCredentialsTab: React.FC = ({ accessToken }) => { (prev ?? []).filter((c) => c.server_id !== serverId), ); } catch { - MessageManager.error("Failed to revoke connection. Please try again."); + toast.error("Failed to revoke connection. Please try again."); } finally { setRevoking((prev) => { const n = new Set(prev); diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx index 58dc62ee6b2..9d921041d92 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/MakeSkillPublicForm.tsx @@ -6,7 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { cn } from "@/lib/cva.config"; import { enableClaudeCodePlugin, disableClaudeCodePlugin } from "../networking"; -import NotificationsManager from "../molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { Plugin } from "./types"; const STEP_TITLES = ["Select Skills", "Confirm"]; @@ -38,7 +38,7 @@ const MakeSkillPublicForm: React.FC = ({ const handleNext = () => { if (selectedSkills.size === 0) { - NotificationsManager.fromBackend("Please select at least one skill"); + toast.fromError("Please select at least one skill"); return; } setCurrentStep(1); @@ -71,7 +71,7 @@ const MakeSkillPublicForm: React.FC = ({ const handleSubmit = async () => { if (selectedSkills.size === 0) { - NotificationsManager.fromBackend("Please select at least one skill"); + toast.fromError("Please select at least one skill"); return; } @@ -91,12 +91,12 @@ const MakeSkillPublicForm: React.FC = ({ }), ); - NotificationsManager.success(`Skill Hub updated — ${selectedSkills.size} skill(s) published`); + toast.success(`Skill Hub updated — ${selectedSkills.size} skill(s) published`); handleClose(); onSuccess(); } catch (error) { console.error("Error publishing skills:", error); - NotificationsManager.fromBackend("Failed to update skills. Please try again."); + toast.fromError("Failed to update skills. Please try again."); } finally { setLoading(false); } diff --git a/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx b/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx index 6a3d5ded452..2aaad786e91 100644 --- a/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx +++ b/ui/litellm-dashboard/src/components/cloudzero_export_modal.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { Text, Button, Callout, TextInput } from "@tremor/react"; import { Modal, Form, Spin, Select } from "antd"; import { getGlobalLitellmHeaderName } from "@/components/networking"; -import NotificationsManager from "./molecules/notifications_manager"; +import { toast } from "@/lib/toast"; interface CloudZeroExportModalProps { isOpen: boolean; @@ -59,11 +59,11 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC } else if (response.status !== 404) { // 404 means no settings configured yet, which is fine const errorData = await response.json(); - NotificationsManager.fromBackend(`Failed to load existing settings: ${errorData.error || "Unknown error"}`); + toast.fromError(`Failed to load existing settings: ${errorData.error || "Unknown error"}`); } } catch (error) { console.error("Error loading CloudZero settings:", error); - NotificationsManager.fromBackend("Failed to load existing settings"); + toast.fromError("Failed to load existing settings"); } finally { setSettingsLoading(false); } @@ -71,7 +71,7 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC const handleSaveCloudZeroSettings = async (values: CloudZeroSettings) => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + toast.fromError("No access token available"); return; } @@ -98,7 +98,7 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC const data = await response.json(); if (response.ok) { - NotificationsManager.success(data.message || "CloudZero settings saved successfully"); + toast.success(data.message || "CloudZero settings saved successfully"); setExistingSettings({ api_key_masked: values.api_key.substring(0, 4) + "****" + values.api_key.slice(-4), connection_id: values.connection_id, @@ -106,12 +106,12 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC }); return true; } else { - NotificationsManager.fromBackend(data.error || "Failed to save CloudZero settings"); + toast.fromError(data.error || "Failed to save CloudZero settings"); return false; } } catch (error) { console.error("Error saving CloudZero settings:", error); - NotificationsManager.fromBackend("Failed to save CloudZero settings"); + toast.fromError("Failed to save CloudZero settings"); return false; } finally { setLoading(false); @@ -120,7 +120,7 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC const handleExportCloudZero = async () => { if (!accessToken) { - NotificationsManager.fromBackend("No access token available"); + toast.fromError("No access token available"); return; } @@ -141,14 +141,14 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC const data = await response.json(); if (response.ok) { - NotificationsManager.success(data.message || "Export to CloudZero completed successfully"); + toast.success(data.message || "Export to CloudZero completed successfully"); onClose(); } else { - NotificationsManager.fromBackend(data.error || "Failed to export to CloudZero"); + toast.fromError(data.error || "Failed to export to CloudZero"); } } catch (error) { console.error("Error exporting to CloudZero:", error); - NotificationsManager.fromBackend("Failed to export to CloudZero"); + toast.fromError("Failed to export to CloudZero"); } finally { setExportLoading(false); } @@ -158,11 +158,11 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC setExportLoading(true); try { // TODO: Implement CSV export functionality - NotificationsManager.info("CSV export functionality coming soon!"); + toast.info("CSV export functionality coming soon!"); onClose(); } catch (error) { console.error("Error exporting CSV:", error); - NotificationsManager.fromBackend("Failed to export CSV"); + toast.fromError("Failed to export CSV"); } finally { setExportLoading(false); } diff --git a/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx index 9b89d85507b..2aecedb8a6e 100644 --- a/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx +++ b/ui/litellm-dashboard/src/components/common_components/ModelAliasManager.tsx @@ -3,7 +3,7 @@ import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline" import { Card, CardTitle } from "@/components/ui/card"; import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table"; import ModelSelector from "./ModelSelector"; -import NotificationsManager from "../molecules/notifications_manager"; +import { toast } from "@/lib/toast"; interface ModelAliasManagerProps { accessToken: string; @@ -40,13 +40,13 @@ const ModelAliasManager: React.FC = ({ const handleAddAlias = () => { if (!newAlias.aliasName || !newAlias.targetModel) { - NotificationsManager.fromBackend("Please provide both alias name and target model"); + toast.fromError("Please provide both alias name and target model"); return; } // Check for duplicate alias names if (aliases.some((alias) => alias.aliasName === newAlias.aliasName)) { - NotificationsManager.fromBackend("An alias with this name already exists"); + toast.fromError("An alias with this name already exists"); return; } @@ -70,7 +70,7 @@ const ModelAliasManager: React.FC = ({ onAliasUpdate(aliasObject); } - NotificationsManager.success("Alias added successfully"); + toast.success("Alias added successfully"); }; const handleEditAlias = (alias: AliasItem) => { @@ -81,13 +81,13 @@ const ModelAliasManager: React.FC = ({ if (!editingAlias) return; if (!editingAlias.aliasName || !editingAlias.targetModel) { - NotificationsManager.fromBackend("Please provide both alias name and target model"); + toast.fromError("Please provide both alias name and target model"); return; } // Check for duplicate alias names (excluding current alias) if (aliases.some((alias) => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) { - NotificationsManager.fromBackend("An alias with this name already exists"); + toast.fromError("An alias with this name already exists"); return; } @@ -106,7 +106,7 @@ const ModelAliasManager: React.FC = ({ onAliasUpdate(aliasObject); } - NotificationsManager.success("Alias updated successfully"); + toast.success("Alias updated successfully"); }; const handleCancelEdit = () => { @@ -127,7 +127,7 @@ const ModelAliasManager: React.FC = ({ onAliasUpdate(aliasObject); } - NotificationsManager.success("Alias deleted successfully"); + toast.success("Alias deleted successfully"); }; // Convert current aliases to object for config example diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx index da36063e68a..019f60aecd9 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import { fireEvent, renderWithProviders, screen, waitFor, within } from "@/../tests/test-utils"; -import NotificationsManager from "@/components/molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import EditAutoRouterModal from "./edit_auto_router_modal"; vi.mock( "@/app/(dashboard)/hooks/autoRouter/useComplexityScorerDefaults", @@ -130,7 +130,7 @@ describe("EditAutoRouterModal keyword matching", () => { await screen.findByText(/Escalation Keywords/i); await user.click(screen.getByRole("button", { name: /save changes/i })); - await waitFor(() => expect(NotificationsManager.fromBackend).toHaveBeenCalled()); + await waitFor(() => expect(toast.fromError).toHaveBeenCalled()); expect(modelPatchUpdateCall).not.toHaveBeenCalled(); }); @@ -731,9 +731,7 @@ describe("EditAutoRouterModal default model", () => { await user.click(await screen.findByRole("button", { name: /save changes/i })); - await waitFor(() => - expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(expect.stringContaining("Simple or Medium tier")), - ); + await waitFor(() => expect(toast.fromError).toHaveBeenCalledWith(expect.stringContaining("Simple or Medium tier"))); expect(modelPatchUpdateCall).not.toHaveBeenCalled(); }); diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx index 32521a3e043..cd4ecee1dc8 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx @@ -31,7 +31,7 @@ import ComplexityRouterConfig, { DEFAULT_TIER_DISTANCE_PENALTY, heuristicScoringRole, } from "../add_model/ComplexityRouterConfig"; -import NotificationsManager from "../molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { Dialog, DialogContent, @@ -367,7 +367,7 @@ const EditAutoRouterModal: React.FC = ({ }); } catch (error) { console.error("Error parsing auto router config:", error); - NotificationsManager.fromBackend("Error loading auto router configuration"); + toast.fromError("Error loading auto router configuration"); } }; @@ -380,12 +380,12 @@ const EditAutoRouterModal: React.FC = ({ const { tiers, classifier_type, classifier_llm_config } = complexityRouterConfig; if (Object.values(tiers).every((models) => models.length === 0)) { setShowValidationErrors(true); - NotificationsManager.fromBackend("Please select at least one model for a complexity tier"); + toast.fromError("Please select at least one model for a complexity tier"); return; } if (classifier_type === "llm" && !classifier_llm_config?.model) { setShowValidationErrors(true); - NotificationsManager.fromBackend("Please select a classifier model, or switch back to Heuristic"); + toast.fromError("Please select a classifier model, or switch back to Heuristic"); return; } // Same guards the create form applies (add_auto_router_tab.tsx). The backend rejects a @@ -395,14 +395,14 @@ const EditAutoRouterModal: React.FC = ({ const keywordRulesError = getKeywordTierRulesError(keywordTierRules); if (keywordRulesError) { setShowValidationErrors(true); - NotificationsManager.fromBackend(keywordRulesError); + toast.fromError(keywordRulesError); return; } const semanticError = getSemanticConfigError({ semanticMatchingEnabled, embeddingModel, keywordTierRules }); if (semanticError) { setShowValidationErrors(true); - NotificationsManager.fromBackend(semanticError); + toast.fromError(semanticError); return; } @@ -414,7 +414,7 @@ const EditAutoRouterModal: React.FC = ({ const defaultModel = resolveComplexityDefaultModel(tiers, complexityRouterConfig.default_model); if (!defaultModel) { setShowValidationErrors(true); - NotificationsManager.fromBackend( + toast.fromError( "Add a model to the Simple or Medium tier, or pin a default model, so requests have somewhere to route.", ); return; @@ -450,7 +450,7 @@ const EditAutoRouterModal: React.FC = ({ modelData.model_info.id, ); - NotificationsManager.success("Auto router configuration updated successfully"); + toast.success("Auto router configuration updated successfully"); onSuccess({ ...modelData, model_name: values.auto_router_name, @@ -490,12 +490,12 @@ const EditAutoRouterModal: React.FC = ({ model_info: updatedModelInfo, }; - NotificationsManager.success("Auto router configuration updated successfully"); + toast.success("Auto router configuration updated successfully"); onSuccess(updatedModelData); onCancel(); } catch (error) { console.error("Error updating auto router:", error); - NotificationsManager.fromBackend("Failed to update auto router configuration"); + toast.fromError("Failed to update auto router configuration"); } finally { setLoading(false); } 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 63ff4f6f725..5d063118b0b 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 @@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { toast } from "@/lib/toast"; import { getEmailEventSettings, updateEmailEventSettings, resetEmailEventSettings } from "../networking"; import { EmailEvent } from "../../types"; import { EmailEventSetting } from "./types"; @@ -31,7 +31,7 @@ const EmailEventSettings: React.FC = ({ accessToken }) setEventSettings(response.settings); } catch (error) { console.error("Failed to fetch email event settings:", error); - NotificationsManager.fromBackend(error); + toast.fromError(error); } finally { setLoading(false); } @@ -49,10 +49,10 @@ const EmailEventSettings: React.FC = ({ accessToken }) try { await updateEmailEventSettings(accessToken, { settings: eventSettings }); - NotificationsManager.success("Email event settings updated successfully"); + toast.success("Email event settings updated successfully"); } catch (error) { console.error("Failed to update email event settings:", error); - NotificationsManager.fromBackend(error); + toast.fromError(error); } }; @@ -61,12 +61,12 @@ const EmailEventSettings: React.FC = ({ accessToken }) try { await resetEmailEventSettings(accessToken); - NotificationsManager.success("Email event settings reset to defaults"); + toast.success("Email event settings reset to defaults"); // Refresh settings after reset fetchEventSettings(); } catch (error) { console.error("Failed to reset email event settings:", error); - NotificationsManager.fromBackend(error); + toast.fromError(error); } }; diff --git a/ui/litellm-dashboard/src/components/email_settings.tsx b/ui/litellm-dashboard/src/components/email_settings.tsx index 6f1c3b1f846..65fae5a2012 100644 --- a/ui/litellm-dashboard/src/components/email_settings.tsx +++ b/ui/litellm-dashboard/src/components/email_settings.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; import { Eye, EyeOff } from "lucide-react"; -import NotificationManager from "./molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { serviceHealthCheck, setCallbacksCall } from "./networking"; import { EmailEventSettings } from "./email_events"; @@ -78,9 +78,9 @@ const EmailSettings: React.FC = ({ accessToken, premiumUser, }; try { await setCallbacksCall(accessToken, payload); - NotificationManager.success("Email settings updated successfully"); + toast.success("Email settings updated successfully"); } catch (error) { - NotificationManager.fromBackend(error); + toast.fromError(error); } }; @@ -161,9 +161,9 @@ const EmailSettings: React.FC = ({ accessToken, premiumUser, if (!accessToken) return; try { await serviceHealthCheck(accessToken, "email"); - NotificationManager.success("Email test triggered. Check your configured email inbox/logs."); + toast.success("Email test triggered. Check your configured email inbox/logs."); } catch (error) { - NotificationManager.fromBackend(error); + toast.fromError(error); } }} > diff --git a/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx b/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx index 775ccee9bee..94e8cb46765 100644 --- a/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx +++ b/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx @@ -2,7 +2,7 @@ import openai from "openai"; import { MessageType } from "../chat_ui/types"; import { TokenUsage } from "../chat_ui/ResponseMetrics"; import { getProxyBaseUrl } from "@/components/networking"; -import NotificationManager from "@/components/molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { extractPromptCacheTokens } from "@/utils/promptCacheUsage"; import type { MCPEvent } from "@/components/mcp_tools/types"; import { MCPServer, MCPToolset } from "@/components/mcp_tools/types"; @@ -317,9 +317,7 @@ export async function makeOpenAIResponsesRequest( } catch (error) { if (signal?.aborted) { } else { - NotificationManager.fromBackend( - `Error occurred while generating model response. Please try again. Error: ${error}`, - ); + toast.fromError(`Error occurred while generating model response. Please try again. Error: ${error}`); } throw error; // Re-throw to allow the caller to handle the error } diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.test.tsx index 021aec5f85f..f23d06e492c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerAuthHeaderNameGetter, registerAuthTokenGetter, registerBaseUrlGetter } from "@/lib/http/runtime"; +import { toast } from "@/lib/toast"; import { ByokCredentialModal } from "./ByokCredentialModal"; import type { MCPServer } from "./types"; @@ -12,10 +13,6 @@ const fetchSpy = vi.hoisted(() => { return spy; }); -vi.mock("@/components/molecules/message_manager", () => ({ - default: { success: vi.fn(), error: vi.fn() }, -})); - const SERVER = { server_id: "srv-1", alias: "Linear", server_name: "Linear" } as MCPServer; const jsonResponse = (body: unknown, status = 200) => @@ -57,16 +54,13 @@ describe("ByokCredentialModal", () => { fetchSpy.mockResolvedValue( jsonResponse({ detail: { error: "This MCP server does not support BYOK credentials" } }, 400), ); - const MessageManager = (await import("@/components/molecules/message_manager")).default; const onSuccess = vi.fn(); const user = userEvent.setup(); render( {}} onSuccess={onSuccess} />); await fillAndSubmit(user); - await waitFor(() => - expect(MessageManager.error).toHaveBeenCalledWith("This MCP server does not support BYOK credentials"), - ); + await waitFor(() => expect(toast.error).toHaveBeenCalledWith("This MCP server does not support BYOK credentials")); expect(onSuccess).not.toHaveBeenCalled(); }); }); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx index f36de019aa5..9fe306de0e8 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/ByokCredentialModal.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Modal, Input, Switch } from "antd"; -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { fetchClient } from "@/lib/http/api"; import { ApiError } from "@/lib/http/client"; import { @@ -50,7 +50,7 @@ export const ByokCredentialModal: React.FC = ({ server const handleAuthorize = async () => { if (!apiKey.trim()) { - MessageManager.error("Please enter your API key"); + toast.error("Please enter your API key"); return; } setLoading(true); @@ -59,11 +59,11 @@ export const ByokCredentialModal: React.FC = ({ server params: { path: { server_id: server.server_id } }, body: { credential: apiKey.trim(), save: saveKey }, }); - MessageManager.success(`Connected to ${serverDisplayName}`); + toast.success(`Connected to ${serverDisplayName}`); onSuccess(server.server_id); handleClose(); } catch (e) { - MessageManager.error(byokSaveErrorMessage(e)); + toast.error(byokSaveErrorMessage(e)); } finally { setLoading(false); } diff --git a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx index 19f6d883930..8121f7c230f 100644 --- a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx +++ b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.test.tsx @@ -5,7 +5,7 @@ import type { ComponentProps } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { CredentialItem, credentialCreateCall, credentialUpdateCall } from "@/components/networking"; -import NotificationsManager from "@/components/molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import CredentialsPanel from "./CredentialsPanel"; @@ -22,10 +22,6 @@ vi.mock("@/app/(dashboard)/hooks/credentials/useCredentials", () => ({ useCredentials: () => mockUseCredentials(), })); -vi.mock("@/components/molecules/notifications_manager", () => ({ - default: { success: vi.fn(), error: vi.fn(), fromBackend: vi.fn() }, -})); - vi.mock("@/components/networking", async (importOriginal) => { const actual = await importOriginal(); return { @@ -160,7 +156,7 @@ describe("CredentialsPanel", () => { await user.click(screen.getByTestId("credential-modal-add-submit")); await waitFor(() => { - expect(NotificationsManager.success).toHaveBeenCalledWith("Credential added successfully"); + expect(toast.success).toHaveBeenCalledWith("Credential added successfully"); }); expect(refetch).toHaveBeenCalled(); expect(screen.queryByTestId("credential-modal-add-submit")).not.toBeInTheDocument(); @@ -178,11 +174,11 @@ describe("CredentialsPanel", () => { await user.click(screen.getByTestId("credential-modal-add-submit")); await waitFor(() => { - expect(NotificationsManager.error).toHaveBeenCalledWith("Failed to add credential"); + expect(toast.error).toHaveBeenCalledWith("Failed to add credential"); }); // The modal stays open so the user can retry, and no success toast fired. expect(screen.getByTestId("credential-modal-add-submit")).toBeInTheDocument(); - expect(NotificationsManager.success).not.toHaveBeenCalled(); + expect(toast.success).not.toHaveBeenCalled(); }); it("drops the masked api key from the update payload while keeping the edited api base", async () => { diff --git a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx index d6109ea7655..bccf546e7e4 100644 --- a/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx +++ b/ui/litellm-dashboard/src/components/model_add/CredentialsPanel.tsx @@ -16,7 +16,7 @@ import { stripMaskedSecrets } from "@/utils/maskedSecretUtils"; import { isProxyAdminRole } from "@/utils/roles"; import DeleteResourceModal from "../common_components/DeleteResourceModal"; -import NotificationsManager from "../molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import CredentialModal from "./CredentialModal"; import CredentialsTable from "./CredentialsTable"; @@ -58,11 +58,11 @@ export default function CredentialsPanel({ uploadProps }: CredentialsPanelProps) try { const newCredential = buildCredential(values, stripMaskedSecrets(withoutRestrictedFields(values))); await credentialUpdateCall(accessToken, values.credential_name as string, newCredential); - NotificationsManager.success("Credential updated successfully"); + toast.success("Credential updated successfully"); setIsUpdateModalOpen(false); await refetchCredentials(); } catch (error) { - NotificationsManager.error("Failed to update credential"); + toast.error("Failed to update credential"); } }; @@ -73,11 +73,11 @@ export default function CredentialsPanel({ uploadProps }: CredentialsPanelProps) try { const newCredential = buildCredential(values, withoutRestrictedFields(values)); await credentialCreateCall(accessToken, newCredential); - NotificationsManager.success("Credential added successfully"); + toast.success("Credential added successfully"); setIsAddModalOpen(false); await refetchCredentials(); } catch (error) { - NotificationsManager.error("Failed to add credential"); + toast.error("Failed to add credential"); } }; @@ -88,10 +88,10 @@ export default function CredentialsPanel({ uploadProps }: CredentialsPanelProps) setIsCredentialDeleting(true); try { await credentialDeleteCall(accessToken, credentialToDelete.credential_name); - NotificationsManager.success("Credential deleted successfully"); + toast.success("Credential deleted successfully"); await refetchCredentials(); } catch (error) { - NotificationsManager.error("Failed to delete credential"); + toast.error("Failed to delete credential"); } finally { setCredentialToDelete(null); setIsDeleteModalOpen(false); diff --git a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.test.tsx b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.test.tsx index a1b5de3d0ea..fb6624ed92d 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.test.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.test.tsx @@ -1,6 +1,6 @@ import { useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig"; import { useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB"; -import NotificationsManager from "@/components/molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { parseErrorMessage } from "@/components/shared/errorUtils"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -10,19 +10,13 @@ import ModelSettingsModal from "./ModelSettingsModal"; vi.mock("@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB"); vi.mock("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig"); -vi.mock("@/components/molecules/notifications_manager", () => ({ - default: { - success: vi.fn(), - fromBackend: vi.fn(), - }, -})); vi.mock("@/components/shared/errorUtils", () => ({ parseErrorMessage: vi.fn(), })); const mockUseStoreModelInDB = vi.mocked(useStoreModelInDB); const mockUseProxyConfig = vi.mocked(useProxyConfig); -const mockNotificationsManager = vi.mocked(NotificationsManager); +const mockToast = vi.mocked(toast); const mockParseErrorMessage = vi.mocked(parseErrorMessage); describe("ModelSettingsModal", () => { @@ -155,7 +149,7 @@ describe("ModelSettingsModal", () => { await user.click(saveButton); await waitFor(() => { - expect(mockNotificationsManager.success).toHaveBeenCalledWith("Model storage settings updated successfully"); + expect(mockToast.success).toHaveBeenCalledWith("Model storage settings updated successfully"); expect(mockRefetch).toHaveBeenCalled(); expect(mockOnSuccess).toHaveBeenCalledTimes(1); }); @@ -173,9 +167,7 @@ describe("ModelSettingsModal", () => { await user.click(saveButton); await waitFor(() => { - expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith( - "Failed to save model storage settings: Network error", - ); + expect(mockToast.fromError).toHaveBeenCalledWith("Failed to save model storage settings: Network error"); }); }); @@ -194,9 +186,7 @@ describe("ModelSettingsModal", () => { await user.click(saveButton); await waitFor(() => { - expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith( - "Failed to save model storage settings: Backend error", - ); + expect(mockToast.fromError).toHaveBeenCalledWith("Failed to save model storage settings: Backend error"); }); }); @@ -299,7 +289,7 @@ describe("ModelSettingsModal", () => { await user.click(saveButton); await waitFor(() => { - expect(mockNotificationsManager.success).toHaveBeenCalled(); + expect(mockToast.success).toHaveBeenCalled(); }); }); }); diff --git a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx index 25c722b82eb..ba050b51c50 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx @@ -2,7 +2,7 @@ import { ConfigType, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig"; import { StoreModelInDBParams, useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB"; -import NotificationsManager from "@/components/molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { parseErrorMessage } from "@/components/shared/errorUtils"; import { Button, Form, Modal, Skeleton, Space, Switch, Typography } from "antd"; import React, { useEffect, useMemo } from "react"; @@ -44,16 +44,16 @@ const ModelSettingsModal: React.FC = ({ isVisible, onCa try { await mutateAsync(formValues, { onSuccess: () => { - NotificationsManager.success("Model storage settings updated successfully"); + toast.success("Model storage settings updated successfully"); refetch(); onSuccess?.(); }, onError: (error) => { - NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error)); + toast.fromError("Failed to save model storage settings: " + parseErrorMessage(error)); }, }); } catch (error) { - NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error)); + toast.fromError("Failed to save model storage settings: " + parseErrorMessage(error)); } }; diff --git a/ui/litellm-dashboard/src/components/model_group_alias_settings.tsx b/ui/litellm-dashboard/src/components/model_group_alias_settings.tsx index b130c455e71..c5e3b28f551 100644 --- a/ui/litellm-dashboard/src/components/model_group_alias_settings.tsx +++ b/ui/litellm-dashboard/src/components/model_group_alias_settings.tsx @@ -3,7 +3,7 @@ import { PlusCircleIcon, PencilIcon, TrashIcon, ChevronDownIcon, ChevronRightIco import { setCallbacksCall } from "./networking"; import { Card, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import NotificationsManager from "./molecules/notifications_manager"; +import { toast } from "@/lib/toast"; type ModelGroupAliasValue = string | { model: string; hidden?: boolean }; @@ -67,20 +67,20 @@ const ModelGroupAliasSettings: React.FC = ({ return true; } catch (error) { console.error("Failed to save model group alias settings:", error); - NotificationsManager.fromBackend("Failed to save model group alias settings"); + toast.fromError("Failed to save model group alias settings"); return false; } }; const handleAddAlias = async () => { if (!newAlias.aliasName || !newAlias.targetModelGroup) { - NotificationsManager.fromBackend("Please provide both alias name and target model group"); + toast.fromError("Please provide both alias name and target model group"); return; } // Check for duplicate alias names if (aliases.some((alias) => alias.aliasName === newAlias.aliasName)) { - NotificationsManager.fromBackend("An alias with this name already exists"); + toast.fromError("An alias with this name already exists"); return; } @@ -95,7 +95,7 @@ const ModelGroupAliasSettings: React.FC = ({ if (await saveAliasesToBackend(updatedAliases)) { setAliases(updatedAliases); setNewAlias({ aliasName: "", targetModelGroup: "" }); - NotificationsManager.success("Alias added successfully"); + toast.success("Alias added successfully"); } }; @@ -107,13 +107,13 @@ const ModelGroupAliasSettings: React.FC = ({ if (!editingAlias) return; if (!editingAlias.aliasName || !editingAlias.targetModelGroup) { - NotificationsManager.fromBackend("Please provide both alias name and target model group"); + toast.fromError("Please provide both alias name and target model group"); return; } // Check for duplicate alias names (excluding current alias) if (aliases.some((alias) => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) { - NotificationsManager.fromBackend("An alias with this name already exists"); + toast.fromError("An alias with this name already exists"); return; } @@ -122,7 +122,7 @@ const ModelGroupAliasSettings: React.FC = ({ if (await saveAliasesToBackend(updatedAliases)) { setAliases(updatedAliases); setEditingAlias(null); - NotificationsManager.success("Alias updated successfully"); + toast.success("Alias updated successfully"); } }; @@ -135,7 +135,7 @@ const ModelGroupAliasSettings: React.FC = ({ if (await saveAliasesToBackend(updatedAliases)) { setAliases(updatedAliases); - NotificationsManager.success("Alias deleted successfully"); + toast.success("Alias deleted successfully"); } }; diff --git a/ui/litellm-dashboard/src/components/model_info_view.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.test.tsx index 74fd398eaab..1f6b783d1b0 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event"; import React, { ReactNode } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import ModelInfoView from "./model_info_view"; -import NotificationsManager from "./molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import * as networking from "./networking"; vi.mock( "@/app/(dashboard)/hooks/autoRouter/useComplexityScorerDefaults", @@ -15,16 +15,6 @@ vi.mock("../../utils/dataUtils", () => ({ copyToClipboard: vi.fn().mockResolvedValue(true), })); -vi.mock("./molecules/notifications_manager", () => ({ - default: { - success: vi.fn(), - error: vi.fn(), - info: vi.fn(), - warning: vi.fn(), - fromBackend: vi.fn(), - }, -})); - vi.mock("./networking", () => ({ modelInfoV1Call: vi.fn(), credentialGetCall: vi.fn(), @@ -56,7 +46,7 @@ vi.mock("@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled", () => usePtuCostAttributionEnabled: () => mockUsePtuCostAttributionEnabled(), })); -const mockNotificationsManager = vi.mocked(NotificationsManager); +const mockToast = vi.mocked(toast); const mockModelInfoV1Call = vi.mocked(networking.modelInfoV1Call); const mockCredentialGetCall = vi.mocked(networking.credentialGetCall); const mockCredentialListCall = vi.mocked(networking.credentialListCall); @@ -259,7 +249,7 @@ describe("ModelInfoView", () => { await waitFor(() => { expect(mockTestConnectionRequest).toHaveBeenCalled(); - expect(mockNotificationsManager.success).toHaveBeenCalledWith("Connection test successful!"); + expect(mockToast.success).toHaveBeenCalledWith("Connection test successful!"); }); }); @@ -304,7 +294,7 @@ describe("ModelInfoView", () => { await user.click(testButton); await waitFor(() => { - expect(mockNotificationsManager.error).toHaveBeenCalled(); + expect(mockToast.error).toHaveBeenCalled(); }); }); @@ -522,7 +512,7 @@ describe("ModelInfoView", () => { await waitFor(() => { expect(mockModelPatchUpdateCall).toHaveBeenCalled(); - expect(mockNotificationsManager.success).toHaveBeenCalledWith("Model settings updated successfully"); + expect(mockToast.success).toHaveBeenCalledWith("Model settings updated successfully"); expect(mockOnModelUpdate).toHaveBeenCalled(); }); }); @@ -1053,7 +1043,7 @@ describe("ModelInfoView", () => { await userEvent.click(testConnectionButton); await waitFor(() => { - expect(mockNotificationsManager.warning).toHaveBeenCalledWith( + expect(mockToast.warning).toHaveBeenCalledWith( "No complexity tiers are configured yet, so there is nothing to test.", ); }); diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 0532238b872..b5f68677aeb 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -53,7 +53,7 @@ import CacheControlSettings from "./add_model/cache_control_settings"; import DeleteResourceModal from "./common_components/DeleteResourceModal"; import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal"; import ReuseCredentialsModal from "./model_add/reuse_credentials"; -import NotificationsManager from "./molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { CredentialItem, credentialCreateCall, @@ -383,9 +383,9 @@ export default function ModelInfoView({ custom_llm_provider: localModelData.litellm_params?.custom_llm_provider, }, }; - NotificationsManager.info("Storing credential.."); + toast.info("Storing credential.."); let credentialResponse = await credentialCreateCall(accessToken, credentialItem); - NotificationsManager.success("Credential stored successfully"); + toast.success("Credential stored successfully"); }; const handleModelUpdate = async (values: any) => { @@ -399,7 +399,7 @@ export default function ModelInfoView({ parsedExtraParams = values.litellm_extra_params ? JSON.parse(values.litellm_extra_params) : {}; delete parsedExtraParams.litellm_credential_name; } catch (e) { - NotificationsManager.fromBackend("Invalid JSON in LiteLLM Params"); + toast.fromError("Invalid JSON in LiteLLM Params"); setIsSaving(false); return; } @@ -511,7 +511,7 @@ export default function ModelInfoView({ } updatedModelInfo = applyPtuModelInfo(updatedModelInfo, values, ptuCostAttributionEnabled); } catch (e) { - NotificationsManager.fromBackend("Invalid JSON in Model Info"); + toast.fromError("Invalid JSON in Model Info"); return; } @@ -543,12 +543,12 @@ export default function ModelInfoView({ onModelUpdate(updatedModelData); } - NotificationsManager.success("Model settings updated successfully"); + toast.success("Model settings updated successfully"); setIsDirty(false); setIsEditing(false); } catch (error) { console.error("Error updating model:", error); - NotificationsManager.fromBackend("Failed to update model settings"); + toast.fromError("Failed to update model settings"); } finally { setIsSaving(false); } @@ -583,7 +583,7 @@ export default function ModelInfoView({ if (isComplexityRouterModel) { const targets = buildComplexityRouterTestTargets(localModelData ?? modelData); if (targets.length === 0) { - NotificationsManager.warning("No complexity tiers are configured yet, so there is nothing to test."); + toast.warning("No complexity tiers are configured yet, so there is nothing to test."); return; } setAutoRouterTestTargets(targets); @@ -592,7 +592,7 @@ export default function ModelInfoView({ return; } try { - NotificationsManager.info("Testing connection..."); + toast.info("Testing connection..."); const response = await testConnectionRequest( accessToken, { @@ -613,15 +613,15 @@ export default function ModelInfoView({ ); if (response.status === "success") { - NotificationsManager.success("Connection test successful!"); + toast.success("Connection test successful!"); } else { throw new Error(response?.result?.error || response?.message || "Unknown error"); } } catch (error) { if (error instanceof Error) { - NotificationsManager.error("Error testing connection: " + truncateString(error.message, 100)); + toast.error("Error testing connection: " + truncateString(error.message, 100)); } else { - NotificationsManager.error("Error testing connection: " + String(error)); + toast.error("Error testing connection: " + String(error)); } } }; @@ -631,7 +631,7 @@ export default function ModelInfoView({ setDeleteLoading(true); if (!accessToken) return; await modelDeleteCall(accessToken, modelId); - NotificationsManager.success("Model deleted successfully"); + toast.success("Model deleted successfully"); if (onModelUpdate) { onModelUpdate({ @@ -643,7 +643,7 @@ export default function ModelInfoView({ onClose(); } catch (error) { console.error("Error deleting the model:", error); - NotificationsManager.fromBackend("Failed to delete model"); + toast.fromError("Failed to delete model"); } finally { setDeleteLoading(false); setIsDeleteModalOpen(false); diff --git a/ui/litellm-dashboard/src/components/molecules/message_manager.test.ts b/ui/litellm-dashboard/src/components/molecules/message_manager.test.ts deleted file mode 100644 index 197a7442978..00000000000 --- a/ui/litellm-dashboard/src/components/molecules/message_manager.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createElement } from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const sonner = vi.hoisted(() => ({ - success: vi.fn(), - info: vi.fn(), - warning: vi.fn(), - error: vi.fn(), - dismiss: vi.fn(), -})); - -vi.mock("sonner", () => ({ toast: sonner })); -vi.mock("@/components/molecules/notifications_manager", () => - vi.importActual("./notifications_manager"), -); - -import MessageManager from "./message_manager"; -import NotificationManager from "./notifications_manager"; - -describe("legacy toast facades", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("MessageManager converts antd-era seconds into milliseconds", () => { - MessageManager.error("failed!", 5); - expect(sonner.error).toHaveBeenCalledWith("failed!", { description: undefined, duration: 5000 }); - }); - - it("MessageManager falls back to the kind default when no duration is given", () => { - MessageManager.success("done!"); - expect(sonner.success).toHaveBeenCalledWith("done!", { description: undefined, duration: 4000 }); - }); - - it("MessageManager.destroy and NotificationManager.clear both dismiss", () => { - MessageManager.destroy(); - NotificationManager.clear(); - expect(sonner.dismiss).toHaveBeenCalledTimes(2); - }); - - it("NotificationManager.fromBackend routes through toast.fromError", () => { - NotificationManager.fromBackend({ message: "Team not found", type: "not_found_error", code: "404" }); - expect(sonner.error).toHaveBeenCalledWith("Not Found", { description: "Team not found", duration: 6000 }); - }); - - it("NotificationManager.fromBackend converts the antd-era extra.duration seconds", () => { - NotificationManager.fromBackend("boom", { duration: 8 }); - expect(sonner.error).toHaveBeenCalledWith("Error", { description: "boom", duration: 8000 }); - }); - - it("NotificationManager keeps the antd config-object form: message is the title, description below it", () => { - NotificationManager.success({ - message: "MCP Server submitted for admin review", - description: "Once an admin approves it, the server will appear in your MCP Servers list.", - duration: 10, - }); - expect(sonner.success).toHaveBeenCalledWith("MCP Server submitted for admin review", { - description: "Once an admin approves it, the server will appear in your MCP Servers list.", - duration: 10000, - }); - }); - - it("NotificationManager falls back to the kind's title when a config object has no message", () => { - NotificationManager.warning({ description: "Heads up" }); - expect(sonner.warning).toHaveBeenCalledWith("Warning", { description: "Heads up", duration: 6000 }); - }); - - it("NotificationManager treats a React element as the message, not a config object", () => { - const element = createElement("span", null, "done"); - NotificationManager.info(element); - expect(sonner.info).toHaveBeenCalledWith(element, { description: undefined, duration: 4000 }); - }); -}); diff --git a/ui/litellm-dashboard/src/components/molecules/message_manager.tsx b/ui/litellm-dashboard/src/components/molecules/message_manager.tsx deleted file mode 100644 index 36ed0b52496..00000000000 --- a/ui/litellm-dashboard/src/components/molecules/message_manager.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { toast } from "@/lib/toast"; - -const secondsToMs = (seconds: number | undefined): number | undefined => - seconds === undefined ? undefined : seconds * 1000; - -const MessageManager = { - success: (content: string, duration?: number): void => toast.success(content, { durationMs: secondsToMs(duration) }), - error: (content: string, duration?: number): void => toast.error(content, { durationMs: secondsToMs(duration) }), - warning: (content: string, duration?: number): void => toast.warning(content, { durationMs: secondsToMs(duration) }), - info: (content: string, duration?: number): void => toast.info(content, { durationMs: secondsToMs(duration) }), - destroy: (): void => toast.dismiss(), -}; - -export default MessageManager; diff --git a/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx b/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx deleted file mode 100644 index 2805f22a52e..00000000000 --- a/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { isValidElement, type ReactNode } from "react"; -import { toast, type ToastKind } from "@/lib/toast"; - -export type NotificationConfig = { - readonly message?: ReactNode; - readonly description?: ReactNode; - readonly duration?: number; - readonly placement?: string; - readonly key?: string; -}; - -type NotificationInput = ReactNode | NotificationConfig; - -const FALLBACK_TITLES: Readonly> = { - success: "Success", - info: "Info", - warning: "Warning", - error: "Error", -}; - -const secondsToMs = (seconds: number | undefined): number | undefined => - seconds === undefined ? undefined : seconds * 1000; - -const isConfig = (input: NotificationInput): input is NotificationConfig => - input !== null && typeof input === "object" && !isValidElement(input) && !(Symbol.iterator in input); - -const show = (kind: ToastKind, input: NotificationInput): void => { - if (!isConfig(input)) { - toast[kind](input); - return; - } - toast[kind](input.message ?? FALLBACK_TITLES[kind], { - description: input.description, - durationMs: secondsToMs(input.duration), - }); -}; - -const NotificationManager = { - success: (input: NotificationInput): void => show("success", input), - info: (input: NotificationInput): void => show("info", input), - warning: (input: NotificationInput): void => show("warning", input), - error: (input: NotificationInput): void => show("error", input), - fromBackend: (input: unknown, extra?: Omit): void => - toast.fromError(input, { durationMs: secondsToMs(extra?.duration) }), - clear: (): void => toast.dismiss(), -}; - -export default NotificationManager; diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts index d32586e1774..86fe86307a2 100644 --- a/ui/litellm-dashboard/src/components/networking.test.ts +++ b/ui/litellm-dashboard/src/components/networking.test.ts @@ -9,15 +9,6 @@ vi.mock("@/utils/cookieUtils", () => ({ storeLoginToken: vi.fn(), })); -vi.mock("./molecules/notifications_manager", () => ({ - default: { - info: vi.fn(), - success: vi.fn(), - error: vi.fn(), - fromBackend: vi.fn(), - }, -})); - describe("networking - expired session handling", () => { const originalFetch = global.fetch; @@ -31,10 +22,10 @@ describe("networking - expired session handling", () => { it("should call clearTokenCookies on expired session", async () => { const errorData = "Authentication Error - Expired Key"; - const { default: NotificationsManager } = await import("./molecules/notifications_manager"); + const { toast } = await import("@/lib/toast"); if (errorData.includes("Authentication Error - Expired Key")) { - NotificationsManager.info("UI Session Expired. Logging out."); + toast.info("UI Session Expired. Logging out."); clearTokenCookies(); } diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 56293339361..a4ea85519eb 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -51,7 +51,7 @@ export const getAutoRouterClassifierDefaultPromptCall = async ( /** * Helper file for calls being made to proxy */ -import MessageManager from "@/components/molecules/message_manager"; +import { toast } from "@/lib/toast"; import { clearTokenCookies, getCookie, storeLoginToken } from "@/utils/cookieUtils"; import { decodeToken } from "@/utils/jwtUtils"; import { TagNewRequest, TagUpdateRequest, TagListResponse, TagInfoResponse } from "./tag_management/types"; @@ -60,7 +60,6 @@ import { EmailEventSettingsResponse, EmailEventSettingsUpdateRequest } from "./e import type { SkillRegisterRequest } from "./claude_code_plugins/types"; import type { ObjectPermission } from "./object_permission_types"; import { jsonFields } from "./common_components/check_openapi_schema"; -import NotificationsManager from "./molecules/notifications_manager"; import type { MCPUserEnvVarsStatus } from "./mcp_tools/types"; import type { CoordinationRedisSettings, @@ -340,7 +339,7 @@ export const handleError = async (errorData: string | any) => { // Convert errorData to string if it isn't already const errorString = typeof errorData === "string" ? errorData : JSON.stringify(errorData); if (errorString.includes("Authentication Error - Expired Key")) { - NotificationsManager.info("UI Session Expired. Logging out."); + toast.info("UI Session Expired. Logging out."); lastErrorTime = currentTime; clearTokenCookies(); const browserLocation = getWindowLocation(); @@ -609,10 +608,10 @@ export const modelCreateCall = async (accessToken: string, formValues: Model) => }); // Close any existing messages before showing new ones - MessageManager.destroy(); + toast.dismiss(); // Sequential success messages - NotificationsManager.success(`Model ${formValues.model_name} created successfully`); + toast.success(`Model ${formValues.model_name} created successfully`); return data; } catch (error) { @@ -1658,7 +1657,6 @@ export const modelInfoCall = async ( url += `?${params.toString()}`; } - //NotificationsManager.info("Requesting model data"); const response = await fetch(url, { method: "GET", headers: { @@ -1674,7 +1672,7 @@ export const modelInfoCall = async ( if (errorData.includes("No model list passed")) { errorData = "No Models Exist. Click Add Model to get started."; } - NotificationsManager.info(errorData); + toast.info(errorData); ModelListerrorShown = true; if (errorTimer) clearTimeout(errorTimer); @@ -1687,7 +1685,6 @@ export const modelInfoCall = async ( } const data = await response.json(); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -1790,9 +1787,7 @@ export const modelHubCall = async (accessToken: string) => { * Get all models on proxy */ try { - //NotificationsManager.info("Requesting model data"); const data = await apiClient.get(`/model_group/info`, { accessToken }); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -2056,9 +2051,7 @@ export const uiSpendLogsCall = async ({ export const adminSpendLogsCall = async (accessToken: string) => { try { - //NotificationsManager.info("Making spend logs request"); const data = await apiClient.get(`/global/spend/logs`, { accessToken }); - //NotificationsManager.success("Spend Logs received"); return data; } catch (error) { console.error("Failed to create key:", error); @@ -2070,7 +2063,6 @@ export const adminTopKeysCall = async (accessToken: string) => { try { let url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend/keys?limit=5` : `/global/spend/keys?limit=5`; - //NotificationsManager.info("Making spend keys request"); const response = await fetch(url, { method: "GET", headers: { @@ -2086,7 +2078,6 @@ export const adminTopKeysCall = async (accessToken: string) => { } const data = await response.json(); - //NotificationsManager.success("Spend Logs received"); return data; } catch (error) { console.error("Failed to create key:", error); @@ -2105,9 +2096,7 @@ export const adminTopEndUsersCall = async ( ? { api_key: keyToken, startTime: startTime, endTime: endTime } : { startTime: startTime, endTime: endTime }; - //NotificationsManager.info("Making top end users request"); const data = await apiClient.post(`/global/spend/end_users`, { accessToken, body }); - //NotificationsManager.success("Top End users received"); return data; } catch (error) { console.error("Failed to create key:", error); @@ -2191,7 +2180,6 @@ export const adminTopModelsCall = async (accessToken: string) => { try { let url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend/models?limit=5` : `/global/spend/models?limit=5`; - //NotificationsManager.info("Making top models request"); const response = await fetch(url, { method: "GET", headers: { @@ -2207,7 +2195,6 @@ export const adminTopModelsCall = async (accessToken: string) => { } const data = await response.json(); - //NotificationsManager.success("Top Models received"); return data; } catch (error) { console.error("Failed to create key:", error); @@ -2394,7 +2381,7 @@ export const keyInfoV1Call = async (accessToken: string, key: string) => { if (!response.ok) { const errorData = await response.text(); handleError(errorData); - NotificationsManager.fromBackend("Failed to fetch key info - " + errorData); + toast.fromError("Failed to fetch key info - " + errorData); } const data = await response.json(); @@ -2753,7 +2740,7 @@ export const teamUpdateCall = async ( const errorData = await response.text(); handleError(errorData); console.error("Error response from the server:", errorData); - NotificationsManager.fromBackend("Failed to update team settings: " + unwrapProxyErrorMessage(errorData)); + toast.fromError("Failed to update team settings: " + unwrapProxyErrorMessage(errorData)); throw new Error(errorData); } const data = (await response.json()) as { data: Team; team_id: string }; @@ -3095,7 +3082,6 @@ export const userUpdateUserCall = async ( user_id: string; data: UserInfo; }; - //NotificationsManager.success("User role updated"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3147,7 +3133,6 @@ export const userBulkUpdateUserCall = async ( successful_updates: number; failed_updates: number; }; - //NotificationsManager.success("User role updated"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3191,9 +3176,7 @@ export const getBudgetList = async (accessToken: string) => { * Get all configurable params for setting a budget */ try { - //NotificationsManager.info("Requesting model data"); const data = await apiClient.get(`/budget/list`, { accessToken }); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3206,9 +3189,7 @@ export const getCallbacksCall = async (accessToken: string, userID: string, user * Get all the models user has access to */ try { - //NotificationsManager.info("Requesting model data"); const data = await apiClient.get(`/get/config/callbacks`, { accessToken }); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3223,7 +3204,6 @@ export const getGeneralSettingsCall = async (accessToken: string) => { ? `${proxyBaseUrl}/config/list?config_type=general_settings` : `/config/list?config_type=general_settings`; - //NotificationsManager.info("Requesting model data"); const response = await fetch(url, { method: "GET", headers: { @@ -3240,7 +3220,6 @@ export const getGeneralSettingsCall = async (accessToken: string) => { } const data = await response.json(); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3348,9 +3327,7 @@ export const getPassThroughEndpointsCall = async (accessToken: string, teamId?: path += `/team/${teamId}`; } - //NotificationsManager.info("Requesting model data"); const data = await apiClient.get(path, { accessToken }); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3365,7 +3342,6 @@ export const getConfigFieldSetting = async (accessToken: string, fieldName: stri ? `${proxyBaseUrl}/config/field/info?field_name=${fieldName}` : `/config/field/info?field_name=${fieldName}`; - //NotificationsManager.info("Requesting model data"); const response = await fetch(url, { method: "GET", headers: { @@ -3395,14 +3371,12 @@ export const createPassThroughEndpoint = async (accessToken: string, formValues: * Set callbacks on proxy */ try { - //NotificationsManager.info("Requesting model data"); const data = await apiClient.post(`/config/pass_through_endpoint`, { accessToken, body: { ...formValues, // Include formValues in the request body }, }); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3418,10 +3392,8 @@ export const updateConfigFieldSetting = async (accessToken: string, fieldName: s field_value: fieldValue, config_type: "general_settings", }; - //NotificationsManager.info("Requesting model data"); const data = await apiClient.post(`/config/field/update`, { accessToken, body: formData }); - //NotificationsManager.info("Received model data"); - NotificationsManager.success("Successfully updated value!"); + toast.success("Successfully updated value!"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3436,9 +3408,8 @@ export const deleteConfigFieldSetting = async (accessToken: string, fieldName: s field_name: fieldName, config_type: "general_settings", }; - //NotificationsManager.info("Requesting model data"); const data = await apiClient.post(`/config/field/delete`, { accessToken, body: formData }); - NotificationsManager.success("Field reset on proxy"); + toast.success("Field reset on proxy"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3453,7 +3424,6 @@ export const deletePassThroughEndpointsCall = async (accessToken: string, endpoi ? `${proxyBaseUrl}/config/pass_through_endpoint?endpoint_id=${endpointId}` : `/config/pass_through_endpoint?endpoint_id=${endpointId}`; - //NotificationsManager.info("Requesting model data"); const response = await fetch(url, { method: "DELETE", headers: { @@ -3470,7 +3440,6 @@ export const deletePassThroughEndpointsCall = async (accessToken: string, endpoi } const data = await response.json(); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3484,14 +3453,12 @@ export const setCallbacksCall = async (accessToken: string, formValues: Record { try { let url = proxyBaseUrl ? `${proxyBaseUrl}/cache/ping` : `/cache/ping`; - //NotificationsManager.info("Requesting model data"); const response = await fetch(url, { method: "GET", headers: { @@ -3555,7 +3521,6 @@ export const cachingHealthCheckCall = async (accessToken: string) => { } const data = await response.json(); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -3598,9 +3563,7 @@ export const getProxyUISettings = async (accessToken: string) => { * Get all the models user has access to */ try { - //NotificationsManager.info("Requesting model data"); const data = await apiClient.get(`/sso/get/ui_settings`, { accessToken }); - //NotificationsManager.info("Received model data"); return data; // Handle success - you might want to update some state or UI based on the created key } catch (error) { @@ -6639,7 +6602,7 @@ export const updatePassThroughEndpoint = async ( } const data = await response.json(); - NotificationsManager.success("Pass through endpoint updated successfully"); + toast.success("Pass through endpoint updated successfully"); return data; } catch (error) { console.error("Failed to update pass through endpoint:", error); diff --git a/ui/litellm-dashboard/src/components/onboarding_link.tsx b/ui/litellm-dashboard/src/components/onboarding_link.tsx index b5f3c6d3e17..f25811719c1 100644 --- a/ui/litellm-dashboard/src/components/onboarding_link.tsx +++ b/ui/litellm-dashboard/src/components/onboarding_link.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Button, Modal, Typography } from "antd"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { Text } from "@tremor/react"; -import NotificationsManager from "./molecules/notifications_manager"; +import { toast } from "@/lib/toast"; export interface InvitationLink { id: string; @@ -100,7 +100,7 @@ export default function OnboardingModal({
- NotificationsManager.success("Copied!")}> + toast.success("Copied!")}> diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx index 98ce3827151..180f9d1f265 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.test.tsx @@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event"; import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils"; import { RegenerateKeyModal } from "./RegenerateKeyModal"; import { KeyResponse } from "../key_team_helpers/key_list"; +import { toast } from "@/lib/toast"; // Mock the networking call const mockRegenerateKeyCall = vi.fn(); @@ -10,14 +11,7 @@ vi.mock("../networking", () => ({ regenerateKeyCall: (...args: unknown[]) => mockRegenerateKeyCall(...args), })); -const mockNotificationFromBackend = vi.fn(); -const mockNotificationSuccess = vi.fn(); -vi.mock("../molecules/notifications_manager", () => ({ - default: { - fromBackend: (...args: unknown[]) => mockNotificationFromBackend(...args), - success: (...args: unknown[]) => mockNotificationSuccess(...args), - }, -})); +const mockNotificationFromBackend = vi.mocked(toast.fromError); const makeToken = (overrides: Partial = {}): KeyResponse => ({ diff --git a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx index 8138614982a..3902f3f736b 100644 --- a/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx +++ b/ui/litellm-dashboard/src/components/organisms/RegenerateKeyModal.tsx @@ -4,7 +4,7 @@ import { Alert, Button, Col, Flex, Form, Input, InputNumber, Modal, Row, Space, import { useEffect, useState } from "react"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { KeyResponse } from "../key_team_helpers/key_list"; -import NotificationManager from "../molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { regenerateKeyCall } from "../networking"; import { calculateExpiryPreviewFromDuration, formatExpiresUtc, isKeyExpired } from "@/utils/keyExpiryUtils"; @@ -62,7 +62,7 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat const response = await regenerateKeyCall(accessToken, selectedToken.token || selectedToken.token_id, formValues); setRegeneratedKey(response.key); - NotificationManager.success("Virtual Key regenerated successfully"); + toast.success("Virtual Key regenerated successfully"); // Build the update payload. Spread the API response first so any new // fields it returns (new token, timestamps, etc.) are captured, then @@ -94,7 +94,7 @@ export function RegenerateKeyModal({ selectedToken, visible, onClose, onKeyUpdat return; } console.error("Error regenerating key:", error); - NotificationManager.fromBackend(error); + toast.fromError(error); } }; diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx index 4f3cd4455a1..8df77246140 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx @@ -237,17 +237,6 @@ vi.mock("../networking", () => ({ getAgentsList: vi.fn().mockResolvedValue({ agents: [] }), })); -vi.mock("../molecules/notifications_manager", () => ({ - default: { - success: vi.fn(), - fromBackend: vi.fn(), - error: vi.fn(), - warning: vi.fn(), - info: vi.fn(), - clear: vi.fn(), - }, -})); - vi.mock("../agent_management/AgentSelector", () => ({ default: () => null })); vi.mock("../common_components/budget_duration_dropdown", () => ({ NEVER_RESETS_BUDGET_DURATION: "none", diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 8573c2f7415..d8cd537bc8a 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -42,7 +42,7 @@ import { Team } from "../key_team_helpers/key_list"; import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; import { NO_MCP_SERVERS_SENTINEL } from "../mcp_tools/constants"; import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions"; -import NotificationsManager from "../molecules/notifications_manager"; +import { toast } from "@/lib/toast"; import { getAgentsList, getGuardrailsList, @@ -371,14 +371,14 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp ); } - NotificationsManager.info("Making API Call"); + toast.info("Making API Call"); setIsModalVisible(true); if (keyOwner === "you") { formValues.user_id = userID; } else if (keyOwner === "agent") { if (!selectedAgentId) { - NotificationsManager.fromBackend("Please select an agent"); + toast.fromError("Please select an agent"); return; } formValues.agent_id = selectedAgentId; @@ -562,7 +562,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp queryClient.invalidateQueries({ queryKey: keyKeys.lists() }); setApiKey(response["key"]); - NotificationsManager.success("Virtual Key Created"); + toast.success("Virtual Key Created"); form.resetFields(); setBudgetLimits([]); setTagRateLimits([]); @@ -571,7 +571,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp localStorage.removeItem("userData" + userID); } catch (error) { const simplifiedError = simplifyKeyGenerateError(error); - NotificationsManager.fromBackend(simplifiedError); + toast.fromError(simplifiedError); } }; @@ -665,7 +665,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp setUserOptions(options); } catch (error) { console.error("Error fetching users:", error); - NotificationsManager.fromBackend("Failed to search for users"); + toast.fromError("Failed to search for users"); } finally { setUserSearchLoading(false); } diff --git a/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.test.tsx b/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.test.tsx index 1799795a428..3ffb11fb90d 100644 --- a/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.test.tsx +++ b/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.test.tsx @@ -4,10 +4,6 @@ import userEvent from "@testing-library/user-event"; import React from "react"; import { describe, expect, it, vi } from "vitest"; -vi.mock("@/components/molecules/notifications_manager", () => ({ - __esModule: true, - default: { success: vi.fn(), fromBackend: vi.fn() }, -})); vi.mock("@/components/ModelSelect/ModelSelect", () => ({ ModelSelect: ({ onChange }: { onChange: (values: string[]) => void }) => (