Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion ui/litellm-dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";

import AntdGlobalProvider from "@/contexts/AntdGlobalProvider";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
Expand All @@ -17,7 +19,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<body className={inter.className}>
<AntdGlobalProvider>{children}</AntdGlobalProvider>
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import React from "react";
import { notification } from "antd";
import { notification as staticNotification } from "antd";
import type { NotificationInstance } from "antd/es/notification/interface";
import { parseErrorMessage } from "../shared/errorUtils";
import { ArgsProps } from "antd/es/notification";

let notificationInstance: NotificationInstance | null = null;

export const setNotificationInstance = (instance: NotificationInstance) => {
notificationInstance = instance;
};

// Helper to get the best available notification instance
const getNotification = () => notificationInstance || staticNotification;
Comment on lines +7 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global mutable instance

notificationInstance is a module-level mutable singleton that gets set by setNotificationInstance() and then used by getNotification(). In Next.js/React this can lead to cross-request/user bleed in SSR/hydration scenarios (the module is shared), and also makes notification behavior depend on mount order (calls before AntdAppInit runs will silently use the static API). A safer pattern is to keep notifications entirely context-driven (or pass the instance explicitly) rather than storing it in a global variable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/litellm-dashboard/src/components/molecules/notifications_manager.tsx
Line: 7:14

Comment:
**Global mutable instance**

`notificationInstance` is a module-level mutable singleton that gets set by `setNotificationInstance()` and then used by `getNotification()`. In Next.js/React this can lead to cross-request/user bleed in SSR/hydration scenarios (the module is shared), and also makes notification behavior depend on mount order (calls before `AntdAppInit` runs will silently use the static API). A safer pattern is to keep notifications entirely context-driven (or pass the instance explicitly) rather than storing it in a global variable.

How can I resolve this? If you propose a fix, please make it concise.


type Placement = "top" | "topLeft" | "topRight" | "bottom" | "bottomLeft" | "bottomRight";

type NotificationConfig = {
Expand Down Expand Up @@ -251,7 +261,7 @@ function looksErrorPayload(input: any, status?: number): boolean {
const NotificationManager = {
error(input: string | NotificationConfig) {
const cfg = normalize(input, "Error");
notification.error({
getNotification().error({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
Expand All @@ -261,7 +271,7 @@ const NotificationManager = {

warning(input: string | NotificationConfig) {
const cfg = normalize(input, "Warning");
notification.warning({
getNotification().warning({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
Expand All @@ -271,7 +281,7 @@ const NotificationManager = {

info(input: string | NotificationConfig) {
const cfg = normalize(input, "Info");
notification.info({
getNotification().info({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
Expand All @@ -281,7 +291,7 @@ const NotificationManager = {

success(input: string | React.ReactNode | NotificationConfig) {
if (React.isValidElement(input)) {
notification.success({
getNotification().success({
...COMMON_NOTIFICATION_PROPS,
message: "Success",
description: input,
Expand All @@ -291,7 +301,7 @@ const NotificationManager = {
return;
}
const cfg = normalize(input as string | NotificationConfig, "Success");
notification.success({
getNotification().success({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
Expand All @@ -316,11 +326,11 @@ const NotificationManager = {
title === "Content Blocked" ||
title === "Integration Error"
) {
notification.warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 7 });
getNotification().warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 7 });
return;
}
if (title === "Server Error") {
notification.error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 8 });
getNotification().error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 8 });
return;
}
if (
Expand All @@ -331,10 +341,10 @@ const NotificationManager = {
title === "Error" ||
title === "Already Exists"
) {
notification.error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
getNotification().error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
return;
}
notification.info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 });
getNotification().info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 });
return;
}

Expand All @@ -343,18 +353,18 @@ const NotificationManager = {
const payload = { ...base, message: cls?.title ?? "Info" };

if (cls?.kind === "success") {
notification.success({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 3.5 });
getNotification().success({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 3.5 });
return;
}
if (cls?.kind === "warning") {
notification.warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
getNotification().warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
return;
}
notification.info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 });
getNotification().info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 });
},

clear() {
notification.destroy();
getNotification().destroy();
},
};

Expand Down
24 changes: 24 additions & 0 deletions ui/litellm-dashboard/src/contexts/AntdGlobalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import React, { useEffect, useRef } from "react";
import { notification } from "antd";
import { setNotificationInstance } from "@/components/molecules/notifications_manager";

export default function AntdGlobalProvider({ children }: { children: React.ReactNode }) {
const [api, contextHolder] = notification.useNotification();
const initialized = useRef(false);

useEffect(() => {
if (!initialized.current) {
setNotificationInstance(api);
initialized.current = true;
}
}, [api]);

return (
<>
{contextHolder}
{children}
</>
);
}
Loading