diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.test.tsx
index 75b78a99128..0fdbe255c16 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.test.tsx
@@ -150,4 +150,77 @@ describe("TagTable", () => {
expect(mockOnEdit).not.toHaveBeenCalled();
expect(mockOnDelete).not.toHaveBeenCalled();
});
+
+ describe("filters", () => {
+ const prodTag: Tag = { ...mockTag, name: "Prod-Billing", description: "Handles Invoices" };
+ const devTag: Tag = { ...mockTag, name: "dev-billing", description: "Sandbox usage" };
+ const prodOnlyTag: Tag = { ...mockTag, name: "prod-search", description: "Search traffic" };
+ const data = [prodTag, devTag, prodOnlyTag];
+
+ it("should narrow rows by tag name containing the text, ignoring case", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.type(screen.getByRole("textbox", { name: "Filter by tag name" }), "PROD");
+ expect(screen.getByText("Prod-Billing")).toBeInTheDocument();
+ expect(screen.getByText("prod-search")).toBeInTheDocument();
+ expect(screen.queryByText("dev-billing")).not.toBeInTheDocument();
+ });
+
+ it("should match the name anywhere in the string, not only as a prefix", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.type(screen.getByRole("textbox", { name: "Filter by tag name" }), "billing");
+ expect(screen.getByText("Prod-Billing")).toBeInTheDocument();
+ expect(screen.getByText("dev-billing")).toBeInTheDocument();
+ expect(screen.queryByText("prod-search")).not.toBeInTheDocument();
+ });
+
+ it("should narrow rows by description containing the text", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.type(screen.getByRole("textbox", { name: "Filter by description" }), "invoice");
+ expect(screen.getByText("Prod-Billing")).toBeInTheDocument();
+ expect(screen.queryByText("dev-billing")).not.toBeInTheDocument();
+ expect(screen.queryByText("prod-search")).not.toBeInTheDocument();
+ });
+
+ it("should require both filters to match when both are set", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.type(screen.getByRole("textbox", { name: "Filter by tag name" }), "billing");
+ await user.type(screen.getByRole("textbox", { name: "Filter by description" }), "sandbox");
+ expect(screen.getByText("dev-billing")).toBeInTheDocument();
+ expect(screen.queryByText("Prod-Billing")).not.toBeInTheDocument();
+ expect(screen.queryByText("prod-search")).not.toBeInTheDocument();
+ });
+
+ it("should restore every row when the filters are cleared", async () => {
+ const user = userEvent.setup();
+ render();
+ const nameFilter = screen.getByRole("textbox", { name: "Filter by tag name" });
+ await user.type(nameFilter, "dev");
+ expect(screen.queryByText("Prod-Billing")).not.toBeInTheDocument();
+ await user.clear(nameFilter);
+ expect(screen.getByText("Prod-Billing")).toBeInTheDocument();
+ expect(screen.getByText("dev-billing")).toBeInTheDocument();
+ expect(screen.getByText("prod-search")).toBeInTheDocument();
+ });
+
+ it("should show a no-matching message rather than the empty state when nothing matches", async () => {
+ const user = userEvent.setup();
+ render();
+ await user.type(screen.getByRole("textbox", { name: "Filter by tag name" }), "zzz");
+ expect(screen.getByText("No matching tags")).toBeInTheDocument();
+ expect(screen.queryByText("No tags yet")).not.toBeInTheDocument();
+ });
+
+ it("should not fail on tags without a description", async () => {
+ const user = userEvent.setup();
+ const noDescription: Tag = { ...mockTag, name: "bare", description: undefined };
+ render();
+ await user.type(screen.getByRole("textbox", { name: "Filter by description" }), "invoice");
+ expect(screen.getByText("Prod-Billing")).toBeInTheDocument();
+ expect(screen.queryByText("bare")).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.tsx
index fb4793ab340..d56695a9056 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/TagTable.tsx
@@ -1,11 +1,12 @@
"use client";
-import { SortingState } from "@tanstack/react-table";
-import { Inbox } from "lucide-react";
+import { SortingState, Table } from "@tanstack/react-table";
+import { Inbox, SearchX } from "lucide-react";
import React, { useMemo, useState } from "react";
import { DataTable } from "@/components/shared/DataTable";
import { Tag } from "@/components/tag_management/types";
+import { Input } from "@/components/ui/input";
import { getTagTableColumns } from "./tagTableColumns";
@@ -31,6 +32,39 @@ function EmptyState() {
);
}
+function NoMatchingTags() {
+ return (
+
+
+
+
+
No matching tags
+
Try a different tag name or description.
+
+ );
+}
+
+function TagFilters({ table }: { table: Table }) {
+ return (
+
+ table.getColumn("name")?.setFilterValue(event.target.value)}
+ />
+ table.getColumn("description")?.setFilterValue(event.target.value)}
+ />
+
+ );
+}
+
const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag, isLoading = false }) => {
const [sorting, setSorting] = useState(DEFAULT_SORTING);
@@ -48,7 +82,9 @@ const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag
onSortingChange={setSorting}
isLoading={isLoading}
loadingMessage="Loading tags…"
- noDataMessage={}
+ filterMode="client"
+ toolbar={(table) => }
+ noDataMessage={data.length === 0 ? : }
size="compact"
/>
);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tagTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tagTableColumns.tsx
index 1c44ae272c6..1cb56e90038 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tagTableColumns.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tagTableColumns.tsx
@@ -119,6 +119,7 @@ export const getTagTableColumns = ({ onSelectTag, onEdit, onDelete }: TagTableCo
header: ({ column }) => ,
size: 260,
enableSorting: true,
+ filterFn: "includesString",
cell: ({ row }) => ,
},
{
@@ -128,6 +129,7 @@ export const getTagTableColumns = ({ onSelectTag, onEdit, onDelete }: TagTableCo
header: "Description",
size: 300,
enableSorting: false,
+ filterFn: "includesString",
cell: ({ row }) => {
const description = row.original.description;
return (