From 07deb662184b26e234da690790216d8ad1eaa6ab Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 14 Jul 2026 18:05:39 -0700 Subject: [PATCH 1/4] refactor(ui): migrate tags table onto shared DataTable --- ui/litellm-dashboard/eslint-suppressions.json | 5 - .../_components/TagTable.test.tsx | 79 ++++-- .../tag-management/_components/TagTable.tsx | 247 +++--------------- .../tag-management/_components/index.tsx | 4 + .../_components/tagTableColumns.tsx | 170 ++++++++++++ 5 files changed, 265 insertions(+), 240 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tagTableColumns.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 67139d2c932..05ad536b260 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1161,11 +1161,6 @@ "count": 1 } }, - "src/app/(dashboard)/tag-management/_components/TagTable.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/tag-management/_components/components/CreateTagModal.tsx": { "no-restricted-imports": { "count": 1 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 e9a35d961f9..684107f7cb6 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 @@ -1,9 +1,12 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; + import { formatCellDate } from "@/components/shared/table_cells"; -import TagTable from "./TagTable"; import { Tag } from "@/components/tag_management/types"; +import TagTable from "./TagTable"; + describe("TagTable", () => { const mockOnEdit = vi.fn(); const mockOnDelete = vi.fn(); @@ -41,28 +44,28 @@ describe("TagTable", () => { vi.clearAllMocks(); }); - it("should render", () => { + it("should render every column header", () => { render(); - expect(screen.getByText("Tag Name")).toBeInTheDocument(); - expect(screen.getByText("Description")).toBeInTheDocument(); - expect(screen.getByText("Allowed Models")).toBeInTheDocument(); - expect(screen.getByText("Created")).toBeInTheDocument(); - expect(screen.getByText("Actions")).toBeInTheDocument(); + for (const header of ["Tag Name", "Description", "Allowed Models", "Created"]) { + expect(screen.getByText(header)).toBeInTheDocument(); + } }); - it("should display no tags found message when data is empty", () => { + it("should display the empty state when data is empty", () => { render(); - expect(screen.getByText("No tags found")).toBeInTheDocument(); + expect(screen.getByText("No tags yet")).toBeInTheDocument(); }); - it("should display tag name", () => { + it("should display tag name and description", () => { render(); expect(screen.getByText("test-tag")).toBeInTheDocument(); + expect(screen.getByText("Test description")).toBeInTheDocument(); }); - it("should display tag description", () => { + it("should display model names from model_info", () => { render(); - expect(screen.getByText("Test description")).toBeInTheDocument(); + expect(screen.getByText("GPT-4")).toBeInTheDocument(); + expect(screen.getByText("Claude-3")).toBeInTheDocument(); }); it("should display All Models badge when models array is empty", () => { @@ -80,30 +83,56 @@ describe("TagTable", () => { expect(screen.getByText(formattedDate)).toBeInTheDocument(); }); - it("should call onSelectTag when tag name is clicked", () => { + it("should sort by created date descending by default", () => { + const olderTag: Tag = { ...mockTag, name: "older-tag", created_at: "2023-01-01T00:00:00Z" }; + const newerTag: Tag = { ...mockTag, name: "newer-tag", created_at: "2025-01-01T00:00:00Z" }; + render(); + const rows = screen.getAllByRole("row").slice(1); + expect(within(rows[0]).getByText("newer-tag")).toBeInTheDocument(); + expect(within(rows[1]).getByText("older-tag")).toBeInTheDocument(); + }); + + it("should call onSelectTag when tag name is clicked", async () => { + const user = userEvent.setup(); render(); - fireEvent.click(screen.getByRole("button", { name: "test-tag" })); + await user.click(screen.getByRole("button", { name: "test-tag" })); expect(mockOnSelectTag).toHaveBeenCalledWith("test-tag"); }); it("should render tag name as non-clickable for dynamic spend tags", () => { render(); expect(screen.queryByRole("button", { name: "dynamic-spend-tag" })).not.toBeInTheDocument(); - fireEvent.click(screen.getByText("dynamic-spend-tag")); + expect(screen.getByText("dynamic-spend-tag")).toBeInTheDocument(); expect(mockOnSelectTag).not.toHaveBeenCalled(); }); - it("should disable edit icon for dynamic spend tags", () => { - render(); - const editIcon = screen.getByLabelText("Edit tag (disabled)"); - expect(editIcon).toBeInTheDocument(); - expect(editIcon).toHaveClass("cursor-not-allowed"); + it("should edit a tag through the actions menu", async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId("tag-actions-test-tag")); + await user.click(await screen.findByTestId("tag-action-edit")); + expect(mockOnEdit).toHaveBeenCalledWith(mockTag); }); - it("should disable delete icon for dynamic spend tags", () => { + it("should delete a tag through the actions menu", async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId("tag-actions-test-tag")); + await user.click(await screen.findByTestId("tag-action-delete")); + expect(mockOnDelete).toHaveBeenCalledWith("test-tag"); + }); + + it("should disable edit and delete for dynamic spend tags", async () => { + const user = userEvent.setup(); render(); - const deleteIcon = screen.getByLabelText("Delete tag (disabled)"); - expect(deleteIcon).toBeInTheDocument(); - expect(deleteIcon).toHaveClass("cursor-not-allowed"); + await user.click(screen.getByTestId("tag-actions-dynamic-spend-tag")); + + const editItem = await screen.findByTestId("tag-action-edit"); + const deleteItem = await screen.findByTestId("tag-action-delete"); + + expect(editItem).toHaveAttribute("data-disabled"); + expect(deleteItem).toHaveAttribute("data-disabled"); + expect(mockOnEdit).not.toHaveBeenCalled(); + expect(mockOnDelete).not.toHaveBeenCalled(); }); }); 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 21bf4657a47..488190a0fdf 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,227 +1,54 @@ -import { ChevronDownIcon, ChevronUpIcon, PencilAltIcon, SwitchVerticalIcon, TrashIcon } from "@heroicons/react/outline"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - getSortedRowModel, - SortingState, - useReactTable, -} from "@tanstack/react-table"; -import { Badge, Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text } from "@tremor/react"; -import { Tooltip } from "antd"; -import React from "react"; -import { DateCell, IdCell } from "@/components/shared/table_cells"; +"use client"; + +import { SortingState } from "@tanstack/react-table"; +import { Inbox } from "lucide-react"; +import React, { useMemo, useState } from "react"; + +import { DataTable } from "@/components/shared/DataTable"; import { Tag } from "@/components/tag_management/types"; +import { getTagTableColumns } from "./tagTableColumns"; + interface TagTableProps { data: Tag[]; onEdit: (tag: Tag) => void; onDelete: (tagName: string) => void; onSelectTag: (tagName: string) => void; + isLoading?: boolean; } -const DYNAMIC_SPEND_TAG_DESCRIPTION = - "This is just a spend tag that was passed dynamically in a request. It does not control any LLM models."; +const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }]; -const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag }) => { - const [sorting, setSorting] = React.useState([{ id: "created_at", desc: true }]); +function EmptyState() { + return ( +
+
+ +
+
No tags yet
+
Create a tag to start routing and restricting model usage.
+
+ ); +} - const columns: ColumnDef[] = [ - { - header: "Tag Name", - accessorKey: "name", - cell: ({ row }) => { - const tag = row.original; - const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION; - return ( -
- -
- ); - }, - }, - { - header: "Description", - accessorKey: "description", - cell: ({ row }) => { - const tag = row.original; - return ( - - {tag.description || "-"} - - ); - }, - }, - { - header: "Allowed Models", - accessorKey: "models", - cell: ({ row }) => { - const tag = row.original; - return ( -
- {tag?.models?.length === 0 ? ( - - All Models - - ) : ( - tag?.models?.map((modelId) => ( - - - {tag.model_info?.[modelId] || modelId} - - - )) - )} -
- ); - }, - }, - { - header: "Created", - accessorKey: "created_at", - sortingFn: "datetime", - cell: ({ row }) => , - }, - { - id: "actions", - header: "Actions", - cell: ({ row }) => { - const tag = row.original; - const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION; - return ( -
- {isDynamicSpendTag ? ( - - - - ) : ( - - onEdit(tag)} - className="cursor-pointer hover:text-blue-500" - /> - - )} - {isDynamicSpendTag ? ( - - - - ) : ( - - onDelete(tag.name)} - className="cursor-pointer hover:text-red-500" - /> - - )} -
- ); - }, - }, - ]; +const TagTable: React.FC = ({ data, onEdit, onDelete, onSelectTag, isLoading = false }) => { + const [sorting, setSorting] = useState(DEFAULT_SORTING); - const table = useReactTable({ - data, - columns, - state: { - sorting, - }, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - enableSorting: true, - }); + const columns = useMemo(() => getTagTableColumns({ onSelectTag, onEdit, onDelete }), [onSelectTag, onEdit, onDelete]); return ( -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - -
-
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -
- {header.id !== "actions" && ( -
- {header.column.getIsSorted() ? ( - { - asc: , - desc: , - }[header.column.getIsSorted() as string] - ) : ( - - )} -
- )} -
-
- ))} -
- ))} -
- - {table.getRowModel().rows.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - -
-

No tags found

-
-
-
- )} -
-
-
-
+ tag.name || String(index)} + sortingMode="client" + sorting={sorting} + onSortingChange={setSorting} + isLoading={isLoading} + loadingMessage="Loading tags…" + noDataMessage={} + size="compact" + /> ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx index 9c5a5592d5e..e6c4554c0cf 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx @@ -27,6 +27,7 @@ interface TagProps { const TagManagement: React.FC = ({ accessToken, userID, userRole }) => { const [tags, setTags] = useState([]); + const [isLoadingTags, setIsLoadingTags] = useState(true); const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); const [selectedTagId, setSelectedTagId] = useState(null); const [editTag, setEditTag] = useState(false); @@ -43,6 +44,8 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => } catch (error) { console.error("Error fetching tags:", error); NotificationsManager.fromBackend("Error fetching tags: " + error); + } finally { + setIsLoadingTags(false); } }; @@ -163,6 +166,7 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => { setSelectedTagId(tag.name); setEditTag(true); 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 new file mode 100644 index 00000000000..dcfd9d10925 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/tagTableColumns.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal, Pencil, Trash2 } from "lucide-react"; + +import { DataTableSortHeader } from "@/components/shared/DataTable"; +import { CellTooltip, DateCell, IdentityCell } from "@/components/shared/table_cells"; +import { Tag } from "@/components/tag_management/types"; +import { Badge } from "@/components/ui/badge"; +import { buttonVariants } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/cva.config"; + +export const DYNAMIC_SPEND_TAG_DESCRIPTION = + "This is just a spend tag that was passed dynamically in a request. It does not control any LLM models."; + +const isDynamicSpendTag = (tag: Tag) => tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION; + +function TagNameCell({ tag, onSelectTag }: { tag: Tag; onSelectTag: (tagName: string) => void }) { + if (isDynamicSpendTag(tag)) { + return ( + {tag.name}} + /> + ); + } + return ( + onSelectTag(tag.name)} + /> + ); +} + +function TagModelsCell({ tag }: { tag: Tag }) { + const models = tag.models ?? []; + if (models.length === 0) { + return All Models; + } + return ( +
+ {models.map((modelId) => ( + + {tag.model_info?.[modelId] || modelId} + + } + /> + ))} +
+ ); +} + +interface TagRowActionsProps { + tag: Tag; + onEdit: (tag: Tag) => void; + onDelete: (tagName: string) => void; +} + +function TagRowActions({ tag, onEdit, onDelete }: TagRowActionsProps) { + const isDynamic = isDynamicSpendTag(tag); + + return ( + + + + + + onEdit(tag)} + > + + Edit + + onDelete(tag.name)} + > + + Delete + + + + ); +} + +interface TagTableColumnsDeps { + onSelectTag: (tagName: string) => void; + onEdit: (tag: Tag) => void; + onDelete: (tagName: string) => void; +} + +export const getTagTableColumns = ({ onSelectTag, onEdit, onDelete }: TagTableColumnsDeps): ColumnDef[] => [ + { + id: "name", + accessorKey: "name", + meta: { title: "Tag Name" }, + header: ({ column }) => , + size: 260, + enableSorting: true, + cell: ({ row }) => , + }, + { + id: "description", + accessorKey: "description", + meta: { title: "Description" }, + header: "Description", + size: 300, + enableSorting: false, + cell: ({ row }) => { + const description = row.original.description; + return ( + + {description || "-"} + + ); + }, + }, + { + id: "models", + meta: { title: "Allowed Models", skeleton: "chips" }, + header: "Allowed Models", + size: 240, + enableSorting: false, + cell: ({ row }) => , + }, + { + id: "created_at", + accessorKey: "created_at", + sortingFn: "datetime", + meta: { title: "Created" }, + header: ({ column }) => , + size: 150, + enableSorting: true, + cell: ({ row }) => , + }, + { + id: "actions", + meta: { className: "text-right", headerClassName: "text-right" }, + header: () => Actions, + size: 64, + enableSorting: false, + enableHiding: false, + cell: ({ row }) => ( +
+ +
+ ), + }, +]; From da064295244d5b72ddc18684e508e6369e6ad61e Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 14 Jul 2026 18:20:22 -0700 Subject: [PATCH 2/4] refactor(ui): truncate tag name/description and mute disabled tag names --- .../tag-management/_components/TagTable.test.tsx | 15 +++++++++++++-- .../_components/tagTableColumns.tsx | 7 ++++--- 2 files changed, 17 insertions(+), 5 deletions(-) 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 684107f7cb6..957f605bae7 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 @@ -99,13 +99,24 @@ describe("TagTable", () => { expect(mockOnSelectTag).toHaveBeenCalledWith("test-tag"); }); - it("should render tag name as non-clickable for dynamic spend tags", () => { + it("should render tag name as non-clickable and muted for dynamic spend tags", () => { render(); expect(screen.queryByRole("button", { name: "dynamic-spend-tag" })).not.toBeInTheDocument(); - expect(screen.getByText("dynamic-spend-tag")).toBeInTheDocument(); + expect(screen.getByText("dynamic-spend-tag")).toHaveClass("text-muted-foreground"); expect(mockOnSelectTag).not.toHaveBeenCalled(); }); + it("should truncate long tag names and descriptions", () => { + const longTag: Tag = { + ...mockTag, + name: "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15) Firefox/152.0", + description: "A very long description that would otherwise stretch the column far beyond what users need to see", + }; + render(); + expect(screen.getByText(longTag.name)).toHaveClass("truncate", "text-primary"); + expect(screen.getByText(longTag.description as string)).toHaveClass("truncate", "max-w-72"); + }); + it("should edit a tag through the actions menu", async () => { const user = userEvent.setup(); render(); 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 dcfd9d10925..1c44ae272c6 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 @@ -26,14 +26,15 @@ function TagNameCell({ tag, onSelectTag }: { tag: Tag; onSelectTag: (tagName: st return ( {tag.name}} + trigger={{tag.name}} /> ); } return ( onSelectTag(tag.name)} /> ); @@ -130,7 +131,7 @@ export const getTagTableColumns = ({ onSelectTag, onEdit, onDelete }: TagTableCo cell: ({ row }) => { const description = row.original.description; return ( - + {description || "-"} ); From 1e4c27dd62b5abe0c03f8e94ea1566453b02a673 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 14 Jul 2026 18:23:46 -0700 Subject: [PATCH 3/4] test(ui): exercise click suppression on disabled tag actions --- .../(dashboard)/tag-management/_components/TagTable.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) 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 957f605bae7..75b78a99128 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 @@ -143,6 +143,10 @@ describe("TagTable", () => { expect(editItem).toHaveAttribute("data-disabled"); expect(deleteItem).toHaveAttribute("data-disabled"); + + await user.click(editItem); + await user.click(deleteItem); + expect(mockOnEdit).not.toHaveBeenCalled(); expect(mockOnDelete).not.toHaveBeenCalled(); }); From 6f19c7b2b37634fad1e5159db8daaa9dc37e1f5c Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 14 Jul 2026 18:32:30 -0700 Subject: [PATCH 4/4] fix(ui): resolve tags loading state when accessToken is null --- .../tag-management/_components/index.test.tsx | 59 +++++++++++++++++++ .../tag-management/_components/index.tsx | 5 +- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx new file mode 100644 index 00000000000..2530ce9fda7 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.test.tsx @@ -0,0 +1,59 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { tagListCall } from "@/components/networking"; + +import TagManagement from "./index"; + +vi.mock("@/components/networking", () => ({ + tagListCall: vi.fn(), + tagCreateCall: vi.fn(), + tagDeleteCall: vi.fn(), + modelInfoCall: vi.fn(), +})); + +vi.mock("./TagTable", () => ({ + __esModule: true, + default: ({ isLoading }: { isLoading?: boolean }) => ( +
{isLoading ? "table-loading" : "table-loaded"}
+ ), +})); + +vi.mock("./tag_info", () => ({ + __esModule: true, + default: () =>
Mock Tag Info View
, +})); + +vi.mock("./components/CreateTagModal", () => ({ + __esModule: true, + default: () =>
Mock Create Tag Modal
, +})); + +const mockTagListCall = vi.mocked(tagListCall); + +describe("TagManagement loading state", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should resolve the loading state when accessToken is null instead of showing the skeleton forever", async () => { + render(); + expect(await screen.findByText("table-loaded")).toBeInTheDocument(); + expect(mockTagListCall).not.toHaveBeenCalled(); + }); + + it("should show the loading state until the tag fetch settles", async () => { + let resolveFetch: (value: Record) => void = () => {}; + mockTagListCall.mockReturnValue( + new Promise((resolve) => { + resolveFetch = resolve; + }), + ); + render(); + expect(screen.getByText("table-loading")).toBeInTheDocument(); + + resolveFetch({}); + expect(await screen.findByText("table-loaded")).toBeInTheDocument(); + expect(mockTagListCall).toHaveBeenCalledWith("sk-test"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx index e6c4554c0cf..16b92bd4553 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/tag-management/_components/index.tsx @@ -37,7 +37,10 @@ const TagManagement: React.FC = ({ accessToken, userID, userRole }) => const [availableModels, setAvailableModels] = useState([]); const fetchTags = async () => { - if (!accessToken) return; + if (!accessToken) { + setIsLoadingTags(false); + return; + } try { const response = await tagListCall(accessToken); setTags(Object.values(response));