mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
refactor(ui): migrate tags table onto shared DataTable
This commit is contained in:
parent
b2202cb1aa
commit
07deb66218
5 changed files with 265 additions and 240 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(<TagTable {...defaultProps} />);
|
||||
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(<TagTable {...defaultProps} />);
|
||||
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(<TagTable {...defaultProps} data={[mockTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[olderTag, newerTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockTag]} />);
|
||||
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(<TagTable {...defaultProps} data={[mockDynamicSpendTag]} />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag }) => {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([{ id: "created_at", desc: true }]);
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1 py-6">
|
||||
<div className="mb-1 flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<Inbox className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">No tags yet</div>
|
||||
<div className="text-sm text-muted-foreground">Create a tag to start routing and restricting model usage.</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const columns: ColumnDef<Tag>[] = [
|
||||
{
|
||||
header: "Tag Name",
|
||||
accessorKey: "name",
|
||||
cell: ({ row }) => {
|
||||
const tag = row.original;
|
||||
const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION;
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<IdCell
|
||||
value={tag.name}
|
||||
truncate={false}
|
||||
onClick={onSelectTag}
|
||||
disabled={isDynamicSpendTag}
|
||||
tooltip={
|
||||
isDynamicSpendTag ? "You cannot view the information of a dynamically generated spend tag" : tag.name
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
cell: ({ row }) => {
|
||||
const tag = row.original;
|
||||
return (
|
||||
<Tooltip title={tag.description}>
|
||||
<span className="text-xs">{tag.description || "-"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Allowed Models",
|
||||
accessorKey: "models",
|
||||
cell: ({ row }) => {
|
||||
const tag = row.original;
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{tag?.models?.length === 0 ? (
|
||||
<Badge size="xs" className="mb-1" color="red">
|
||||
All Models
|
||||
</Badge>
|
||||
) : (
|
||||
tag?.models?.map((modelId) => (
|
||||
<Badge key={modelId} size="xs" className="mb-1" color="blue">
|
||||
<Tooltip title={`ID: ${modelId}`}>
|
||||
<Text>{tag.model_info?.[modelId] || modelId}</Text>
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created",
|
||||
accessorKey: "created_at",
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const tag = row.original;
|
||||
const isDynamicSpendTag = tag.description === DYNAMIC_SPEND_TAG_DESCRIPTION;
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{isDynamicSpendTag ? (
|
||||
<Tooltip title="Dynamically generated spend tags cannot be edited">
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
className="opacity-50 cursor-not-allowed"
|
||||
aria-label="Edit tag (disabled)"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Edit tag">
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
onClick={() => onEdit(tag)}
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isDynamicSpendTag ? (
|
||||
<Tooltip title="Dynamically generated spend tags cannot be deleted">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
className="opacity-50 cursor-not-allowed"
|
||||
aria-label="Delete tag (disabled)"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Delete tag">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => onDelete(tag.name)}
|
||||
className="cursor-pointer hover:text-red-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
const TagTable: React.FC<TagTableProps> = ({ data, onEdit, onDelete, onSelectTag, isLoading = false }) => {
|
||||
const [sorting, setSorting] = useState<SortingState>(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 (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className={`py-1 h-8 ${
|
||||
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
|
||||
}`}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
|
||||
cell.column.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No tags found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
getRowId={(tag, index) => tag.name || String(index)}
|
||||
sortingMode="client"
|
||||
sorting={sorting}
|
||||
onSortingChange={setSorting}
|
||||
isLoading={isLoading}
|
||||
loadingMessage="Loading tags…"
|
||||
noDataMessage={<EmptyState />}
|
||||
size="compact"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface TagProps {
|
|||
|
||||
const TagManagement: React.FC<TagProps> = ({ accessToken, userID, userRole }) => {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [isLoadingTags, setIsLoadingTags] = useState(true);
|
||||
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
|
||||
const [selectedTagId, setSelectedTagId] = useState<string | null>(null);
|
||||
const [editTag, setEditTag] = useState<boolean>(false);
|
||||
|
|
@ -43,6 +44,8 @@ const TagManagement: React.FC<TagProps> = ({ 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<TagProps> = ({ accessToken, userID, userRole }) =>
|
|||
<Col numColSpan={1}>
|
||||
<TagTable
|
||||
data={tags}
|
||||
isLoading={isLoadingTags}
|
||||
onEdit={(tag) => {
|
||||
setSelectedTagId(tag.name);
|
||||
setEditTag(true);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<CellTooltip
|
||||
content="You cannot view the information of a dynamically generated spend tag"
|
||||
trigger={<span className="block truncate font-mono text-xs">{tag.name}</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IdentityCell
|
||||
title={tag.name}
|
||||
titleClassName="font-mono text-xs font-normal"
|
||||
onClick={() => onSelectTag(tag.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TagModelsCell({ tag }: { tag: Tag }) {
|
||||
const models = tag.models ?? [];
|
||||
if (models.length === 0) {
|
||||
return <Badge variant="secondary">All Models</Badge>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{models.map((modelId) => (
|
||||
<CellTooltip
|
||||
key={modelId}
|
||||
content={`ID: ${modelId}`}
|
||||
trigger={
|
||||
<Badge variant="outline" className="cursor-default">
|
||||
{tag.model_info?.[modelId] || modelId}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagRowActionsProps {
|
||||
tag: Tag;
|
||||
onEdit: (tag: Tag) => void;
|
||||
onDelete: (tagName: string) => void;
|
||||
}
|
||||
|
||||
function TagRowActions({ tag, onEdit, onDelete }: TagRowActionsProps) {
|
||||
const isDynamic = isDynamicSpendTag(tag);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
aria-label="Open tag actions"
|
||||
data-testid={`tag-actions-${tag.name}`}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "text-muted-foreground")}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuItem
|
||||
disabled={isDynamic}
|
||||
data-testid="tag-action-edit"
|
||||
title={isDynamic ? "Dynamically generated spend tags cannot be edited" : undefined}
|
||||
onClick={() => onEdit(tag)}
|
||||
>
|
||||
<Pencil />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
disabled={isDynamic}
|
||||
data-testid="tag-action-delete"
|
||||
title={isDynamic ? "Dynamically generated spend tags cannot be deleted" : undefined}
|
||||
onClick={() => onDelete(tag.name)}
|
||||
>
|
||||
<Trash2 />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
interface TagTableColumnsDeps {
|
||||
onSelectTag: (tagName: string) => void;
|
||||
onEdit: (tag: Tag) => void;
|
||||
onDelete: (tagName: string) => void;
|
||||
}
|
||||
|
||||
export const getTagTableColumns = ({ onSelectTag, onEdit, onDelete }: TagTableColumnsDeps): ColumnDef<Tag>[] => [
|
||||
{
|
||||
id: "name",
|
||||
accessorKey: "name",
|
||||
meta: { title: "Tag Name" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Tag Name" />,
|
||||
size: 260,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <TagNameCell tag={row.original} onSelectTag={onSelectTag} />,
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
accessorKey: "description",
|
||||
meta: { title: "Description" },
|
||||
header: "Description",
|
||||
size: 300,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const description = row.original.description;
|
||||
return (
|
||||
<span className="block truncate text-sm text-muted-foreground" title={description}>
|
||||
{description || "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "models",
|
||||
meta: { title: "Allowed Models", skeleton: "chips" },
|
||||
header: "Allowed Models",
|
||||
size: 240,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <TagModelsCell tag={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
accessorKey: "created_at",
|
||||
sortingFn: "datetime",
|
||||
meta: { title: "Created" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Created" />,
|
||||
size: 150,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <DateCell value={row.original.created_at} precision="date" />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
meta: { className: "text-right", headerClassName: "text-right" },
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
size: 64,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end">
|
||||
<TagRowActions tag={row.original} onEdit={onEdit} onDelete={onDelete} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
Loading…
Add table
Reference in a new issue