(UI) Edit Model flow improvements (#8729)

* order models by created at

* use existing table component on models page

* sorting for created at

* ui clean up models page

* remove provider filter

* fix columns sorting

* decent switching

* ui fix models page

* show edit / delete button on root of table

* clean up columns

* working edit model flow

* decent working model edit page

* fix edit model

* show created at and created by

* ui easy model edit flow

* clean up columns

* ui clean up updated at
This commit is contained in:
Ishaan Jaff 2025-02-21 19:27:18 -08:00 • committed by GitHub
parent ad450b7616
commit 8e99accef5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 430 additions and 178 deletions

View file

@ -1,9 +1,9 @@
import { ColumnDef } from "@tanstack/react-table";
import { Button, Badge } from "@tremor/react";
import { Button, Badge, Icon } from "@tremor/react";
import { Tooltip } from "antd";
import { getProviderLogoAndName } from "../provider_info_helpers";
import { ModelData } from "./types";
import { TrashIcon, PencilIcon } from "@heroicons/react/outline";
import { TrashIcon, PencilIcon, PencilAltIcon } from "@heroicons/react/outline";
import DeleteModelButton from "../delete_model_button";
export const columns = (
@ -13,6 +13,7 @@ export const columns = (
getDisplayModelName: (model: any) => string,
handleEditClick: (model: any) => void,
handleRefreshClick: () => void,
setEditModel: (edit: boolean) => void,
): ColumnDef<ModelData>[] => [
{
header: "Model ID",
@ -36,11 +37,18 @@ export const columns = (
},
},
{
header: "Model Name",
header: "Public Model Name",
accessorKey: "model_name",
cell: ({ row }) => (
<p className="text-xs">{getDisplayModelName(row.original) || "-"}</p>
),
cell: ({ row }) => {
const displayName = getDisplayModelName(row.original) || "-";
return (
<Tooltip title={displayName}>
<p className="text-xs">
{displayName.length > 20 ? displayName.slice(0, 20) + "..." : displayName}
</p>
</Tooltip>
);
},
},
{
header: "Provider",
@ -100,6 +108,19 @@ export const columns = (
);
},
},
{
header: "Updated At",
accessorKey: "model_info.updated_at",
sortingFn: "datetime",
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: "Created By",
accessorKey: "model_info.created_by",
@ -113,7 +134,11 @@ export const columns = (
},
},
{
header: "Input Cost (per 1M tokens)",
header: () => (
<Tooltip title="Cost per 1M tokens">
<span>Input Cost</span>
</Tooltip>
),
accessorKey: "input_cost",
cell: ({ row }) => {
const model = row.original;
@ -125,7 +150,11 @@ export const columns = (
},
},
{
header: "Output Cost (per 1M tokens)",
header: () => (
<Tooltip title="Cost per 1M tokens">
<span>Output Cost</span>
</Tooltip>
),
accessorKey: "output_cost",
cell: ({ row }) => {
const model = row.original;
@ -176,4 +205,31 @@ export const columns = (
);
},
},
{
id: "actions",
header: "",
cell: ({ row }) => {
const model = row.original;
return (
<div className="flex items-center justify-end gap-2 pr-4">
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
setSelectedModelId(model.model_info.id);
setEditModel(true);
}}
/>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => {
setSelectedModelId(model.model_info.id);
setEditModel(false);
}}
/>
</div>
);
},
},
];

View file

@ -46,75 +46,87 @@ export function ModelDataTable<TData, TValue>({
});
return (
<div className="rounded-lg custom-border">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className="py-1 h-8 cursor-pointer hover:bg-gray-50"
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</div>
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
</div>
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>🚅 Loading models...</p>
</div>
</TableCell>
</TableRow>
) : table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap"
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${
header.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
: ''
}`}
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</div>
{header.id !== 'actions' && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
</TableHeaderCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No models found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>🚅 Loading models...</p>
</div>
</TableCell>
</TableRow>
) : table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]'
: ''
}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No models found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View file

@ -1,6 +1,7 @@
export interface ModelInfo {
id: string;
created_at: string;
updated_at: string;
created_by: string;
team_id: string;
db_model: boolean;

View file

@ -11,6 +11,8 @@ import {
Grid,
Badge,
Button as TremorButton,
TextInput,
NumberInput,
} from "@tremor/react";
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline";
import { modelDeleteCall, modelUpdateCall } from "./networking";
@ -43,12 +45,62 @@ export default function ModelInfoView({
setEditModalVisible,
setSelectedModel
}: ModelInfoViewProps) {
const [isEditing, setIsEditing] = useState(false);
const [form] = Form.useForm();
const [localModelData, setLocalModelData] = useState(modelData);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const canEditModel = userRole === "Admin";
const handleModelUpdate = async (values: any) => {
try {
if (!accessToken) return;
setIsSaving(true);
const updateData = {
model_name: values.model_name,
litellm_params: {
...localModelData.litellm_params,
model: values.litellm_model_name,
api_base: values.api_base,
custom_llm_provider: values.custom_llm_provider,
organization: values.organization,
tpm: values.tpm,
rpm: values.rpm,
max_retries: values.max_retries,
timeout: values.timeout,
stream_timeout: values.stream_timeout,
input_cost_per_token: values.input_cost / 1_000_000,
output_cost_per_token: values.output_cost / 1_000_000,
},
model_info: {
id: modelId,
}
};
await modelUpdateCall(accessToken, updateData);
setLocalModelData({
...localModelData,
model_name: values.model_name,
litellm_model_name: values.litellm_model_name,
litellm_params: updateData.litellm_params
});
message.success("Model settings updated successfully");
setIsDirty(false);
setIsEditing(false);
} catch (error) {
console.error("Error updating model:", error);
message.error("Failed to update model settings");
} finally {
setIsSaving(false);
}
};
if (!modelData) {
return (
<div className="p-4">
@ -154,122 +206,253 @@ export default function ModelInfoView({
</Card>
</Grid>
{/* Audit info shown as a subtle banner below the overview */}
<div className="mb-6 text-sm text-gray-500 flex items-center gap-x-6">
<div className="flex items-center gap-x-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Created At {modelData.model_info.created_at
? new Date(modelData.model_info.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
: "Not Set"}
</div>
<div className="flex items-center gap-x-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Created By {modelData.model_info.created_by || "Not Set"}
</div>
</div>
{/* Settings Card */}
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Model Settings</Title>
{(canEditModel && !isEditing) && (
<TremorButton
variant="light"
{canEditModel && !isEditing && (
<TremorButton
variant="secondary"
onClick={() => setIsEditing(true)}
className="flex items-center"
>
Edit Settings
Edit Model
</TremorButton>
)}
</div>
{isEditing ? (
<EditModelModal
visible={isEditing}
onCancel={() => setIsEditing(false)}
model={modelData}
onSubmit={(data: FormData) => handleEditModelSubmit(data, accessToken, setEditModalVisible, setSelectedModel)}
/>
) : (
<Form
form={form}
onFinish={handleModelUpdate}
initialValues={{
model_name: localModelData.model_name,
litellm_model_name: localModelData.litellm_model_name,
api_base: localModelData.litellm_params?.api_base,
custom_llm_provider: localModelData.litellm_params?.custom_llm_provider,
organization: localModelData.litellm_params?.organization,
tpm: localModelData.litellm_params?.tpm,
rpm: localModelData.litellm_params?.rpm,
max_retries: localModelData.litellm_params?.max_retries,
timeout: localModelData.litellm_params?.timeout,
stream_timeout: localModelData.litellm_params?.stream_timeout,
input_cost: localModelData.litellm_params?.input_cost_per_token ?
(localModelData.litellm_params.input_cost_per_token * 1_000_000) : modelData.input_cost * 1_000_000,
output_cost: localModelData.litellm_params?.output_cost_per_token ?
(localModelData.litellm_params.output_cost_per_token * 1_000_000) : modelData.output_cost * 1_000_000,
}}
layout="vertical"
onValuesChange={() => setIsDirty(true)}
>
<div className="space-y-4">
<div>
<Text className="font-medium">Model ID</Text>
<div className="font-mono">{modelData.model_info.id}</div>
</div>
<div>
<Text className="font-medium">Public Model Name</Text>
<div>{getDisplayModelName(modelData)}</div>
<div className="space-y-4">
<div>
<Text className="font-medium">Model Name</Text>
{isEditing ? (
<Form.Item name="model_name" className="mb-0">
<TextInput placeholder="Enter model name" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">{localModelData.model_name}</div>
)}
</div>
<div>
<Text className="font-medium">LiteLLM Model Name</Text>
{isEditing ? (
<Form.Item name="litellm_model_name" className="mb-0">
<TextInput placeholder="Enter LiteLLM model name" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">{localModelData.litellm_model_name}</div>
)}
</div>
<div>
<Text className="font-medium">Input Cost (per 1M tokens)</Text>
{isEditing ? (
<Form.Item name="input_cost" className="mb-0">
<NumberInput placeholder="Enter input cost" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.input_cost_per_token
? (localModelData.litellm_params.input_cost_per_token * 1_000_000).toFixed(4)
: modelData.input_cost * 1_000_000}
</div>
)}
</div>
<div>
<Text className="font-medium">Output Cost (per 1M tokens)</Text>
{isEditing ? (
<Form.Item name="output_cost" className="mb-0">
<NumberInput placeholder="Enter output cost" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.output_cost_per_token
? (localModelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4)
: modelData.output_cost * 1_000_000}
</div>
)}
</div>
<div>
<Text className="font-medium">API Base</Text>
{isEditing ? (
<Form.Item name="api_base" className="mb-0">
<TextInput placeholder="Enter API base" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.api_base || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">Custom LLM Provider</Text>
{isEditing ? (
<Form.Item name="custom_llm_provider" className="mb-0">
<TextInput placeholder="Enter custom LLM provider" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.custom_llm_provider || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">Organization</Text>
{isEditing ? (
<Form.Item name="organization" className="mb-0">
<TextInput placeholder="Enter organization" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.organization || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">TPM (Tokens per Minute)</Text>
{isEditing ? (
<Form.Item name="tpm" className="mb-0">
<NumberInput placeholder="Enter TPM" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.tpm || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">RPM (Requests per Minute)</Text>
{isEditing ? (
<Form.Item name="rpm" className="mb-0">
<NumberInput placeholder="Enter RPM" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.rpm || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">Max Retries</Text>
{isEditing ? (
<Form.Item name="max_retries" className="mb-0">
<NumberInput placeholder="Enter max retries" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.max_retries || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">Timeout (seconds)</Text>
{isEditing ? (
<Form.Item name="timeout" className="mb-0">
<NumberInput placeholder="Enter timeout" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.timeout || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">Stream Timeout (seconds)</Text>
{isEditing ? (
<Form.Item name="stream_timeout" className="mb-0">
<NumberInput placeholder="Enter stream timeout" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
{localModelData.litellm_params?.stream_timeout || "Not Set"}
</div>
)}
</div>
<div>
<Text className="font-medium">Team ID</Text>
<div className="mt-1 p-2 bg-gray-50 rounded">
{modelData.model_info.team_id || "Not Set"}
</div>
</div>
</div>
<div>
<Text className="font-medium">LiteLLM Model Name</Text>
<div>{modelData.litellm_model_name}</div>
</div>
<div>
<Text className="font-medium">Input Cost (per 1M tokens)</Text>
<div>{modelData.litellm_params?.input_cost_per_token ? (modelData.litellm_params.input_cost_per_token * 1_000_000).toFixed(4) : "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Output Cost (per 1M tokens)</Text>
<div>{modelData.litellm_params?.output_cost_per_token ? (modelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4) : "Not Set"}</div>
</div>
<div>
<Text className="font-medium">API Base</Text>
<div>{modelData.litellm_params?.api_base || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Custom LLM Provider</Text>
<div>{modelData.litellm_params?.custom_llm_provider || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Model</Text>
<div>{modelData.litellm_params?.model || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Organization</Text>
<div>{modelData.litellm_params?.organization || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">TPM (Tokens per Minute)</Text>
<div>{modelData.litellm_params?.tpm || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">RPM (Requests per Minute)</Text>
<div>{modelData.litellm_params?.rpm || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Max Retries</Text>
<div>{modelData.litellm_params?.max_retries || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Timeout (seconds)</Text>
<div>{modelData.litellm_params?.timeout || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Stream Timeout (seconds)</Text>
<div>{modelData.litellm_params?.stream_timeout || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Team ID</Text>
<div>{modelData.model_info.team_id || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Created At</Text>
<div>{modelData.model_info.created_at ? new Date(modelData.model_info.created_at).toLocaleString() : "Not Set"}</div>
</div>
<div>
<Text className="font-medium">Created By</Text>
<div>{modelData.model_info.created_by || "Not Set"}</div>
</div>
<div>
<Text className="font-medium">LiteLLM Parameters</Text>
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{JSON.stringify(modelData.cleanedLitellmParams, null, 2)}
</pre>
</div>
{isEditing && (
<div className="mt-6 flex justify-end gap-2">
<TremorButton
variant="secondary"
onClick={() => {
form.resetFields();
setIsDirty(false);
setIsEditing(false);
}}
>
Cancel
</TremorButton>
<TremorButton
variant="primary"
onClick={() => form.submit()}
loading={isSaving}
>
Save Changes
</TremorButton>
</div>
)}
</div>
)}
</Form>
</Card>
</TabPanel>