From b8241d666441eb0ac1a633aba204acc9dd3cb805 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 13 Aug 2026 09:20:48 -0700 Subject: [PATCH] refactor(ui): migrate guardrails monitor table to shared DataTable (#36709) * test(ui): characterize guardrails monitor table * refactor(ui): migrate guardrails monitor table to shared DataTable --- ui/litellm-dashboard/eslint-suppressions.json | 4 +- .../_components/GuardrailsOverview.test.tsx | 95 +++++++++ .../_components/GuardrailsOverview.tsx | 193 ++++++++++-------- 3 files changed, 203 insertions(+), 89 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.test.tsx 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: () => 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 +

+
+
+
+
+ )} /> - +