mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
a0475ee4f7
commit
a9985fc17c
4 changed files with 449 additions and 395 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
];
|
||||
},
|
||||
];
|
||||
|
|
|
|||
1
ui/litellm-dashboard/tsconfig.tsbuildinfo
Normal file
1
ui/litellm-dashboard/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue