diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index ca6364b005e..a4dcf0f6c73 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -320,10 +320,10 @@
},
"src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx": {
"no-nested-ternary": {
- "count": 8
+ "count": 5
},
"no-restricted-imports": {
- "count": 2
+ "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
new file mode 100644
index 00000000000..9bfa71f77be
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.test.tsx
@@ -0,0 +1,95 @@
+import { render, screen } 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";
+import * as networking from "@/components/networking";
+import { GuardrailsOverview } from "./GuardrailsOverview";
+
+vi.mock("@/components/networking", () => ({
+ getGuardrailsUsageOverview: vi.fn(),
+}));
+
+vi.mock("./ScoreChart", () => ({
+ ScoreChart: () =>
Score chart
,
+}));
+
+vi.mock("./EvaluationSettingsModal", () => ({
+ EvaluationSettingsModal: () => null,
+}));
+
+const mockGetGuardrailsUsageOverview = vi.mocked(networking.getGuardrailsUsageOverview);
+
+function wrapper({ children }: { children: React.ReactNode }) {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ },
+ });
+ return {children};
+}
+
+describe("GuardrailsOverview", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockGetGuardrailsUsageOverview.mockResolvedValue({
+ rows: [
+ {
+ id: "guardrail-low",
+ name: "Low Failure Guardrail",
+ type: "content_filter",
+ provider: "LiteLLM",
+ requestsEvaluated: 1200,
+ failRate: 2.5,
+ avgLatency: 45,
+ status: "healthy",
+ trend: "down",
+ },
+ {
+ id: "guardrail-high",
+ name: "High Failure Guardrail",
+ type: "content_filter",
+ provider: "Bedrock",
+ requestsEvaluated: 300,
+ failRate: 18,
+ status: "warning",
+ trend: "up",
+ },
+ ],
+ chart: [],
+ totalRequests: 1500,
+ totalBlocked: 84,
+ passRate: 94.4,
+ });
+ });
+
+ it("renders performance data and selects a guardrail", async () => {
+ const onSelectGuardrail = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ ,
+ { wrapper },
+ );
+
+ expect(await screen.findByRole("columnheader", { name: "Guardrail" })).toBeInTheDocument();
+ expect(screen.getByRole("columnheader", { name: /Requests/ })).toBeInTheDocument();
+ expect(screen.getByRole("columnheader", { name: /Fail Rate/ })).toBeInTheDocument();
+ expect(await screen.findByText("Low Failure Guardrail")).toBeInTheDocument();
+ expect(screen.getByText("1,200")).toBeInTheDocument();
+ expect(screen.getByText("18%")).toBeInTheDocument();
+ expect(screen.getByText("45ms")).toBeInTheDocument();
+
+ const rows = screen.getAllByRole("row");
+ expect(rows[1]).toHaveTextContent("High Failure Guardrail");
+ expect(rows[2]).toHaveTextContent("Low Failure Guardrail");
+
+ await user.click(screen.getByRole("button", { name: "Low Failure Guardrail" }));
+
+ expect(onSelectGuardrail).toHaveBeenCalledWith("guardrail-low");
+ });
+});
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 9d57f36f93f..8d45b4a4ee8 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,8 +1,9 @@
import { DownloadOutlined, RiseOutlined, SafetyOutlined, SettingOutlined, WarningOutlined } from "@ant-design/icons";
import { useQuery } from "@tanstack/react-query";
-import { Button, Card, Col, Row, Spin, Table, Typography } from "antd";
-import type { ColumnsType } from "antd/es/table";
+import type { ColumnDef, OnChangeFn, SortingState } from "@tanstack/react-table";
+import { Button, Col, Row, Spin, Typography } from "antd";
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 { EvaluationSettingsModal } from "./EvaluationSettingsModal";
@@ -82,99 +83,112 @@ export function GuardrailsOverview({
const isLoading = guardrailsLoading;
const error = guardrailsError;
- const columns: ColumnsType = [
+ const columns: ColumnDef[] = [
{
- title: "Guardrail",
- dataIndex: "name",
- key: "name",
- render: (name: string, row) => (
+ header: "Guardrail",
+ accessorKey: "name",
+ enableSorting: false,
+ cell: ({ row }) => (
),
},
{
- title: "Provider",
- dataIndex: "provider",
- key: "provider",
- render: (provider: string) => (
+ header: "Provider",
+ accessorKey: "provider",
+ enableSorting: false,
+ cell: ({ row }) => (
- {provider}
+ {row.original.provider}
),
},
{
- title: "Requests",
- dataIndex: "requestsEvaluated",
- key: "requestsEvaluated",
- align: "right",
- sorter: true,
- sortOrder: sortBy === "requestsEvaluated" ? (sortDir === "desc" ? "descend" : "ascend") : null,
- render: (v: number) => v.toLocaleString(),
+ header: ({ column }) => ,
+ accessorKey: "requestsEvaluated",
+ meta: { numeric: true },
+ sortDescFirst: false,
+ cell: ({ row }) => row.original.requestsEvaluated.toLocaleString(),
},
{
- title: "Fail Rate",
- dataIndex: "failRate",
- key: "failRate",
- align: "right",
- sorter: true,
- sortOrder: sortBy === "failRate" ? (sortDir === "desc" ? "descend" : "ascend") : null,
- render: (v: number, row) => (
- 15 ? "text-red-600" : v > 5 ? "text-amber-600" : "text-green-600"}>
- {v}%{row.trend === "up" && ↑}
- {row.trend === "down" && ↓}
-
- ),
- },
- {
- title: "Avg. latency added",
- dataIndex: "avgLatency",
- key: "avgLatency",
- align: "right",
- sorter: true,
- sortOrder: sortBy === "avgLatency" ? (sortDir === "desc" ? "descend" : "ascend") : null,
- render: (v?: number) => (
+ header: ({ column }) => ,
+ accessorKey: "failRate",
+ meta: { numeric: true },
+ sortDescFirst: false,
+ cell: ({ row }) => (
150 ? "text-red-600" : v > 50 ? "text-amber-600" : "text-green-600"
+ row.original.failRate > 15
+ ? "text-red-600"
+ : row.original.failRate > 5
+ ? "text-amber-600"
+ : "text-green-600"
}
>
- {v != null ? `${v}ms` : "—"}
+ {row.original.failRate}%{row.original.trend === "up" && ↑}
+ {row.original.trend === "down" && ↓}
),
},
{
- title: "Status",
- dataIndex: "status",
- key: "status",
- align: "center",
- render: (status: string) => (
+ header: ({ column }) => ,
+ accessorKey: "avgLatency",
+ meta: { numeric: true },
+ sortDescFirst: false,
+ cell: ({ row }) => (
+ 150
+ ? "text-red-600"
+ : row.original.avgLatency > 50
+ ? "text-amber-600"
+ : "text-green-600"
+ }
+ >
+ {row.original.avgLatency != null ? `${row.original.avgLatency}ms` : "—"}
+
+ ),
+ },
+ {
+ header: "Status",
+ accessorKey: "status",
+ enableSorting: false,
+ cell: ({ row }) => (
- {status}
+ {row.original.status}
),
},
];
const sortableKeys: SortKey[] = ["failRate", "requestsEvaluated", "avgLatency"];
- const handleTableChange = (_pagination: unknown, _filters: unknown, sorter: unknown) => {
- const s = sorter as { field?: keyof PerformanceRow; order?: string };
- if (s?.field && sortableKeys.includes(s.field as SortKey)) {
- setSortBy(s.field as SortKey);
- setSortDir(s.order === "ascend" ? "asc" : "desc");
+ const sorting = useMemo(() => [{ id: sortBy, desc: sortDir === "desc" }], [sortBy, sortDir]);
+ const handleSortingChange: OnChangeFn = (updater) => {
+ const nextSorting = typeof updater === "function" ? updater(sorting) : updater;
+ const primarySort = nextSorting[0];
+ if (primarySort && sortableKeys.includes(primarySort.id as SortKey)) {
+ setSortBy(primarySort.id as SortKey);
+ setSortDir(primarySort.desc ? "desc" : "asc");
}
};
@@ -233,43 +247,48 @@ export function GuardrailsOverview({
-
+
{(isLoading || error) && (
-
+
{isLoading && }
{error && Failed to load data. Try again.}
)}
-
-
-
- Guardrail Performance
-
-
Click a guardrail to view details, logs, and configuration
-
-
- }
- onClick={() => setEvaluationModalOpen(true)}
- title="Evaluation settings"
- />
-
-
-
({
- onClick: () => onSelectGuardrail(row.id),
- style: { cursor: "pointer" },
- })}
+ data={sorted}
+ getRowId={(row) => row.id}
+ isLoading={isLoading}
+ noDataMessage="No data for this period"
+ onRowClick={(row) => onSelectGuardrail(row.id)}
+ rowClassName={() => "cursor-pointer"}
+ sortingMode="server"
+ sorting={sorting}
+ onSortingChange={handleSortingChange}
+ enableSortingRemoval={false}
+ size="compact"
+ toolbar={() => (
+
+
+
+ Guardrail Performance
+
+
+ Click a guardrail to view details, logs, and configuration
+
+
+
+ }
+ onClick={() => setEvaluationModalOpen(true)}
+ title="Evaluation settings"
+ />
+
+
+ )}
/>
-
+