Merge pull request #21076 from BerriAI/litellm_ui_model_table_cred

[Feature] UI - Model Page: Improve Credentials Messaging
This commit is contained in:
yuneng-jiang
2026-02-12 16:28:45 -08:00
committed by GitHub
2 changed files with 194 additions and 18 deletions
@@ -200,7 +200,7 @@ describe("columns", () => {
expect(screen.getByText("my-credential")).toBeInTheDocument();
});
it("should display 'No credentials' when credential name is missing", () => {
it("should display 'Manual' when credential name is missing", () => {
const cols = columns(
defaultProps.userRole,
defaultProps.userID,
@@ -221,7 +221,132 @@ describe("columns", () => {
});
render(<TestTable data={[model]} columns={cols} />);
expect(screen.getByText("No credentials")).toBeInTheDocument();
expect(screen.getByText("Manual")).toBeInTheDocument();
});
describe("credentials column", () => {
it("should display Credentials header with info icon", () => {
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("Credentials")).toBeInTheDocument();
// Info icon is in a flex container with Credentials - ant icons render as span with role="img"
const credentialsHeader = screen.getByText("Credentials").closest("span");
expect(credentialsHeader?.parentElement?.querySelector('[role="img"]')).toBeInTheDocument();
});
it("should display reusable credential with SyncOutlined icon and credential name", () => {
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-reusable-credential",
},
});
render(<TestTable data={[model]} columns={cols} />);
expect(screen.getByText("my-reusable-credential")).toBeInTheDocument();
const credentialCell = screen.getByText("my-reusable-credential").closest("div");
expect(credentialCell).toHaveClass("flex");
expect(screen.getByText("my-reusable-credential")).toHaveClass("text-blue-600");
});
it("should display Manual with EditOutlined when no credential name", () => {
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("Manual")).toBeInTheDocument();
expect(screen.getByText("Manual")).toHaveClass("text-gray-500");
});
it("should display Manual when litellm_params is undefined", () => {
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: undefined as any,
});
render(<TestTable data={[model]} columns={cols} />);
expect(screen.getByText("Manual")).toBeInTheDocument();
});
it("should display Manual when litellm_credential_name is empty string", () => {
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: "",
},
});
render(<TestTable data={[model]} columns={cols} />);
expect(screen.getByText("Manual")).toBeInTheDocument();
});
});
it("should display created by information for DB models", () => {
@@ -1,11 +1,45 @@
import { KeyIcon, TrashIcon } from "@heroicons/react/outline";
import { EditOutlined, InfoCircleOutlined, SyncOutlined } from "@ant-design/icons";
import { TrashIcon } from "@heroicons/react/outline";
import { ColumnDef } from "@tanstack/react-table";
import { Badge, Button, Icon } from "@tremor/react";
import { Popover, Tooltip, Typography, Space, Flex } from "antd";
import { Divider, Flex, Popover, Space, Tooltip, Typography } from "antd";
import { ModelData } from "../../model_dashboard/types";
import { ProviderLogo } from "./ProviderLogo";
const { Text } = Typography;
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>
);
export const columns = (
userRole: string,
@@ -127,7 +161,21 @@ export const columns = (
},
},
{
header: () => <span className="text-sm font-semibold">Credentials</span>,
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,
@@ -135,20 +183,23 @@ export const columns = (
cell: ({ row }) => {
const model = row.original;
const credentialName = model.litellm_params?.litellm_credential_name;
const isReusable = !!credentialName;
return credentialName ? (
<Tooltip title={`Credential: ${credentialName}`}>
<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}
</span>
</div>
</Tooltip>
) : (
return (
<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>
{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>
);
},