mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-12 18:25:41 +00:00
fix model page col resize
This commit is contained in:
@@ -95,7 +95,14 @@ export function AllModelsDataTable<TData, TValue>({
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="relative min-w-full">
|
||||
<Table className="[&_td]:py-2 [&_th]:py-2 w-full">
|
||||
<Table
|
||||
className="[&_td]:py-2 [&_th]:py-2"
|
||||
style={{
|
||||
width: tableInstance.getTotalSize(),
|
||||
minWidth: "100%",
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
@@ -171,7 +178,7 @@ export function AllModelsDataTable<TData, TValue>({
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`py-0.5 ${cell.column.id === "actions"
|
||||
className={`py-0.5 overflow-hidden ${cell.column.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] w-[120px] ml-8"
|
||||
: ""
|
||||
} ${cell.column.columnDef.meta?.className || ""}`}
|
||||
|
||||
@@ -94,7 +94,14 @@ export function ModelDataTable<TData, TValue>({
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="relative min-w-full">
|
||||
<Table className="[&_td]:py-2 [&_th]:py-2 w-full">
|
||||
<Table
|
||||
className="[&_td]:py-2 [&_th]:py-2"
|
||||
style={{
|
||||
width: tableInstance.getTotalSize(),
|
||||
minWidth: "100%",
|
||||
tableLayout: "fixed",
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
{tableInstance.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
@@ -161,7 +168,7 @@ export function ModelDataTable<TData, TValue>({
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`py-0.5 ${
|
||||
className={`py-0.5 overflow-hidden ${
|
||||
cell.column.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] w-[120px] ml-8"
|
||||
: ""
|
||||
|
||||
@@ -0,0 +1,808 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { useReactTable, getCoreRowModel, flexRender } from "@tanstack/react-table";
|
||||
import { columns } from "./columns";
|
||||
import { ModelData } from "../../model_dashboard/types";
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import * as providerInfoHelpers from "../../provider_info_helpers";
|
||||
|
||||
vi.mock("../../provider_info_helpers");
|
||||
|
||||
vi.mock("@tremor/react", async (importOriginal) => {
|
||||
const React = await import("react");
|
||||
const actual = await importOriginal<typeof import("@tremor/react")>();
|
||||
return {
|
||||
...actual,
|
||||
Icon: React.forwardRef<HTMLButtonElement, any>(({ icon: IconComponent, onClick, className, ...props }, ref) => {
|
||||
const ariaLabel = className?.includes("cursor-not-allowed")
|
||||
? "Config model cannot be deleted on the dashboard. Please delete it from the config file."
|
||||
: "Delete model";
|
||||
return React.createElement(
|
||||
"button",
|
||||
{ ...props, onClick, className, ref, "aria-label": ariaLabel },
|
||||
IconComponent && React.createElement(IconComponent, { className: "w-4 h-4" }),
|
||||
);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const createMockModel = (overrides: Partial<ModelData> = {}): ModelData => ({
|
||||
model_info: {
|
||||
id: "test-model-id",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-02T00:00:00Z",
|
||||
created_by: "test-user",
|
||||
team_id: "test-team-id",
|
||||
db_model: true,
|
||||
access_groups: ["group1"],
|
||||
},
|
||||
model_name: "test-model",
|
||||
provider: "openai",
|
||||
litellm_model_name: "gpt-4",
|
||||
input_cost: 0.01,
|
||||
output_cost: 0.03,
|
||||
max_tokens: 4096,
|
||||
max_input_tokens: 8192,
|
||||
litellm_params: {
|
||||
model: "gpt-4",
|
||||
litellm_credential_name: "test-credential",
|
||||
},
|
||||
cleanedLitellmParams: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const TestTable = ({
|
||||
data,
|
||||
columns: cols,
|
||||
}: {
|
||||
data: ModelData[];
|
||||
columns: ReturnType<typeof columns>;
|
||||
}) => {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns: cols,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
describe("columns", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(providerInfoHelpers.getProviderLogoAndName).mockImplementation((provider: string) => {
|
||||
const providerMap: Record<string, { displayName: string; logo: string }> = {
|
||||
openai: { displayName: "OpenAI", logo: "/openai-logo.png" },
|
||||
anthropic: { displayName: "Anthropic", logo: "/anthropic-logo.png" },
|
||||
};
|
||||
return providerMap[provider] || { displayName: provider || "Unknown provider", logo: "" };
|
||||
});
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
userRole: "Admin",
|
||||
userID: "test-user",
|
||||
premiumUser: false,
|
||||
setSelectedModelId: vi.fn(),
|
||||
setSelectedTeamId: vi.fn(),
|
||||
getDisplayModelName: vi.fn((model: ModelData) => model.model_name || "-"),
|
||||
handleEditClick: vi.fn(),
|
||||
handleRefreshClick: vi.fn(),
|
||||
expandedRows: new Set<string>(),
|
||||
setExpandedRows: vi.fn(),
|
||||
};
|
||||
|
||||
it("should render columns with table structure", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel();
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("Model ID")).toBeInTheDocument();
|
||||
expect(screen.getByText("Model Information")).toBeInTheDocument();
|
||||
expect(screen.getByText("Credentials")).toBeInTheDocument();
|
||||
expect(screen.getByText("Created By")).toBeInTheDocument();
|
||||
expect(screen.getByText("Updated At")).toBeInTheDocument();
|
||||
expect(screen.getByText("Costs")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team ID")).toBeInTheDocument();
|
||||
expect(screen.getByText("Model Access Group")).toBeInTheDocument();
|
||||
expect(screen.getByText("Status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Actions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display model information with provider logo", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_name: "GPT-4",
|
||||
provider: "openai",
|
||||
litellm_model_name: "gpt-4",
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("GPT-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display credential name when available", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
litellm_params: {
|
||||
model: "gpt-4",
|
||||
litellm_credential_name: "my-credential",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("my-credential")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display 'No credentials' when credential name is missing", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
litellm_params: {
|
||||
model: "gpt-4",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("No credentials")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display created by information for DB models", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: true,
|
||||
created_by: "admin-user",
|
||||
created_at: "2024-01-15T10:30:00Z",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("admin-user")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display 'Defined in config' for config models", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: false,
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("Defined in config")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display costs when available", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
input_cost: 0.01,
|
||||
output_cost: 0.03,
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("In: $0.01")).toBeInTheDocument();
|
||||
expect(screen.getByText("Out: $0.03")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display '-' when costs are missing", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
input_cost: undefined as any,
|
||||
output_cost: undefined as any,
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const costCells = screen.getAllByText("-");
|
||||
expect(costCells.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should display '-' when team ID is missing", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
team_id: "",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const teamIdCells = screen.getAllByText("-");
|
||||
expect(teamIdCells.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should display access groups", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
access_groups: ["group1", "group2"],
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("group1")).toBeInTheDocument();
|
||||
expect(screen.getByText("+1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should expand access groups when expand button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setExpandedRows = vi.fn();
|
||||
const expandedRows = new Set<string>();
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
expandedRows,
|
||||
setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
id: "model-with-groups",
|
||||
access_groups: ["group1", "group2", "group3"],
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const expandButton = screen.getByText("+2");
|
||||
expect(expandButton).toBeInTheDocument();
|
||||
|
||||
await user.click(expandButton);
|
||||
expect(setExpandedRows).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should display '-' when access groups are empty", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
access_groups: null,
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const emptyCells = screen.getAllByText("-");
|
||||
expect(emptyCells.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should display 'DB Model' status for DB models", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: true,
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("DB Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display 'Config Model' status for config models", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: false,
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("Config Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should allow Admin to delete DB models", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setSelectedModelId = vi.fn();
|
||||
const cols = columns(
|
||||
"Admin",
|
||||
"admin-user",
|
||||
defaultProps.premiumUser,
|
||||
setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: true,
|
||||
id: "deletable-model",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete model" });
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
await user.click(deleteButton);
|
||||
expect(setSelectedModelId).toHaveBeenCalledWith("deletable-model");
|
||||
});
|
||||
|
||||
it("should allow model creator to delete their own DB models", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setSelectedModelId = vi.fn();
|
||||
const cols = columns(
|
||||
"User",
|
||||
"model-creator",
|
||||
defaultProps.premiumUser,
|
||||
setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: true,
|
||||
created_by: "model-creator",
|
||||
id: "user-model",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete model" });
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
|
||||
await user.click(deleteButton);
|
||||
expect(setSelectedModelId).toHaveBeenCalledWith("user-model");
|
||||
});
|
||||
|
||||
|
||||
it("should disable delete for config models", () => {
|
||||
const cols = columns(
|
||||
"Admin",
|
||||
"admin-user",
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: false,
|
||||
},
|
||||
});
|
||||
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");
|
||||
});
|
||||
|
||||
it("should display collapsed access groups with expand button", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
new Set<string>(),
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
access_groups: ["group1", "group2", "group3"],
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("group1")).toBeInTheDocument();
|
||||
expect(screen.getByText("+2")).toBeInTheDocument();
|
||||
expect(screen.queryByText("group2")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("group3")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display expanded access groups when expanded", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
new Set<string>(["test-model-id"]),
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
id: "test-model-id",
|
||||
access_groups: ["group1", "group2", "group3"],
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("group1")).toBeInTheDocument();
|
||||
expect(screen.getByText("group2")).toBeInTheDocument();
|
||||
expect(screen.getByText("group3")).toBeInTheDocument();
|
||||
expect(screen.getByText("−")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display single access group without expand button", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
access_groups: ["group1"],
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("group1")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/\+/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
it("should handle missing display name gracefully", () => {
|
||||
const getDisplayModelName = vi.fn(() => "");
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel();
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("-")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle missing created_at date", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
created_at: "",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("Unknown date")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle missing updated_at date", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
updated_at: "",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
const updatedAtCells = screen.getAllByText("-");
|
||||
expect(updatedAtCells.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should handle missing created_by for DB models", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
model_info: {
|
||||
...createMockModel().model_info,
|
||||
db_model: true,
|
||||
created_by: "",
|
||||
},
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("Unknown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display only input cost when output cost is missing", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
input_cost: 0.01,
|
||||
output_cost: undefined as any,
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("In: $0.01")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/Out:/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display only output cost when input cost is missing", () => {
|
||||
const cols = columns(
|
||||
defaultProps.userRole,
|
||||
defaultProps.userID,
|
||||
defaultProps.premiumUser,
|
||||
defaultProps.setSelectedModelId,
|
||||
defaultProps.setSelectedTeamId,
|
||||
defaultProps.getDisplayModelName,
|
||||
defaultProps.handleEditClick,
|
||||
defaultProps.handleRefreshClick,
|
||||
defaultProps.expandedRows,
|
||||
defaultProps.setExpandedRows,
|
||||
);
|
||||
|
||||
const model = createMockModel({
|
||||
input_cost: undefined as any,
|
||||
output_cost: 0.03,
|
||||
});
|
||||
render(<TestTable data={[model]} columns={cols} />);
|
||||
|
||||
expect(screen.getByText("Out: $0.03")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/In:/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import { KeyIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Badge, Button, Icon } from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import { Popover, Tooltip, Typography, Space, Flex } from "antd";
|
||||
import { ModelData } from "../../model_dashboard/types";
|
||||
import { ProviderLogo } from "./ProviderLogo";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const columns = (
|
||||
userRole: string,
|
||||
userID: string,
|
||||
@@ -21,16 +23,20 @@ export const columns = (
|
||||
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}>
|
||||
<div
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
|
||||
<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={() => setSelectedModelId(model.model_info.id)}
|
||||
>
|
||||
{model.model_info.id}
|
||||
</div>
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
@@ -38,28 +44,67 @@ export const columns = (
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Model Information</span>,
|
||||
accessorKey: "model_name",
|
||||
size: 250, // Fixed column width
|
||||
size: 250,
|
||||
minSize: 120,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const displayName = getDisplayModelName(row.original) || "-";
|
||||
const tooltipContent = (
|
||||
<div>
|
||||
<div>
|
||||
<strong>Provider:</strong> {model.provider || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Public Model Name:</strong> {displayName}
|
||||
</div>
|
||||
<div>
|
||||
<strong>LiteLLM Model Name:</strong> {model.litellm_model_name || "-"}
|
||||
</div>
|
||||
</div>
|
||||
const popoverContent = (
|
||||
<Space
|
||||
direction="vertical"
|
||||
size={12}
|
||||
style={{ minWidth: 220 }}
|
||||
>
|
||||
<Flex align="center" gap={8}>
|
||||
<ProviderLogo provider={model.provider} />
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12 }}
|
||||
ellipsis
|
||||
>
|
||||
{model.provider || "Unknown provider"}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<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>
|
||||
|
||||
<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 (
|
||||
<Tooltip title={tooltipContent}>
|
||||
<div className="flex items-start space-x-2 min-w-0 w-full max-w-[250px]">
|
||||
{/* Provider Icon */}
|
||||
<Popover content={popoverContent} placement="right" arrow={{ pointAtCenter: true }} styles={{
|
||||
root: {
|
||||
maxWidth: 500,
|
||||
}
|
||||
}}>
|
||||
<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} />
|
||||
@@ -68,17 +113,16 @@ export const columns = (
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model Names Container */}
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
{/* Public Model Name */}
|
||||
<div className="text-xs font-medium text-gray-900 truncate max-w-[210px]">{displayName}</div>
|
||||
{/* LiteLLM Model Name */}
|
||||
<div className="text-xs text-gray-500 truncate mt-0.5 max-w-[210px]">
|
||||
<Text ellipsis className="text-gray-900" style={{ fontSize: 12, fontWeight: 500, lineHeight: '16px' }}>
|
||||
{displayName}
|
||||
</Text>
|
||||
<Text ellipsis type="secondary" style={{ fontSize: 12, lineHeight: '16px', marginTop: 2 }}>
|
||||
{model.litellm_model_name || "-"}
|
||||
</div>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -86,14 +130,15 @@ export const columns = (
|
||||
header: () => <span className="text-sm font-semibold">Credentials</span>,
|
||||
accessorKey: "litellm_credential_name",
|
||||
enableSorting: false,
|
||||
size: 180, // Fixed column width
|
||||
size: 180,
|
||||
minSize: 100,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const credentialName = model.litellm_params?.litellm_credential_name;
|
||||
|
||||
return credentialName ? (
|
||||
<Tooltip title={`Credential: ${credentialName}`}>
|
||||
<div className="flex items-center space-x-2 max-w-[180px]">
|
||||
<div className="flex items-center space-x-2 min-w-0 w-full">
|
||||
<KeyIcon className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
||||
<span className="text-xs truncate" title={credentialName}>
|
||||
{credentialName}
|
||||
@@ -101,7 +146,7 @@ export const columns = (
|
||||
</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 max-w-[180px]">
|
||||
<div className="flex items-center space-x-2 min-w-0 w-full">
|
||||
<KeyIcon className="w-4 h-4 text-gray-300 flex-shrink-0" />
|
||||
<span className="text-xs text-gray-400">No credentials</span>
|
||||
</div>
|
||||
@@ -112,7 +157,8 @@ export const columns = (
|
||||
header: () => <span className="text-sm font-semibold">Created By</span>,
|
||||
accessorKey: "model_info.created_by",
|
||||
sortingFn: "datetime",
|
||||
size: 160, // Fixed column width
|
||||
size: 160,
|
||||
minSize: 100,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const isConfigModel = !model.model_info?.db_model;
|
||||
@@ -120,7 +166,7 @@ export const columns = (
|
||||
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 max-w-[160px]">
|
||||
<div className="flex flex-col min-w-0 w-full">
|
||||
{/* Created By - Primary */}
|
||||
<div
|
||||
className="text-xs font-medium text-gray-900 truncate"
|
||||
@@ -143,6 +189,8 @@ export const columns = (
|
||||
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 (
|
||||
@@ -155,7 +203,8 @@ export const columns = (
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Costs</span>,
|
||||
accessorKey: "input_cost",
|
||||
size: 120, // Fixed column width
|
||||
size: 120,
|
||||
minSize: 80,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const inputCost = model.input_cost;
|
||||
@@ -164,7 +213,7 @@ export const columns = (
|
||||
// If both costs are missing or undefined, show "-"
|
||||
if (!inputCost && !outputCost) {
|
||||
return (
|
||||
<div className="max-w-[120px]">
|
||||
<div className="w-full">
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
</div>
|
||||
);
|
||||
@@ -172,7 +221,7 @@ export const columns = (
|
||||
|
||||
return (
|
||||
<Tooltip title="Cost per 1M tokens">
|
||||
<div className="flex flex-col min-w-0 max-w-[120px]">
|
||||
<div className="flex flex-col min-w-0 w-full">
|
||||
{/* Input Cost - Primary */}
|
||||
{inputCost && <div className="text-xs font-medium text-gray-900 truncate">In: ${inputCost}</div>}
|
||||
{/* Output Cost - Secondary */}
|
||||
@@ -186,15 +235,17 @@ export const columns = (
|
||||
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">
|
||||
<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 max-w-[200px]"
|
||||
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={() => setSelectedTeamId(model.model_info.team_id)}
|
||||
>
|
||||
{model.model_info.team_id.slice(0, 7)}...
|
||||
@@ -210,6 +261,8 @@ export const columns = (
|
||||
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;
|
||||
@@ -233,7 +286,7 @@ export const columns = (
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 overflow-hidden">
|
||||
<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>
|
||||
@@ -268,6 +321,8 @@ export const columns = (
|
||||
{
|
||||
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 (
|
||||
@@ -285,6 +340,9 @@ export const columns = (
|
||||
{
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user