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);
}