feat(ui): common_components/ModelAliasManager to shadcn

- @tremor Card/Title/Text + @heroicons PlusCircle/Pencil/Trash \u2192
  shadcn Card/Button/Input + lucide Plus/Pencil/Trash2.
- Tremor Table family preserved (phase 1 whitelist).
- Edit/Delete action chips keep categorical blue/red tints; file added
  to .eslintrc.json overrides. Added-alias button reuses categorical
  emerald when enabled.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 16:18:51 +00:00
parent 3ee523b8ae
commit 5ff1af0dea
No known key found for this signature in database
2 changed files with 124 additions and 86 deletions

View file

@ -60,6 +60,7 @@
"src/components/policies/policy_table.tsx",
"src/components/guardrails/guardrail_table.tsx",
"src/components/prompts/prompt_table.tsx",
"src/components/common_components/ModelAliasManager.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,6 +1,18 @@
import React, { useState, useEffect } from "react";
import { PlusCircleIcon, PencilIcon, TrashIcon } from "@heroicons/react/outline";
import { Card, Title, Text, Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
import { Pencil, Plus, Trash2 } from "lucide-react";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
} from "@tremor/react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import ModelSelector from "./ModelSelector";
import NotificationsManager from "../molecules/notifications_manager";
@ -28,24 +40,34 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
const [editingAlias, setEditingAlias] = useState<AliasItem | null>(null);
useEffect(() => {
// Convert object to array for display
const aliasArray = Object.entries(initialModelAliases).map(([aliasName, targetModel], index) => ({
id: `${index}-${aliasName}`,
aliasName,
targetModel,
}));
const aliasArray = Object.entries(initialModelAliases).map(
([aliasName, targetModel], index) => ({
id: `${index}-${aliasName}`,
aliasName,
targetModel,
}),
);
setAliases(aliasArray);
}, [initialModelAliases]);
const notifyParent = (list: AliasItem[]) => {
const obj: { [key: string]: string } = {};
list.forEach((a) => (obj[a.aliasName] = a.targetModel));
if (onAliasUpdate) onAliasUpdate(obj);
};
const handleAddAlias = () => {
if (!newAlias.aliasName || !newAlias.targetModel) {
NotificationsManager.fromBackend("Please provide both alias name and target model");
NotificationsManager.fromBackend(
"Please provide both alias name and target model",
);
return;
}
// Check for duplicate alias names
if (aliases.some((alias) => alias.aliasName === newAlias.aliasName)) {
NotificationsManager.fromBackend("An alias with this name already exists");
NotificationsManager.fromBackend(
"An alias with this name already exists",
);
return;
}
@ -58,16 +80,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
const updatedAliases = [...aliases, newAliasObj];
setAliases(updatedAliases);
setNewAlias({ aliasName: "", targetModel: "" });
// Convert array back to object format and notify parent
const aliasObject: { [key: string]: string } = {};
updatedAliases.forEach((alias) => {
aliasObject[alias.aliasName] = alias.targetModel;
});
if (onAliasUpdate) {
onAliasUpdate(aliasObject);
}
notifyParent(updatedAliases);
NotificationsManager.success("Alias added successfully");
};
@ -80,30 +93,32 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
if (!editingAlias) return;
if (!editingAlias.aliasName || !editingAlias.targetModel) {
NotificationsManager.fromBackend("Please provide both alias name and target model");
NotificationsManager.fromBackend(
"Please provide both alias name and target model",
);
return;
}
// Check for duplicate alias names (excluding current alias)
if (aliases.some((alias) => alias.id !== editingAlias.id && alias.aliasName === editingAlias.aliasName)) {
NotificationsManager.fromBackend("An alias with this name already exists");
if (
aliases.some(
(alias) =>
alias.id !== editingAlias.id &&
alias.aliasName === editingAlias.aliasName,
)
) {
NotificationsManager.fromBackend(
"An alias with this name already exists",
);
return;
}
const updatedAliases = aliases.map((alias) => (alias.id === editingAlias.id ? editingAlias : alias));
const updatedAliases = aliases.map((alias) =>
alias.id === editingAlias.id ? editingAlias : alias,
);
setAliases(updatedAliases);
setEditingAlias(null);
// Convert array back to object format and notify parent
const aliasObject: { [key: string]: string } = {};
updatedAliases.forEach((alias) => {
aliasObject[alias.aliasName] = alias.targetModel;
});
if (onAliasUpdate) {
onAliasUpdate(aliasObject);
}
notifyParent(updatedAliases);
NotificationsManager.success("Alias updated successfully");
};
@ -115,21 +130,11 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
const deleteAlias = (aliasId: string) => {
const updatedAliases = aliases.filter((alias) => alias.id !== aliasId);
setAliases(updatedAliases);
// Convert array back to object format and notify parent
const aliasObject: { [key: string]: string } = {};
updatedAliases.forEach((alias) => {
aliasObject[alias.aliasName] = alias.targetModel;
});
if (onAliasUpdate) {
onAliasUpdate(aliasObject);
}
notifyParent(updatedAliases);
NotificationsManager.success("Alias deleted successfully");
};
// Convert current aliases to object for config example
const aliasObject = aliases.reduce(
(acc, alias) => {
acc[alias.aliasName] = alias.targetModel;
@ -138,14 +143,18 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{} as { [key: string]: string },
);
const addDisabled = !newAlias.aliasName || !newAlias.targetModel;
return (
<div className="mt-4">
<div className="mb-6">
<Text className="text-sm font-medium text-gray-700 mb-2">Add New Alias</Text>
<p className="text-sm font-medium text-foreground mb-2">Add New Alias</p>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-gray-500 mb-1">Alias Name</label>
<input
<label className="block text-xs text-muted-foreground mb-1">
Alias Name
</label>
<Input
type="text"
value={newAlias.aliasName}
onChange={(e) =>
@ -155,11 +164,12 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
})
}
placeholder="e.g., gpt-4o"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Target Model</label>
<label className="block text-xs text-muted-foreground mb-1">
Target Model
</label>
<ModelSelector
accessToken={accessToken}
value={newAlias.targetModel}
@ -174,26 +184,35 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
/>
</div>
<div className="flex items-end">
<button
<Button
onClick={handleAddAlias}
disabled={!newAlias.aliasName || !newAlias.targetModel}
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModel ? "bg-gray-300 text-gray-500 cursor-not-allowed" : "bg-green-600 text-white hover:bg-green-700"}`}
disabled={addDisabled}
className={cn(
!addDisabled &&
"bg-emerald-600 text-white hover:bg-emerald-700",
)}
>
<PlusCircleIcon className="w-4 h-4 mr-1" />
<Plus className="h-4 w-4" />
Add Alias
</button>
</Button>
</div>
</div>
</div>
<Text className="text-sm font-medium text-gray-700 mb-2">Manage Existing Aliases</Text>
<p className="text-sm font-medium text-foreground mb-2">
Manage Existing Aliases
</p>
<div className="rounded-lg custom-border relative mb-6">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
<TableRow>
<TableHeaderCell className="py-1 h-8">Alias Name</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">Target Model</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">
Alias Name
</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">
Target Model
</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">Actions</TableHeaderCell>
</TableRow>
</TableHead>
@ -203,7 +222,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
{editingAlias && editingAlias.id === alias.id ? (
<>
<TableCell className="py-0.5">
<input
<Input
type="text"
value={editingAlias.aliasName}
onChange={(e) =>
@ -212,7 +231,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
aliasName: e.target.value,
})
}
className="w-full px-2 py-1 border border-gray-300 rounded-md text-sm"
className="h-8"
/>
</TableCell>
<TableCell className="py-0.5">
@ -231,39 +250,53 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
<Button
size="sm"
variant="secondary"
onClick={handleUpdateAlias}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
className="h-7"
>
Save
</button>
<button
</Button>
<Button
size="sm"
variant="outline"
onClick={handleCancelEdit}
className="text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100"
className="h-7"
>
Cancel
</button>
</Button>
</div>
</TableCell>
</>
) : (
<>
<TableCell className="py-0.5 text-sm text-gray-900">{alias.aliasName}</TableCell>
<TableCell className="py-0.5 text-sm text-gray-500">{alias.targetModel}</TableCell>
<TableCell className="py-0.5 text-sm text-foreground">
{alias.aliasName}
</TableCell>
<TableCell className="py-0.5 text-sm text-muted-foreground">
{alias.targetModel}
</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-blue-600 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 dark:text-blue-300"
onClick={() => handleEditAlias(alias)}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
aria-label="Edit alias"
>
<PencilIcon className="w-3 h-3" />
</button>
<button
<Pencil className="h-3 w-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-destructive bg-red-50 hover:bg-red-100 dark:bg-red-950/30 dark:hover:bg-red-950/60"
onClick={() => deleteAlias(alias.id)}
className="text-xs bg-red-50 text-red-600 px-2 py-1 rounded hover:bg-red-100"
aria-label="Delete alias"
>
<TrashIcon className="w-3 h-3" />
</button>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</>
@ -272,7 +305,10 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
))}
{aliases.length === 0 && (
<TableRow>
<TableCell colSpan={3} className="py-0.5 text-sm text-gray-500 text-center">
<TableCell
colSpan={3}
className="py-0.5 text-sm text-muted-foreground text-center"
>
No aliases added yet. Add a new alias above.
</TableCell>
</TableRow>
@ -282,16 +318,17 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
</div>
</div>
{/* Configuration Example */}
{showExampleConfig && (
<Card>
<Title className="mb-4">Configuration Example</Title>
<Text className="text-gray-600 mb-4">Here&apos;s how your current aliases would look in the config:</Text>
<div className="bg-gray-100 rounded-lg p-4 font-mono text-sm">
<div className="text-gray-700">
<Card className="p-4">
<h5 className="mb-4 font-semibold">Configuration Example</h5>
<p className="text-muted-foreground mb-4">
Here&apos;s how your current aliases would look in the config:
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<div className="text-foreground">
model_aliases:
{Object.keys(aliasObject).length === 0 ? (
<span className="text-gray-500">
<span className="text-muted-foreground">
<br />
&nbsp;&nbsp;# No aliases configured yet
</span>