diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.test.tsx
index 9f27daab6b3..4e0590df72d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.test.tsx
@@ -1,36 +1,61 @@
-import { render, screen, waitFor } from "@testing-library/react";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { describe, expect, it, vi } from "vitest";
+import { type UrlUpdateEvent } from "nuqs/adapters/testing";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import userEvent from "@testing-library/user-event";
import GuardrailsMonitorView from "./GuardrailsMonitorView";
import * as networking from "@/components/networking";
+import { renderWithProviders, screen, testQueryClient, waitFor } from "@/../tests/test-utils";
vi.mock("@/components/networking", () => ({
getGuardrailsUsageOverview: vi.fn(),
+ getGuardrailsUsageDetail: vi.fn(),
+ getGuardrailsUsageLogs: vi.fn(),
formatDate: vi.fn((d: Date) => d.toISOString().slice(0, 10)),
}));
-const mockGetGuardrailsUsageOverview = vi.mocked(networking.getGuardrailsUsageOverview);
+vi.mock("@/components/GuardrailsMonitor/LogViewer", () => ({
+ LogViewer: ({ guardrailName }: { guardrailName: string }) =>
{guardrailName}
,
+}));
-function wrapper({ children }: { children: React.ReactNode }) {
- const queryClient = new QueryClient({
- defaultOptions: {
- queries: { retry: false },
- },
- });
- return {children};
-}
+const mockGetGuardrailsUsageOverview = vi.mocked(networking.getGuardrailsUsageOverview);
+const mockGetGuardrailsUsageDetail = vi.mocked(networking.getGuardrailsUsageDetail);
+const mockGetGuardrailsUsageLogs = vi.mocked(networking.getGuardrailsUsageLogs);
+
+const emptyOverview = { rows: [], chart: [], totalRequests: 0, totalBlocked: 0, passRate: 100 };
+
+const piiRow = {
+ id: "gr-pii",
+ name: "PII Guard",
+ type: "pii",
+ provider: "LiteLLM",
+ requestsEvaluated: 10,
+ failRate: 10,
+ status: "healthy" as const,
+ trend: "stable" as const,
+};
+
+const piiDetail = {
+ guardrail_name: "PII Guard",
+ description: "",
+ status: "healthy",
+ provider: "LiteLLM",
+ type: "pii",
+ requestsEvaluated: 10,
+ failRate: 10,
+ avgScore: 0.5,
+ avgLatency: 20,
+};
describe("GuardrailsMonitorView", () => {
- it("should render overview and fetch guardrails usage when accessToken is provided", async () => {
- mockGetGuardrailsUsageOverview.mockResolvedValue({
- rows: [],
- chart: [],
- totalRequests: 0,
- totalBlocked: 0,
- passRate: 100,
- });
+ beforeEach(() => {
+ testQueryClient.clear();
+ vi.clearAllMocks();
+ mockGetGuardrailsUsageOverview.mockResolvedValue(emptyOverview);
+ mockGetGuardrailsUsageDetail.mockResolvedValue(piiDetail);
+ mockGetGuardrailsUsageLogs.mockResolvedValue({ logs: [], total: 0 });
+ });
- render(, { wrapper });
+ it("should render overview and fetch guardrails usage when accessToken is provided", async () => {
+ renderWithProviders();
expect(await screen.findByRole("heading", { name: /Guardrails Monitor/i })).toBeInTheDocument();
await waitFor(() => {
@@ -39,7 +64,54 @@ describe("GuardrailsMonitorView", () => {
});
it("should render without crashing when accessToken is null", async () => {
- render(, { wrapper });
+ renderWithProviders();
expect(await screen.findByRole("heading", { name: /Guardrails Monitor/i })).toBeInTheDocument();
});
+
+ describe("guardrail detail deep link (?guardrail=)", () => {
+ it("should open the detail view directly from a ?guardrail= deep link", async () => {
+ renderWithProviders(, { searchParams: "?guardrail=gr-pii" });
+
+ expect(await screen.findByRole("heading", { name: "PII Guard" })).toBeInTheDocument();
+ expect(mockGetGuardrailsUsageDetail).toHaveBeenCalledWith(
+ "test-token",
+ "gr-pii",
+ expect.any(String),
+ expect.any(String),
+ );
+ expect(screen.queryByRole("heading", { name: /Guardrails Monitor/i })).not.toBeInTheDocument();
+ });
+
+ it("should push ?guardrail= as a new history entry when a guardrail is selected", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
+ mockGetGuardrailsUsageOverview.mockResolvedValue({ ...emptyOverview, rows: [piiRow] });
+ renderWithProviders(, { onUrlUpdate });
+
+ await user.click(await screen.findByRole("button", { name: "PII Guard" }));
+
+ await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
+ const lastUpdate = onUrlUpdate.mock.calls.at(-1)![0];
+ expect(lastUpdate.searchParams.get("guardrail")).toBe("gr-pii");
+ expect(lastUpdate.options.history).toBe("push");
+ expect(await screen.findByRole("heading", { name: "PII Guard" })).toBeInTheDocument();
+ });
+
+ it("should clear ?guardrail= by replacing history when going back to the overview", async () => {
+ const user = userEvent.setup();
+ const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
+ renderWithProviders(, {
+ searchParams: "?guardrail=gr-pii",
+ onUrlUpdate,
+ });
+
+ await user.click(await screen.findByRole("button", { name: /back to overview/i }));
+
+ await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
+ const lastUpdate = onUrlUpdate.mock.calls.at(-1)![0];
+ expect(lastUpdate.searchParams.has("guardrail")).toBe(false);
+ expect(lastUpdate.options.history).toBe("replace");
+ expect(await screen.findByRole("heading", { name: /Guardrails Monitor/i })).toBeInTheDocument();
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx
index a9acf3e6377..f90a46e19e4 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx
@@ -1,12 +1,11 @@
import type { DateRangePickerValue } from "@/components/shared/date_picker_types";
+import { parseAsString, useQueryState } from "nuqs";
import React, { useCallback, useMemo, useState } from "react";
import { formatDate } from "@/components/networking";
import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
import { GuardrailDetail } from "./GuardrailDetail";
import { GuardrailsOverview } from "./GuardrailsOverview";
-type View = { type: "overview" } | { type: "detail"; guardrailId: string };
-
interface GuardrailsMonitorViewProps {
accessToken?: string | null;
}
@@ -16,7 +15,10 @@ const defaultStart = new Date();
defaultStart.setDate(defaultStart.getDate() - 7);
export default function GuardrailsMonitorView({ accessToken = null }: GuardrailsMonitorViewProps) {
- const [view, setView] = useState({ type: "overview" });
+ const [selectedGuardrailId, setSelectedGuardrailId] = useQueryState(
+ "guardrail",
+ parseAsString.withOptions({ history: "push" }),
+ );
const initialFrom = useMemo(() => new Date(defaultStart), []);
const initialTo = useMemo(() => new Date(defaultEnd), []);
@@ -34,11 +36,11 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails
}, []);
const handleSelectGuardrail = (id: string) => {
- setView({ type: "detail", guardrailId: id });
+ void setSelectedGuardrailId(id);
};
const handleBack = () => {
- setView({ type: "overview" });
+ void setSelectedGuardrailId(null, { history: "replace" });
};
const dateRangeControl = (
@@ -47,7 +49,7 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails
return (
- {view.type === "overview" ? (
+ {!selectedGuardrailId ? (
{dateRangeControl}
({
getGuardrailsList: vi.fn(),
@@ -15,16 +16,21 @@ vi.mock("./add_guardrail_form", () => ({
vi.mock("./guardrail_table", () => ({
__esModule: true,
- default: ({ guardrailsList, onDeleteClick }: any) => (
+ default: ({ guardrailsList, onDeleteClick, onGuardrailClick }: any) => (
Mock Guardrail Table
{guardrailsList.length > 0 && (
-
+ <>
+
+
+ >
)}
),
@@ -32,7 +38,12 @@ vi.mock("./guardrail_table", () => ({
vi.mock("./guardrail_info", () => ({
__esModule: true,
- default: () => Mock Guardrail Info View
,
+ default: ({ guardrailId, onClose }: { guardrailId: string; onClose: () => void }) => (
+
+
Mock Guardrail Info View {guardrailId}
+
+
+ ),
}));
vi.mock("./GuardrailTestPlayground", async () => {
@@ -112,7 +123,7 @@ describe("GuardrailsPanel", () => {
});
it("should render the component", async () => {
- render();
+ renderWithProviders();
expect(screen.getByText("Guardrails")).toBeInTheDocument();
// Activate the Guardrails tab so its content (including the Add button) is rendered
fireEvent.click(screen.getByText("Guardrails"));
@@ -120,7 +131,7 @@ describe("GuardrailsPanel", () => {
});
it("should delete the clicked guardrail after confirming in the modal", async () => {
- render();
+ renderWithProviders();
fireEvent.click(screen.getByText("Guardrails"));
fireEvent.click(await screen.findByTestId("delete-button"));
@@ -139,14 +150,14 @@ describe("GuardrailsPanel", () => {
});
it("should mount every tab panel up front so panel state survives tab switches", async () => {
- render();
+ renderWithProviders();
expect(await screen.findByLabelText("playground draft")).toBeInTheDocument();
expect(screen.getByText("Mock Team Guardrails Tab")).toBeInTheDocument();
});
it("should keep test playground state when switching tabs away and back", async () => {
- render();
+ renderWithProviders();
fireEvent.click(screen.getByText("Test Playground"));
@@ -161,7 +172,7 @@ describe("GuardrailsPanel", () => {
});
it("should not delete anything when the modal is cancelled", async () => {
- render();
+ renderWithProviders();
fireEvent.click(screen.getByText("Guardrails"));
fireEvent.click(await screen.findByTestId("delete-button"));
@@ -171,4 +182,42 @@ describe("GuardrailsPanel", () => {
expect(mockDeleteGuardrailCall).not.toHaveBeenCalled();
});
+
+ describe("guardrail detail deep link (?guardrail=)", () => {
+ it("should open the guardrail info view directly from a ?guardrail= deep link", async () => {
+ renderWithProviders(, { searchParams: "?guardrail=test-guardrail-1" });
+
+ expect(await screen.findByTestId("guardrail-info-view")).toHaveTextContent("test-guardrail-1");
+ expect(screen.queryByText("Mock Guardrail Table")).not.toBeInTheDocument();
+ });
+
+ it("should push ?guardrail= as a new history entry when a guardrail row is clicked", async () => {
+ const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
+ renderWithProviders(, { onUrlUpdate });
+
+ fireEvent.click(await screen.findByTestId("open-button"));
+
+ await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
+ const lastUpdate = onUrlUpdate.mock.calls.at(-1)![0];
+ expect(lastUpdate.searchParams.get("guardrail")).toBe("test-guardrail-1");
+ expect(lastUpdate.options.history).toBe("push");
+ expect(await screen.findByTestId("guardrail-info-view")).toHaveTextContent("test-guardrail-1");
+ });
+
+ it("should clear ?guardrail= by replacing history when the info view is closed", async () => {
+ const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>();
+ renderWithProviders(, {
+ searchParams: "?guardrail=test-guardrail-1",
+ onUrlUpdate,
+ });
+
+ fireEvent.click(await screen.findByRole("button", { name: "Close Guardrail Info" }));
+
+ await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled());
+ const lastUpdate = onUrlUpdate.mock.calls.at(-1)![0];
+ expect(lastUpdate.searchParams.has("guardrail")).toBe(false);
+ expect(lastUpdate.options.history).toBe("replace");
+ expect(await screen.findByText("Mock Guardrail Table")).toBeInTheDocument();
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx
index 7e59abf8e3d..901e39004f3 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.tsx
@@ -1,3 +1,4 @@
+import { parseAsString, useQueryState } from "nuqs";
import React, { useState, useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ChevronDown, Code, Plus } from "lucide-react";
@@ -40,7 +41,10 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
const [isDeleting, setIsDeleting] = useState(false);
const [guardrailToDelete, setGuardrailToDelete] = useState(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const [selectedGuardrailId, setSelectedGuardrailId] = useState(null);
+ const [selectedGuardrailId, setSelectedGuardrailId] = useQueryState(
+ "guardrail",
+ parseAsString.withOptions({ history: "push" }),
+ );
const isAdmin = userRole ? isAdminRole(userRole) : false;
const fetchGuardrails = async () => {
@@ -63,16 +67,20 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
fetchGuardrails();
}, [accessToken]);
+ const closeGuardrailDetail = () => {
+ void setSelectedGuardrailId(null, { history: "replace" });
+ };
+
const handleAddGuardrail = () => {
if (selectedGuardrailId) {
- setSelectedGuardrailId(null);
+ closeGuardrailDetail();
}
setIsAddModalVisible(true);
};
const handleAddCustomCodeGuardrail = () => {
if (selectedGuardrailId) {
- setSelectedGuardrailId(null);
+ closeGuardrailDetail();
}
setIsCustomCodeModalVisible(true);
};
@@ -175,7 +183,7 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
{selectedGuardrailId ? (
setSelectedGuardrailId(null)}
+ onClose={closeGuardrailDetail}
accessToken={accessToken}
isAdmin={isAdmin}
/>
@@ -184,7 +192,7 @@ const GuardrailsPanel: React.FC = ({ accessToken, userRole
guardrailsList={guardrailsList}
isLoading={isLoading}
onDeleteClick={handleDeleteClick}
- onGuardrailClick={(id) => setSelectedGuardrailId(id)}
+ onGuardrailClick={(id) => void setSelectedGuardrailId(id)}
/>
)}