Improve display of model tags on Keys, Teams and Org page (#11022)

* added accordian for models tag

* remove margin top

* match badge styles same as on teams page

* add badge accordian for organizations

* badges fixed on keys page

* fixed for singular and plural

* fix merge conflict
This commit is contained in:
tanjiro 2025-05-29 08:41:26 +09:00 • committed by GitHub
parent 461e1a4751
commit 14cdf61537
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 423 additions and 212 deletions

View file

@ -33,8 +33,11 @@ import {
TableBody,
TableRow,
TableCell,
Icon,
} from "@tremor/react";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
import { Badge, Text } from "@tremor/react";
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
interface AllKeysTableProps {
keys: KeyResponse[];
@ -152,6 +155,7 @@ export function AllKeysTable({
desc: true
}];
});
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
// Use the filter logic hook
@ -363,19 +367,93 @@ export function AllKeysTable({
cell: (info) => {
const models = info.getValue() as string[];
return (
<div className="flex flex-wrap gap-1">
{models && models.length > 0 ? (
models.map((model, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 rounded text-xs"
>
{model}
</span>
))
) : (
"-"
)}
<div className="flex flex-col py-2">
{Array.isArray(models) ? (
<div className="flex flex-col">
{models.length === 0 ? (
<Badge size={"xs"} className="mb-1" color="red">
<Text>All Proxy Models</Text>
</Badge>
) : (
<>
<div className="flex items-start">
{models.length > 3 && (
<div>
<Icon
icon={expandedAccordions[info.row.id] ? ChevronDownIcon : ChevronRightIcon}
className="cursor-pointer"
size="xs"
onClick={() => {
setExpandedAccordions(prev => ({
...prev,
[info.row.id]: !prev[info.row.id]
}));
}}
/>
</div>
)}
<div className="flex flex-wrap gap-1">
{models.slice(0, 3).map((model, index) => (
model === "all-proxy-models" ? (
<Badge
key={index}
size={"xs"}
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index}
size={"xs"}
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
))}
{models.length > 3 && !expandedAccordions[info.row.id] && (
<Badge size={"xs"} color="gray" className="cursor-pointer">
<Text>+{models.length - 3} {models.length - 3 === 1 ? 'more model' : 'more models'}</Text>
</Badge>
)}
{expandedAccordions[info.row.id] && (
<div className="flex flex-wrap gap-1">
{models.slice(3).map((model, index) => (
model === "all-proxy-models" ? (
<Badge
key={index + 3}
size={"xs"}
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index + 3}
size={"xs"}
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
))}
</div>
)}
</div>
</div>
</>
)}
</div>
) : null}
</div>
);
},
@ -621,11 +699,12 @@ export function AllKeysTable({
{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)]'
: ''
}`}
style={{
maxWidth: "8-x",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === 'models' && (cell.getValue() as string[]).length > 3 ? "px-0" : ""}`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>

View file

@ -23,7 +23,7 @@ import NumericalInput from "./shared/numerical_input";
import { Input } from "antd";
import { Modal, Form, Tooltip, Select as Select2 } from "antd";
import { InfoCircleOutlined } from '@ant-design/icons';
import { PencilAltIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
import { PencilAltIcon, TrashIcon, RefreshIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
import { TextInput } from "@tremor/react";
import { getModelDisplayName } from './key_team_helpers/fetch_available_models_team_key';
import { message } from 'antd';
@ -65,6 +65,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
const [orgToDelete, setOrgToDelete] = useState<string | null>(null);
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
const [form] = Form.useForm();
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
useEffect(() => {
if (organizations.length === 0 && accessToken) {
@ -264,169 +265,248 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
</TableRow>
</TableHead>
<TableBody>
{organizations && organizations.length > 0
? organizations
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.map((org: Organization) => (
<TableRow key={org.organization_id}>
<TableCell>
<div className="overflow-hidden">
<Tooltip title={org.organization_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]"
onClick={() => setSelectedOrgId(org.organization_id)}
>
{org.organization_id?.slice(0, 7)}...
</Button>
</Tooltip>
</div>
</TableCell>
<TableCell>{org.organization_alias}</TableCell>
<TableCell>
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
</TableCell>
<TableCell>{org.spend.toFixed(4)}</TableCell>
<TableCell>
{org.litellm_budget_table?.max_budget !== null && org.litellm_budget_table?.max_budget !== undefined ? org.litellm_budget_table?.max_budget : "No limit"}
</TableCell>
<TableCell>
{Array.isArray(org.models) && (
<div className="flex flex-col">
{org.models.length === 0 ? (
<Badge size="xs" className="mb-1" color="red">
All Proxy Models
</Badge>
) : (
org.models.map((model, index) =>
model === "all-proxy-models" ? (
<Badge key={index} size="xs" className="mb-1" color="red">
All Proxy Models
</Badge>
) : (
<Badge key={index} size="xs" className="mb-1" color="blue">
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Badge>
)
)
)}
</div>
)}
</TableCell>
<TableCell>
<Text>
TPM: {org.litellm_budget_table?.tpm_limit ? org.litellm_budget_table?.tpm_limit : "Unlimited"}
<br />
RPM: {org.litellm_budget_table?.rpm_limit ? org.litellm_budget_table?.rpm_limit : "Unlimited"}
</Text>
</TableCell>
<TableCell>
<Text>{org.members?.length || 0} Members</Text>
</TableCell>
<TableCell>
{userRole === "Admin" && (
<TableBody>
{organizations && organizations.length > 0
? organizations
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.map((org: Organization) => (
<TableRow key={org.organization_id}>
<TableCell>
<div className="overflow-hidden">
<Tooltip title={org.organization_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]"
onClick={() => setSelectedOrgId(org.organization_id)}
>
{org.organization_id?.slice(0, 7)}...
</Button>
</Tooltip>
</div>
</TableCell>
<TableCell>{org.organization_alias}</TableCell>
<TableCell>
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
</TableCell>
<TableCell>{org.spend}</TableCell>
<TableCell>
{org.litellm_budget_table?.max_budget !== null && org.litellm_budget_table?.max_budget !== undefined ? org.litellm_budget_table?.max_budget : "No limit"}
</TableCell>
<TableCell
style={{
maxWidth: "8-x",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
className={org.models.length > 3 ? "px-0" : ""}
>
<div className="flex flex-col">
{Array.isArray(org.models) ? (
<div className="flex flex-col">
{org.models.length === 0 ? (
<Badge size={"xs"} className="mb-1" color="red">
<Text>All Proxy Models</Text>
</Badge>
) : (
<>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
setSelectedOrgId(org.organization_id);
setEditOrg(true);
}}
/>
<Icon
onClick={() => handleDelete(org.organization_id)}
icon={TrashIcon}
size="sm"
/>
<div className="flex items-start">
{org.models.length > 3 && (
<div>
<Icon
icon={expandedAccordions[org.organization_id || ''] ? ChevronDownIcon : ChevronRightIcon}
className="cursor-pointer"
size="xs"
onClick={() => {
setExpandedAccordions(prev => ({
...prev,
[org.organization_id || '']: !prev[org.organization_id || '']
}));
}}
/>
</div>
)}
<div className="flex flex-wrap gap-1">
{org.models.slice(0, 3).map((model, index) => (
model === "all-proxy-models" ? (
<Badge
key={index}
size={"xs"}
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index}
size={"xs"}
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
))}
{org.models.length > 3 && !expandedAccordions[org.organization_id || ''] && (
<Badge size={"xs"} color="gray" className="cursor-pointer">
<Text>+{org.models.length - 3} {org.models.length - 3 === 1 ? 'more model' : 'more models'}</Text>
</Badge>
)}
{expandedAccordions[org.organization_id || ''] && (
<div className="flex flex-wrap gap-1">
{org.models.slice(3).map((model, index) => (
model === "all-proxy-models" ? (
<Badge
key={index + 3}
size={"xs"}
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index + 3}
size={"xs"}
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
))}
</div>
)}
</div>
</div>
</>
)}
</TableCell>
</TableRow>
))
: null}
</TableBody>
</Table>
</Card>
</Col>
{(userRole === "Admin" || userRole === "Org Admin") && (
<Col numColSpan={1}>
<Modal
title="Create Organization"
visible={isOrgModalVisible}
width={800}
footer={null}
onCancel={handleCancel}
</div>
) : null}
</div>
</TableCell>
<TableCell>
<Text>
TPM: {org.litellm_budget_table?.tpm_limit ? org.litellm_budget_table?.tpm_limit : "Unlimited"}
<br />
RPM: {org.litellm_budget_table?.rpm_limit ? org.litellm_budget_table?.rpm_limit : "Unlimited"}
</Text>
</TableCell>
<TableCell>
<Text>{org.members?.length || 0} Members</Text>
</TableCell>
<TableCell>
{userRole === "Admin" && (
<>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => {
setSelectedOrgId(org.organization_id);
setEditOrg(true);
}}
/>
<Icon
onClick={() => handleDelete(org.organization_id)}
icon={TrashIcon}
size="sm"
/>
</>
)}
</TableCell>
</TableRow>
))
: null}
</TableBody>
</Table>
</Card>
</Col>
{(userRole === "Admin" || userRole === "Org Admin") && (
<Col numColSpan={1}>
<Button
className="mx-auto"
onClick={() => setIsOrgModalVisible(true)}
>
+ Create New Organization
</Button>
<Modal
title="Create Organization"
visible={isOrgModalVisible}
width={800}
footer={null}
onCancel={handleCancel}
>
<Form
form={form}
onFinish={handleCreate}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form.Item
label="Organization Name"
name="organization_alias"
rules={[{ required: true, message: "Please input an organization name" }]}
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="Models" name="models">
<Select2
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
<Form
form={form}
onFinish={handleCreate}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form.Item
label="Organization Name"
name="organization_alias"
rules={[{ required: true, message: "Please input an organization name" }]}
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="Models" name="models">
<Select2
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
<Select2.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select2.Option>
{userModels && userModels.length > 0 && userModels.map((model) => (
<Select2.Option key={model} value={model}>
{getModelDisplayName(model)}
</Select2.Option>
))}
</Select2>
</Form.Item>
<Select2.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select2.Option>
{userModels && userModels.length > 0 && userModels.map((model) => (
<Select2.Option key={model} value={model}>
{getModelDisplayName(model)}
</Select2.Option>
))}
</Select2>
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<NumericalInput step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Select2 defaultValue={null} placeholder="n/a">
<Select2.Option value="24h">daily</Select2.Option>
<Select2.Option value="7d">weekly</Select2.Option>
<Select2.Option value="30d">monthly</Select2.Option>
</Select2>
</Form.Item>
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
<NumericalInput step={1} width={400} />
</Form.Item>
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
<NumericalInput step={1} width={400} />
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<NumericalInput step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Select2 defaultValue={null} placeholder="n/a">
<Select2.Option value="24h">daily</Select2.Option>
<Select2.Option value="7d">weekly</Select2.Option>
<Select2.Option value="30d">monthly</Select2.Option>
</Select2>
</Form.Item>
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
<NumericalInput step={1} width={400} />
</Form.Item>
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
<NumericalInput step={1} width={400} />
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="submit">Create Organization</Button>
</div>
</Form>
</Modal>
</Col>
)}
</Grid>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="submit">Create Organization</Button>
</div>
</Form>
</Modal>
</Col>
)}
</Grid>
</TabPanel>
</TabPanels>
</TabGroup>
</Col>
</Grid>
{isDeleteModalOpen ? (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">

View file

@ -11,6 +11,8 @@ import {
RefreshIcon,
StatusOnlineIcon,
TrashIcon,
ChevronDownIcon,
ChevronRightIcon
} from "@heroicons/react/outline";
import {
Button as Button2,
@ -177,6 +179,7 @@ const Teams: React.FC<TeamProps> = ({
// Add this state near the other useState declarations
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
useEffect(() => {
console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
@ -697,48 +700,97 @@ const Teams: React.FC<TeamProps> = ({
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
className={team.models.length > 3 ? "px-0" : ""}
>
{Array.isArray(team.models) ? (
<div
style={{
display: "flex",
flexDirection: "column",
}}
>
{team.models.length === 0 ? (
<Badge size={"xs"} className="mb-1" color="red">
<Text>All Proxy Models</Text>
</Badge>
) : (
team.models.map(
(model: string, index: number) =>
model === "all-proxy-models" ? (
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index}
size={"xs"}
className="mb-1"
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
)
)}
</div>
) : null}
<div className="flex flex-col">
{Array.isArray(team.models) ? (
<div className="flex flex-col">
{team.models.length === 0 ? (
<Badge size={"xs"} className="mb-1" color="red">
<Text>All Proxy Models</Text>
</Badge>
) : (
<>
<div className="flex items-start">
{team.models.length > 3 && (
<div>
<Icon
icon={expandedAccordions[team.team_id] ? ChevronDownIcon : ChevronRightIcon}
className="cursor-pointer"
size="xs"
onClick={() => {
setExpandedAccordions(prev => ({
...prev,
[team.team_id]: !prev[team.team_id]
}));
}}
/>
</div>
)}
<div className="flex flex-wrap gap-1">
{team.models.slice(0, 3).map((model: string, index: number) => (
model === "all-proxy-models" ? (
<Badge
key={index}
size={"xs"}
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index}
size={"xs"}
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
))}
{team.models.length > 3 && !expandedAccordions[team.team_id] && (
<Badge size={"xs"} color="gray" className="cursor-pointer">
<Text>+{team.models.length - 3} {team.models.length - 3 === 1 ? 'more model' : 'more models'}</Text>
</Badge>
)}
{expandedAccordions[team.team_id] && (
<div className="flex flex-wrap gap-1">
{team.models.slice(3).map((model: string, index: number) => (
model === "all-proxy-models" ? (
<Badge
key={index + 3}
size={"xs"}
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
) : (
<Badge
key={index + 3}
size={"xs"}
color="blue"
>
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
))}
</div>
)}
</div>
</div>
</>
)}
</div>
) : null}
</div>
</TableCell>
<TableCell>