From db7454c6ee2b838c983b0b6419c34080c458043c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 13 Dec 2025 15:16:13 -0800 Subject: [PATCH] Show progress and pause on hover for notifications --- .../molecules/notifications_manager.test.tsx | 23 ++++++++++++++++- .../molecules/notifications_manager.tsx | 25 +++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/ui/litellm-dashboard/src/components/molecules/notifications_manager.test.tsx b/ui/litellm-dashboard/src/components/molecules/notifications_manager.test.tsx index dabca71746..c886f78b4b 100644 --- a/ui/litellm-dashboard/src/components/molecules/notifications_manager.test.tsx +++ b/ui/litellm-dashboard/src/components/molecules/notifications_manager.test.tsx @@ -1,6 +1,6 @@ import { notification } from "antd"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import NotificationManager from "./notifications_manager"; +import NotificationManager, { COMMON_NOTIFICATION_PROPS } from "./notifications_manager"; vi.mock("@/components/molecules/notifications_manager", async () => { const actual = await vi.importActual( @@ -46,4 +46,25 @@ describe("NotificationManager", () => { ); }); }); + + describe("COMMON_NOTIFICATION_PROPS", () => { + const notificationTypes = [ + { type: "error", method: NotificationManager.error, mockFn: notification.error }, + { type: "warning", method: NotificationManager.warning, mockFn: notification.warning }, + { type: "info", method: NotificationManager.info, mockFn: notification.info }, + { type: "success", method: NotificationManager.success, mockFn: notification.success }, + ]; + + notificationTypes.forEach(({ type, method, mockFn }) => { + it(`should pass COMMON_NOTIFICATION_PROPS to ${type} notifications`, () => { + method(`Test ${type}`); + + expect(mockFn).toHaveBeenCalledWith( + expect.objectContaining({ + ...COMMON_NOTIFICATION_PROPS, + }), + ); + }); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx b/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx index 6c6a0e38f2..71b14bee88 100644 --- a/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx +++ b/ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx @@ -1,6 +1,7 @@ import React from "react"; import { notification } from "antd"; import { parseErrorMessage } from "../shared/errorUtils"; +import { ArgsProps } from "antd/es/notification"; type Placement = "top" | "topLeft" | "topRight" | "bottom" | "bottomLeft" | "bottomRight"; @@ -234,6 +235,11 @@ function extractDescription(input: any): string { return parseErrorMessage(backendMsg); } +export const COMMON_NOTIFICATION_PROPS: Partial = { + showProgress: true, + pauseOnHover: true, +}; + function looksErrorPayload(input: any, status?: number): boolean { if (status !== undefined) return true; if (input instanceof Error) return true; @@ -246,6 +252,7 @@ const NotificationManager = { error(input: string | NotificationConfig) { const cfg = normalize(input, "Error"); notification.error({ + ...COMMON_NOTIFICATION_PROPS, ...cfg, placement: cfg.placement ?? defaultPlacement(), duration: cfg.duration ?? 6, @@ -255,6 +262,7 @@ const NotificationManager = { warning(input: string | NotificationConfig) { const cfg = normalize(input, "Warning"); notification.warning({ + ...COMMON_NOTIFICATION_PROPS, ...cfg, placement: cfg.placement ?? defaultPlacement(), duration: cfg.duration ?? 5, @@ -264,6 +272,7 @@ const NotificationManager = { info(input: string | NotificationConfig) { const cfg = normalize(input, "Info"); notification.info({ + ...COMMON_NOTIFICATION_PROPS, ...cfg, placement: cfg.placement ?? defaultPlacement(), duration: cfg.duration ?? 4, @@ -273,6 +282,7 @@ const NotificationManager = { success(input: string | React.ReactNode | NotificationConfig) { if (React.isValidElement(input)) { notification.success({ + ...COMMON_NOTIFICATION_PROPS, message: "Success", description: input, placement: defaultPlacement(), @@ -282,6 +292,7 @@ const NotificationManager = { } const cfg = normalize(input as string | NotificationConfig, "Success"); notification.success({ + ...COMMON_NOTIFICATION_PROPS, ...cfg, placement: cfg.placement ?? defaultPlacement(), duration: cfg.duration ?? 3.5, @@ -305,11 +316,11 @@ const NotificationManager = { title === "Content Blocked" || title === "Integration Error" ) { - notification.warning({ ...payload, duration: extra?.duration ?? 7 }); + notification.warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 7 }); return; } if (title === "Server Error") { - notification.error({ ...payload, duration: extra?.duration ?? 8 }); + notification.error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 8 }); return; } if ( @@ -320,10 +331,10 @@ const NotificationManager = { title === "Error" || title === "Already Exists" ) { - notification.error({ ...payload, duration: extra?.duration ?? 6 }); + notification.error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 }); return; } - notification.info({ ...payload, duration: extra?.duration ?? 4 }); + notification.info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 }); return; } @@ -332,14 +343,14 @@ const NotificationManager = { const payload = { ...base, message: cls?.title ?? "Info" }; if (cls?.kind === "success") { - notification.success({ ...payload, duration: extra?.duration ?? 3.5 }); + notification.success({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 3.5 }); return; } if (cls?.kind === "warning") { - notification.warning({ ...payload, duration: extra?.duration ?? 6 }); + notification.warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 }); return; } - notification.info({ ...payload, duration: extra?.duration ?? 4 }); + notification.info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 }); }, clear() {