diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 042570aff0a..063aee5f08f 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -321,9 +321,6 @@ "src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx": { "no-nested-ternary": { "count": 5 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/guardrails/_components/GuardrailTestPanel.tsx": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.test.tsx index 9bfa71f77be..b616982d69b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -14,7 +14,7 @@ vi.mock("./ScoreChart", () => ({ })); vi.mock("./EvaluationSettingsModal", () => ({ - EvaluationSettingsModal: () => null, + EvaluationSettingsModal: ({ open }: { open: boolean }) => (open ?
Evaluation settings modal
: null), })); const mockGetGuardrailsUsageOverview = vi.mocked(networking.getGuardrailsUsageOverview); @@ -28,6 +28,18 @@ function wrapper({ children }: { children: React.ReactNode }) { return {children}; } +function renderOverview(onSelectGuardrail = vi.fn()) { + return render( + , + { wrapper }, + ); +} + describe("GuardrailsOverview", () => { beforeEach(() => { vi.clearAllMocks(); @@ -92,4 +104,58 @@ describe("GuardrailsOverview", () => { expect(onSelectGuardrail).toHaveBeenCalledWith("guardrail-low"); }); + + it("renders the page header and the export action", async () => { + renderOverview(); + + expect(await screen.findByRole("heading", { name: "Guardrails Monitor", level: 1 })).toBeInTheDocument(); + expect(screen.getByText("Monitor guardrail performance across all requests")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Export Data/i })).toBeInTheDocument(); + }); + + it("renders every summary metric card", async () => { + renderOverview(); + + expect(await screen.findByText("1,500")).toBeInTheDocument(); + expect(screen.getByText("Total Evaluations")).toBeInTheDocument(); + expect(screen.getByText("Blocked Requests")).toBeInTheDocument(); + expect(screen.getByText("84")).toBeInTheDocument(); + expect(screen.getByText("Pass Rate")).toBeInTheDocument(); + expect(screen.getByText("94.4%")).toBeInTheDocument(); + expect(screen.getByText("23ms")).toBeInTheDocument(); + expect(screen.getByText("Active Guardrails")).toBeInTheDocument(); + expect(screen.getByText("2")).toBeInTheDocument(); + }); + + it("renders the table toolbar heading and its description", async () => { + renderOverview(); + + expect(await screen.findByRole("heading", { name: "Guardrail Performance", level: 5 })).toBeInTheDocument(); + expect(screen.getByText("Click a guardrail to view details, logs, and configuration")).toBeInTheDocument(); + }); + + it("opens the evaluation settings modal from the toolbar action", async () => { + const user = userEvent.setup(); + renderOverview(); + + expect(screen.queryByText("Evaluation settings modal")).not.toBeInTheDocument(); + + await user.click(await screen.findByTitle("Evaluation settings")); + + expect(await screen.findByText("Evaluation settings modal")).toBeInTheDocument(); + }); + + it("marks the overview busy while the usage request is in flight", async () => { + mockGetGuardrailsUsageOverview.mockReturnValue(new Promise(() => {})); + renderOverview(); + + await waitFor(() => expect(document.querySelector('[aria-busy="true"]')).toBeInTheDocument()); + }); + + it("shows a failure message when the usage request rejects", async () => { + mockGetGuardrailsUsageOverview.mockRejectedValue(new Error("network down")); + renderOverview(); + + expect(await screen.findByText("Failed to load data. Try again.")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx index 8d45b4a4ee8..e1048c5322f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx @@ -1,11 +1,12 @@ -import { DownloadOutlined, RiseOutlined, SafetyOutlined, SettingOutlined, WarningOutlined } from "@ant-design/icons"; import { useQuery } from "@tanstack/react-query"; import type { ColumnDef, OnChangeFn, SortingState } from "@tanstack/react-table"; -import { Button, Col, Row, Spin, Typography } from "antd"; +import { Download, Settings, Shield, TrendingUp, TriangleAlert } from "lucide-react"; import React, { useMemo, useState } from "react"; import { DataTable, DataTableSortHeader } from "@/components/shared/DataTable"; import { getGuardrailsUsageOverview } from "@/components/networking"; import { type PerformanceRow } from "@/components/GuardrailsMonitor/mockData"; +import { Button } from "@/components/ui/button"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { EvaluationSettingsModal } from "./EvaluationSettingsModal"; import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard"; import { ScoreChart } from "./ScoreChart"; @@ -197,51 +198,42 @@ export function GuardrailsOverview({
- +

Guardrails Monitor

Monitor guardrail performance across all requests

-
- - - - - - } - /> - - - } - /> - - - 150 ? "text-red-600" : metrics.avgLatency > 50 ? "text-amber-600" : "text-green-600" - } - /> - - - - - +
+ + } + /> + } + /> + 150 ? "text-red-600" : metrics.avgLatency > 50 ? "text-amber-600" : "text-green-600" + } + /> + +
@@ -250,7 +242,11 @@ export function GuardrailsOverview({
{(isLoading || error) && (
- {isLoading && } + {isLoading && ( + + + + )} {error && Failed to load data. Try again.}
)} @@ -270,20 +266,20 @@ export function GuardrailsOverview({ toolbar={() => (
- - Guardrail Performance - +
Guardrail Performance

Click a guardrail to view details, logs, and configuration

)}