feat(ui): migrate molecules/models/columns to shadcn (drops antd Popover/Tooltip/Typography + tremor Badge/Button)

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
cursor 2026-04-24 06:01:28 +00:00
parent a0475ee4f7
commit a9985fc17c
No known key found for this signature in database
4 changed files with 449 additions and 395 deletions

View file

@ -133,7 +133,9 @@
"src/components/policies/impact_preview_alert.tsx",
"src/components/policies/impact_popover.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx"
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx",
"src/components/molecules/models/columns.tsx",
"src/components/common_components/MemberTable.tsx"
],
"rules": {
"litellm-ui/no-banned-ui-imports": "off",

View file

@ -243,9 +243,10 @@ describe("columns", () => {
render(<TestTable data={[model]} columns={cols} />);
expect(screen.getByText("Credentials")).toBeInTheDocument();
// Info icon is in a flex container with Credentials - ant icons render as span with role="img"
// Info icon is rendered by lucide-react as an SVG inside a button
// trigger in the header. Find any SVG next to the header text.
const credentialsHeader = screen.getByText("Credentials").closest("span");
expect(credentialsHeader?.parentElement?.querySelector('[role="img"]')).toBeInTheDocument();
expect(credentialsHeader?.parentElement?.querySelector("svg")).toBeInTheDocument();
});
it("should display reusable credential with SyncOutlined icon and credential name", () => {
@ -298,7 +299,7 @@ describe("columns", () => {
render(<TestTable data={[model]} columns={cols} />);
expect(screen.getByText("Manual")).toBeInTheDocument();
expect(screen.getByText("Manual")).toHaveClass("text-gray-500");
expect(screen.getByText("Manual")).toHaveClass("text-muted-foreground");
});
it("should display Manual when litellm_params is undefined", () => {
@ -700,9 +701,12 @@ describe("columns", () => {
});
render(<TestTable data={[model]} columns={cols} />);
const deleteButton = screen.getByRole("button", { name: /config model cannot be deleted/i });
expect(deleteButton).toBeInTheDocument();
expect(deleteButton).toHaveClass("cursor-not-allowed");
// For config models we render a disabled-looking <span> wrapping the
// Trash icon (not a <button>), with a tooltip explaining why it's
// disabled. Find the icon by its class.
const trashIcon = document.querySelector(".cursor-not-allowed");
expect(trashIcon).toBeInTheDocument();
expect(trashIcon).toHaveClass("cursor-not-allowed");
});
it("should display collapsed access groups with expand button", () => {

View file

@ -1,442 +1,489 @@
import { Pencil as EditOutlined, Info as InfoCircleOutlined, RefreshCcw as SyncOutlined } from "lucide-react";
import { Trash2 } from "lucide-react";
import React from "react";
import {
Pencil as EditOutlined,
Info as InfoCircleOutlined,
RefreshCcw as SyncOutlined,
Trash2,
} from "lucide-react";
import { ColumnDef } from "@tanstack/react-table";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Badge, Button, Icon } from "@tremor/react";
import { Divider, Flex, Popover, Space, Tooltip, Typography } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ModelData } from "../../model_dashboard/types";
import { ProviderLogo } from "./ProviderLogo";
const { Text, Title } = Typography;
const credentialsInfoPopoverContent = (
<Space direction="vertical" size={12}>
<Text strong style={{ fontSize: 13 }}>
Credential types
</Text>
<Space direction="vertical" size={8}>
<Flex align="center" gap={8}>
<Space direction="vertical">
<Flex align="center" gap={8}>
<SyncOutlined style={{ color: "#1890ff" }} />
<Title level={5} style={{ margin: 0, color: "#1890ff" }}>Reusable</Title>
</Flex>
<Text type="secondary">
Credentials saved in LiteLLM that can be added to models repeatedly.
</Text>
</Space>
</Flex>
<Divider size="small" />
<Flex align="center" gap={8}>
<Space direction="vertical" size={8}>
<Flex align="center" gap={8}>
<EditOutlined style={{ color: "#8c8c8c", fontSize: 14, flexShrink: 0 }} />
<Title level={5} style={{ margin: 0 }}>Manual</Title>
</Flex>
<Text type="secondary">
Credentials added directly during model creation or defined in the config file.
</Text>
</Space>
</Flex>
</Space>
</Space>
// Simple hoverable tooltip wrapper — mirrors antd Tooltip's `title` prop.
const WithTooltip: React.FC<{
title: React.ReactNode;
children: React.ReactNode;
asChild?: boolean;
}> = ({ title, children, asChild = true }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent className="max-w-sm">{title}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
const CredentialsInfoPopoverContent: React.FC = () => (
<div className="flex flex-col gap-3">
<span className="text-[13px] font-semibold">Credential types</span>
<div className="flex flex-col gap-2">
<div className="flex items-start gap-2">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<SyncOutlined className="h-3.5 w-3.5 text-blue-500" />
<h5 className="m-0 text-blue-500 text-sm font-semibold">
Reusable
</h5>
</div>
<span className="text-muted-foreground text-xs">
Credentials saved in LiteLLM that can be added to models
repeatedly.
</span>
</div>
</div>
<Separator />
<div className="flex items-start gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<EditOutlined className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<h5 className="m-0 text-sm font-semibold">Manual</h5>
</div>
<span className="text-muted-foreground text-xs">
Credentials added directly during model creation or defined in
the config file.
</span>
</div>
</div>
</div>
</div>
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const columns = (
userRole: string,
userID: string,
premiumUser: boolean,
setSelectedModelId: (id: string) => void,
setSelectedTeamId: (id: string) => void,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getDisplayModelName: (model: any) => string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handleEditClick: (model: any) => void,
handleRefreshClick: () => void,
expandedRows: Set<string>,
setExpandedRows: (expandedRows: Set<string>) => void,
onDeleteClick?: (modelId: string) => void,
): ColumnDef<ModelData>[] => [
{
header: () => <span className="text-sm font-semibold">Model ID</span>,
accessorKey: "model_info.id",
enableSorting: false,
size: 130,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<Tooltip title={model.model_info.id}>
<Text
ellipsis
className="text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs cursor-pointer w-full block"
style={{ fontSize: 14, padding: '1px 8px' }}
onClick={(e) => {
e.stopPropagation();
setSelectedModelId(model.model_info.id);
}}
>
{model.model_info.id}
</Text>
</Tooltip>
);
},
},
{
header: () => <span className="text-sm font-semibold">Model Information</span>,
accessorKey: "model_name",
size: 250,
minSize: 120,
cell: ({ row }) => {
const model = row.original;
const displayName = getDisplayModelName(row.original) || "-";
const popoverContent = (
<Space
direction="vertical"
size={12}
style={{ minWidth: 220 }}
{
header: () => <span className="text-sm font-semibold">Model ID</span>,
accessorKey: "model_info.id",
enableSorting: false,
size: 130,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<WithTooltip title={model.model_info.id}>
<span
className="truncate text-blue-500 bg-blue-50 hover:bg-blue-100 cursor-pointer w-full block text-sm px-2"
onClick={(e) => {
e.stopPropagation();
setSelectedModelId(model.model_info.id);
}}
>
<Flex align="center" gap={8}>
<ProviderLogo provider={model.provider} />
<Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis
{model.model_info.id}
</span>
</WithTooltip>
);
},
},
{
header: () => (
<span className="text-sm font-semibold">Model Information</span>
),
accessorKey: "model_name",
size: 250,
minSize: 120,
cell: ({ row }) => {
const model = row.original;
const displayName = getDisplayModelName(row.original) || "-";
const popoverContent = (
<div className="flex flex-col gap-3 min-w-[220px]">
<div className="flex items-center gap-2">
<ProviderLogo provider={model.provider} />
<span className="text-muted-foreground text-xs truncate">
{model.provider || "Unknown provider"}
</span>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex flex-col gap-0.5 w-full">
<span className="text-muted-foreground text-[11px]">
Public Model Name
</span>
<span
className="font-semibold text-[13px] max-w-[480px] truncate"
title={displayName}
>
{model.provider || "Unknown provider"}
</Text>
</Flex>
{displayName}
</span>
</div>
<Space direction="vertical" size={6}>
<Space direction="vertical" size={2} style={{ width: "100%" }}>
<Text type="secondary" style={{ fontSize: 11 }}>
Public Model Name
</Text>
<Text
strong
style={{ fontSize: 13, maxWidth: 480 }}
ellipsis
title={displayName}
>
{displayName}
</Text>
</Space>
<div className="flex flex-col gap-0.5">
<span className="text-muted-foreground text-[11px]">
LiteLLM Model Name
</span>
<span
className="text-[13px] truncate"
title={model.litellm_model_name || "-"}
>
{model.litellm_model_name || "-"}
</span>
</div>
</div>
</div>
);
<Space direction="vertical" size={2}>
<Text type="secondary" style={{ fontSize: 11 }}>
LiteLLM Model Name
</Text>
<Text
style={{ fontSize: 13 }}
copyable={{ text: model.litellm_model_name || "-" }}
ellipsis
title={model.litellm_model_name || "-"}
>
{model.litellm_model_name || "-"}
</Text>
</Space>
</Space>
</Space>
);
return (
<Popover content={popoverContent} placement="right" arrow={{ pointAtCenter: true }} styles={{
root: {
maxWidth: 500,
}
}}>
return (
<Popover>
<PopoverTrigger asChild>
<div className="flex items-start space-x-2 min-w-0 w-full cursor-pointer">
<div className="flex-shrink-0 mt-0.5">
{model.provider ? (
<ProviderLogo provider={model.provider} />
) : (
<div className="w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs">-</div>
<div className="w-4 h-4 rounded-full bg-muted flex items-center justify-center text-xs">
-
</div>
)}
</div>
<div className="flex flex-col min-w-0 flex-1">
<Text ellipsis className="text-gray-900" style={{ fontSize: 12, fontWeight: 500, lineHeight: '16px' }}>
<span className="truncate text-foreground text-xs font-medium leading-4">
{displayName}
</Text>
<Text ellipsis type="secondary" style={{ fontSize: 12, lineHeight: '16px', marginTop: 2 }}>
</span>
<span className="truncate text-muted-foreground text-xs leading-4 mt-[2px]">
{model.litellm_model_name || "-"}
</Text>
</span>
</div>
</div>
</Popover>
);
},
</PopoverTrigger>
<PopoverContent side="right" className="max-w-[500px]">
{popoverContent}
</PopoverContent>
</Popover>
);
},
{
header: () => (
<span className="flex items-center gap-1">
<span className="text-sm font-semibold">Credentials</span>
<Popover
content={credentialsInfoPopoverContent}
placement="bottom"
arrow={{ pointAtCenter: true }}
>
<InfoCircleOutlined
className="cursor-pointer text-gray-400 hover:text-gray-600"
style={{ fontSize: 12 }}
/>
</Popover>
</span>
),
accessorKey: "litellm_credential_name",
enableSorting: false,
size: 180,
minSize: 100,
cell: ({ row }) => {
const model = row.original;
const credentialName = model.litellm_params?.litellm_credential_name;
const isReusable = !!credentialName;
},
{
header: () => (
<span className="flex items-center gap-1">
<span className="text-sm font-semibold">Credentials</span>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="cursor-pointer text-muted-foreground hover:text-foreground"
>
<InfoCircleOutlined className="h-3 w-3" />
</button>
</PopoverTrigger>
<PopoverContent side="bottom">
<CredentialsInfoPopoverContent />
</PopoverContent>
</Popover>
</span>
),
accessorKey: "litellm_credential_name",
enableSorting: false,
size: 180,
minSize: 100,
cell: ({ row }) => {
const model = row.original;
const credentialName = model.litellm_params?.litellm_credential_name;
const isReusable = !!credentialName;
return (
<div className="flex items-center space-x-2 min-w-0 w-full">
{isReusable ? (
<>
<SyncOutlined className="flex-shrink-0 h-3.5 w-3.5 text-blue-500" />
<span
className="text-xs truncate text-blue-600"
title={credentialName}
>
{credentialName}
</span>
</>
) : (
<>
<EditOutlined className="flex-shrink-0 h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">Manual</span>
</>
)}
</div>
);
},
},
{
header: () => <span className="text-sm font-semibold">Created By</span>,
accessorKey: "model_info.created_by",
sortingFn: "datetime",
size: 160,
minSize: 100,
cell: ({ row }) => {
const model = row.original;
const isConfigModel = !model.model_info?.db_model;
const createdBy = model.model_info.created_by;
const createdAt = model.model_info.created_at
? new Date(model.model_info.created_at).toLocaleDateString()
: null;
return (
<div className="flex flex-col min-w-0 w-full">
<div
className="text-xs font-medium text-foreground truncate"
title={isConfigModel ? "Defined in config" : createdBy || "Unknown"}
>
{isConfigModel ? "Defined in config" : createdBy || "Unknown"}
</div>
<div
className="text-xs text-muted-foreground truncate mt-0.5"
title={
isConfigModel ? "Config file" : createdAt || "Unknown date"
}
>
{isConfigModel ? "-" : createdAt || "Unknown date"}
</div>
</div>
);
},
},
{
header: () => <span className="text-sm font-semibold">Updated At</span>,
accessorKey: "model_info.updated_at",
sortingFn: "datetime",
size: 120,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<span className="text-xs">
{model.model_info.updated_at
? new Date(model.model_info.updated_at).toLocaleDateString()
: "-"}
</span>
);
},
},
{
header: () => <span className="text-sm font-semibold">Costs</span>,
accessorKey: "input_cost",
size: 120,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
const inputCost = model.input_cost;
const outputCost = model.output_cost;
if (inputCost == null && outputCost == null) {
return (
<div className="flex items-center space-x-2 min-w-0 w-full">
{isReusable ? (
<>
<SyncOutlined className="flex-shrink-0" style={{ color: "#1890ff", fontSize: 14 }} />
<span className="text-xs truncate text-blue-600" title={credentialName}>
{credentialName}
</span>
</>
) : (
<>
<EditOutlined className="flex-shrink-0" style={{ color: "#8c8c8c", fontSize: 14 }} />
<span className="text-xs text-gray-500">Manual</span>
</>
<div className="w-full">
<span className="text-xs text-muted-foreground">-</span>
</div>
);
}
return (
<WithTooltip title="Cost per 1M tokens">
<div className="flex flex-col min-w-0 w-full">
{inputCost != null && (
<div className="text-xs font-medium text-foreground truncate">
In: ${inputCost}
</div>
)}
{outputCost != null && (
<div className="text-xs text-muted-foreground truncate mt-0.5">
Out: ${outputCost}
</div>
)}
</div>
);
},
</WithTooltip>
);
},
{
header: () => <span className="text-sm font-semibold">Created By</span>,
accessorKey: "model_info.created_by",
sortingFn: "datetime",
size: 160,
minSize: 100,
cell: ({ row }) => {
const model = row.original;
const isConfigModel = !model.model_info?.db_model;
const createdBy = model.model_info.created_by;
const createdAt = model.model_info.created_at ? new Date(model.model_info.created_at).toLocaleDateString() : null;
return (
<div className="flex flex-col min-w-0 w-full">
{/* Created By - Primary */}
<div
className="text-xs font-medium text-gray-900 truncate"
title={isConfigModel ? "Defined in config" : createdBy || "Unknown"}
},
{
header: () => <span className="text-sm font-semibold">Team ID</span>,
accessorKey: "model_info.team_id",
enableSorting: false,
size: 130,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return model.model_info.team_id ? (
<div className="overflow-hidden w-full">
<WithTooltip title={model.model_info.team_id}>
<Button
variant="ghost"
size="sm"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 h-auto text-left overflow-hidden truncate w-full justify-start"
onClick={(e) => {
e.stopPropagation();
setSelectedTeamId(model.model_info.team_id);
}}
>
{isConfigModel ? "Defined in config" : createdBy || "Unknown"}
</div>
{/* Created At - Secondary */}
<div
className="text-xs text-gray-500 truncate mt-0.5"
title={isConfigModel ? "Config file" : createdAt || "Unknown date"}
>
{isConfigModel ? "-" : createdAt || "Unknown date"}
</div>
</div>
);
},
{model.model_info.team_id.slice(0, 7)}...
</Button>
</WithTooltip>
</div>
) : (
"-"
);
},
{
header: () => <span className="text-sm font-semibold">Updated At</span>,
accessorKey: "model_info.updated_at",
sortingFn: "datetime",
size: 120,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<span className="text-xs">
{model.model_info.updated_at ? new Date(model.model_info.updated_at).toLocaleDateString() : "-"}
</span>
);
},
},
{
header: () => <span className="text-sm font-semibold">Costs</span>,
accessorKey: "input_cost",
size: 120,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
const inputCost = model.input_cost;
const outputCost = model.output_cost;
},
{
header: () => (
<span className="text-sm font-semibold">Model Access Group</span>
),
accessorKey: "model_info.model_access_group",
enableSorting: false,
size: 180,
minSize: 100,
cell: ({ row }) => {
const model = row.original;
const accessGroups = model.model_info.access_groups;
// If both costs are missing or undefined, show "-"
if (inputCost == null && outputCost == null) {
return (
<div className="w-full">
<span className="text-xs text-gray-400">-</span>
</div>
);
if (!accessGroups || accessGroups.length === 0) {
return "-";
}
const modelId = model.model_info.id;
const isExpanded = expandedRows.has(modelId);
const shouldShowExpandButton = accessGroups.length > 1;
const toggleExpanded = () => {
const newExpanded = new Set(expandedRows);
if (isExpanded) {
newExpanded.delete(modelId);
} else {
newExpanded.add(modelId);
}
setExpandedRows(newExpanded);
};
return (
<Tooltip title="Cost per 1M tokens">
<div className="flex flex-col min-w-0 w-full">
{/* Input Cost - Primary */}
{inputCost != null && <div className="text-xs font-medium text-gray-900 truncate">In: ${inputCost}</div>}
{/* Output Cost - Secondary */}
{outputCost != null && <div className="text-xs text-gray-500 truncate mt-0.5">Out: ${outputCost}</div>}
</div>
</Tooltip>
);
},
},
{
header: () => <span className="text-sm font-semibold">Team ID</span>,
accessorKey: "model_info.team_id",
enableSorting: false,
size: 130,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return model.model_info.team_id ? (
<div className="overflow-hidden w-full">
<Tooltip title={model.model_info.team_id}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate w-full"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
setSelectedTeamId(model.model_info.team_id);
}}
return (
<div className="flex items-center gap-1 overflow-hidden w-full">
<Badge
variant="secondary"
className="bg-blue-100 text-blue-700 text-xs px-1.5 py-0.5 h-5 leading-tight flex-shrink-0"
>
{accessGroups[0]}
</Badge>
{(isExpanded ||
(!shouldShowExpandButton && accessGroups.length === 2)) &&
accessGroups.slice(1).map((group: string, index: number) => (
<Badge
key={index + 1}
variant="secondary"
className="bg-blue-100 text-blue-700 text-xs px-1.5 py-0.5 h-5 leading-tight flex-shrink-0"
>
{model.model_info.team_id.slice(0, 7)}...
</Button>
</Tooltip>
</div>
) : (
"-"
);
},
{group}
</Badge>
))}
{shouldShowExpandButton && (
<button
onClick={(e) => {
e.stopPropagation();
toggleExpanded();
}}
className="text-xs text-blue-600 hover:text-blue-800 px-1 py-0.5 rounded hover:bg-blue-50 h-5 leading-tight flex-shrink-0 whitespace-nowrap"
>
{isExpanded ? "−" : `+${accessGroups.length - 1}`}
</button>
)}
</div>
);
},
{
header: () => <span className="text-sm font-semibold">Model Access Group</span>,
accessorKey: "model_info.model_access_group",
enableSorting: false,
size: 180,
minSize: 100,
cell: ({ row }) => {
const model = row.original;
const accessGroups = model.model_info.access_groups;
if (!accessGroups || accessGroups.length === 0) {
return "-";
}
const modelId = model.model_info.id;
const isExpanded = expandedRows.has(modelId);
const shouldShowExpandButton = accessGroups.length > 1;
const toggleExpanded = () => {
const newExpanded = new Set(expandedRows);
if (isExpanded) {
newExpanded.delete(modelId);
} else {
newExpanded.add(modelId);
}
setExpandedRows(newExpanded);
};
return (
<div className="flex items-center gap-1 overflow-hidden w-full">
<Badge size="xs" color="blue" className="text-xs px-1.5 py-0.5 h-5 leading-tight flex-shrink-0">
{accessGroups[0]}
</Badge>
{(isExpanded || (!shouldShowExpandButton && accessGroups.length === 2)) &&
accessGroups.slice(1).map((group: string, index: number) => (
<Badge
key={index + 1}
size="xs"
color="blue"
className="text-xs px-1.5 py-0.5 h-5 leading-tight flex-shrink-0"
>
{group}
</Badge>
))}
{shouldShowExpandButton && (
},
{
header: () => <span className="text-sm font-semibold">Status</span>,
accessorKey: "model_info.db_model",
size: 120,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<div
className={`
inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
${model.model_info.db_model ? "bg-blue-50 text-blue-600" : "bg-muted text-muted-foreground"}
`}
>
{model.model_info.db_model ? "DB Model" : "Config Model"}
</div>
);
},
},
{
id: "actions",
header: () => <span className="text-sm font-semibold">Actions</span>,
size: 60,
minSize: 40,
enableResizing: false,
cell: ({ row }) => {
const model = row.original;
const canEditModel =
userRole === "Admin" || model.model_info?.created_by === userID;
const isConfigModel = !model.model_info?.db_model;
return (
<div className="flex items-center justify-end gap-2 pr-4">
{isConfigModel ? (
<WithTooltip
title="Config model cannot be deleted on the dashboard. Please delete it from the config file."
>
<span className="inline-flex items-center opacity-50 cursor-not-allowed">
<Trash2 className="h-4 w-4" />
</span>
</WithTooltip>
) : (
<WithTooltip title="Delete model">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleExpanded();
}}
className="text-xs text-blue-600 hover:text-blue-800 px-1 py-0.5 rounded hover:bg-blue-50 h-5 leading-tight flex-shrink-0 whitespace-nowrap"
>
{isExpanded ? "−" : `+${accessGroups.length - 1}`}
</button>
)}
</div>
);
},
},
{
header: () => <span className="text-sm font-semibold">Status</span>,
accessorKey: "model_info.db_model",
size: 120,
minSize: 80,
cell: ({ row }) => {
const model = row.original;
return (
<div
className={`
inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
${model.model_info.db_model ? "bg-blue-50 text-blue-600" : "bg-gray-100 text-gray-600"}
`}
>
{model.model_info.db_model ? "DB Model" : "Config Model"}
</div>
);
},
},
{
id: "actions",
header: () => <span className="text-sm font-semibold">Actions</span>,
size: 60,
minSize: 40,
enableResizing: false,
cell: ({ row }) => {
const model = row.original;
const canEditModel = userRole === "Admin" || model.model_info?.created_by === userID;
const isConfigModel = !model.model_info?.db_model;
return (
<div className="flex items-center justify-end gap-2 pr-4">
{isConfigModel ? (
<Tooltip title="Config model cannot be deleted on the dashboard. Please delete it from the config file.">
<span className="inline-flex items-center opacity-50 cursor-not-allowed">
<Trash2 className="h-4 w-4" />
</span>
</Tooltip>
) : (
<Tooltip title="Delete model">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
if (canEditModel && onDeleteClick) {
onDeleteClick(model.model_info.id);
}
}}
className={
!canEditModel
? "opacity-50 cursor-not-allowed"
: "cursor-pointer hover:text-destructive"
if (canEditModel && onDeleteClick) {
onDeleteClick(model.model_info.id);
}
aria-label="Delete model"
>
<Trash2 className="h-4 w-4" />
</button>
</Tooltip>
)}
</div>
);
},
}}
className={
!canEditModel
? "opacity-50 cursor-not-allowed"
: "cursor-pointer hover:text-destructive"
}
aria-label="Delete model"
>
<Trash2 className="h-4 w-4" />
</button>
</WithTooltip>
)}
</div>
);
},
];
},
];

File diff suppressed because one or more lines are too long