diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.test.tsx index 3b6534ab0f0..fe0ce2a06ef 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.test.tsx @@ -104,7 +104,12 @@ vi.mock("./add_policy_form", () => ({ vi.mock("./guardrail_selection_modal", () => ({ __esModule: true, - default: () => null, + default: ({ visible, onConfirm }: { visible: boolean; onConfirm: (defs: unknown[]) => void }) => + visible ? ( + + ) : null, })); vi.mock("./template_parameter_modal", () => ({ @@ -114,7 +119,20 @@ vi.mock("./template_parameter_modal", () => ({ vi.mock("./ai_suggestion_modal", () => ({ __esModule: true, - default: () => null, + default: ({ onSelectTemplates }: { onSelectTemplates: (templates: unknown[]) => void }) => ( + + ), })); vi.mock("./policy_test_panel", () => ({ @@ -196,3 +214,30 @@ describe("PoliciesPanel attachment delete", () => { }); }); }); + +describe("PoliciesPanel template queue", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("stops processing queued templates after unmount", async () => { + const user = userEvent.setup(); + const { unmount } = renderWithProviders(); + + await user.click(await screen.findByTestId("ai-suggestion-select-two")); + + const callsAfterFirstTemplate = networkingMocks.getGuardrailsList.mock.calls.length; + await user.click(await screen.findByTestId("guardrail-selection-confirm")); + + await waitFor(() => { + expect(networkingMocks.getGuardrailsList.mock.calls.length).toBe(callsAfterFirstTemplate + 1); + }); + + const callsBeforeUnmount = networkingMocks.getGuardrailsList.mock.calls.length; + unmount(); + + await new Promise((resolve) => setTimeout(resolve, 700)); + + expect(networkingMocks.getGuardrailsList.mock.calls.length).toBe(callsBeforeUnmount); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.tsx index 722d8face3d..ca25c13091a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/_components/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; import { Alert } from "antd"; @@ -66,6 +66,13 @@ const PoliciesPanel: React.FC = ({ accessToken, userRole }) const [loadedTemplates, setLoadedTemplates] = useState([]); const [templateQueue, setTemplateQueue] = useState([]); const [templateQueueProgress, setTemplateQueueProgress] = useState<{ current: number; total: number } | null>(null); + const templateQueueTimerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (templateQueueTimerRef.current !== null) clearTimeout(templateQueueTimerRef.current); + }; + }, []); const isAdmin = userRole ? isAdminRole(userRole) : false; @@ -338,7 +345,7 @@ const PoliciesPanel: React.FC = ({ accessToken, userRole }) setTemplateQueue(remaining); setTemplateQueueProgress((prev) => (prev ? { ...prev, current: prev.current + 1 } : null)); // Small delay so user can see the success message - setTimeout(() => handleUseTemplate(nextTemplate), 500); + templateQueueTimerRef.current = setTimeout(() => handleUseTemplate(nextTemplate), 500); } else { setTemplateQueueProgress(null); }