feat(ui): migrate tool policies and organizations to shadcn

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 12:39:00 +00:00
parent 511049458d
commit 0fd4379e32
No known key found for this signature in database
4 changed files with 549 additions and 364 deletions

View file

@ -24,6 +24,8 @@
"src/components/molecules/notifications_manager.tsx",
"src/components/molecules/message_manager.tsx",
"src/components/ToolPolicies/PolicySelect.tsx",
"src/components/ToolDetail.tsx",
"src/components/ToolPolicies.tsx",
"src/components/GuardrailsMonitor/GuardrailConfig.tsx",
"src/components/GuardrailsMonitor/GuardrailDetail.tsx",
"src/components/GuardrailsMonitor/LogViewer.tsx",

View file

@ -2,8 +2,16 @@
import { ArrowLeft as ArrowLeftOutlined, History as HistoryOutlined, Wrench as ToolOutlined } from "lucide-react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Button, Select, Spin } from "antd";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import React, { useCallback, useMemo, useState } from "react";
import TeamDropdown from "@/components/common_components/team_dropdown";
import { LogViewer } from "@/components/GuardrailsMonitor/LogViewer";
import type { LogEntry } from "@/components/GuardrailsMonitor/mockData";
@ -16,7 +24,6 @@ import {
keyListCall,
teamListCall,
updateToolPolicy,
type ToolPolicyOption,
type ToolPolicyOverrideRow,
} from "@/components/networking";
import type { Team } from "@/components/key_team_helpers/key_list";
@ -210,7 +217,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
if (detailLoading && !detail) {
return (
<div className="flex items-center justify-center py-12">
<Spin size="large" />
<Skeleton className="h-10 w-32" />
</div>
);
}
@ -218,9 +225,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
if (detailError && !detail) {
return (
<div>
<Button type="link" icon={<ArrowLeftOutlined />} onClick={onBack} className="pl-0 mb-4">
<Button variant="link" onClick={onBack} className="pl-0 mb-4">
<ArrowLeftOutlined className="mr-1 h-4 w-4" />
Back to Tool Policies
</Button>
{/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}
<p className="text-red-600">Failed to load tool details.</p>
</div>
);
@ -243,11 +252,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
<div>
<div className="mb-6">
<Button
type="link"
icon={<ArrowLeftOutlined />}
variant="link"
onClick={onBack}
className="pl-0 mb-4"
>
<ArrowLeftOutlined className="mr-1 h-4 w-4" />
Back to Tool Policies
</Button>
@ -341,9 +350,9 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
{!ov.team_id && !ov.key_hash ? "—" : ""}
</span>
<Button
type="link"
danger
size="small"
variant="link"
size="sm"
className="text-destructive"
disabled={overrideSaving}
onClick={() => handleRemoveOverride(ov)}
>
@ -392,29 +401,28 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
/>
) : (
<Select
placeholder="Select key"
allowClear
showSearch
optionFilterProp="label"
value={blockKey ? blockKey.token : undefined}
onChange={(token) => {
value={blockKey ? blockKey.token : ""}
onValueChange={(token) => {
const k = keys.find((x) => x.token === token);
setBlockKey(k ?? null);
}}
options={keys.map((k) => ({
value: k.token,
label: k.key_alias || k.token?.substring?.(0, 12) || k.token,
}))}
className="w-full"
style={{ minWidth: 200 }}
/>
>
<SelectTrigger className="w-full min-w-[200px]">
<SelectValue placeholder="Select key" />
</SelectTrigger>
<SelectContent>
{keys.map((k) => (
<SelectItem key={k.token} value={k.token}>
{k.key_alias || k.token?.substring?.(0, 12) || k.token}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<Button
type="primary"
danger
variant="destructive"
disabled={overrideSaving || (blockScope === "team" ? !blockTeamId : !blockKey?.token)}
loading={overrideSaving}
onClick={handleAddOverride}
>
Block for {blockScope}

View file

@ -1,8 +1,14 @@
"use client";
import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Button, Switch, Tooltip } from "antd";
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
import { TimeCell } from "./view_logs/time_cell";
import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
@ -53,6 +59,23 @@ interface ToolPoliciesProps {
onSelectTool?: (toolName: string) => void;
}
function TT({
title,
children,
}: {
title: React.ReactNode;
children: React.ReactNode;
}) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span>{children}</span>
</TooltipTrigger>
<TooltipContent>{title}</TooltipContent>
</Tooltip>
);
}
export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelectTool }) => {
const [tools, setTools] = useState<ToolRow[]>([]);
const [loading, setLoading] = useState(true);
@ -263,6 +286,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
};
return (
<TooltipProvider>
<div className="w-full">
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Tool Policies</h1>
@ -348,7 +372,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">Live Tail</span>
<Switch checked={isLiveTail} onChange={setIsLiveTail} />
<Switch checked={isLiveTail} onCheckedChange={setIsLiveTail} />
</div>
<button
@ -476,9 +500,9 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
onClick={() => onSelectTool?.(tool.tool_name)}
className="text-left w-full font-mono text-xs max-w-[20ch] truncate block font-medium text-blue-600 hover:text-blue-800 hover:underline focus:outline-none focus:ring-0"
>
<Tooltip title={onSelectTool ? "Click to view details and block for team/key" : tool.tool_name}>
<TT title={onSelectTool ? "Click to view details and block for team/key" : tool.tool_name}>
<span>{tool.tool_name}</span>
</Tooltip>
</TT>
</button>
</TableCell>
<TableCell className="py-0.5 max-h-8">
@ -505,26 +529,26 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
</div>
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.team_id ?? "-"}>
<TT title={tool.team_id ?? "-"}>
<span className="max-w-[15ch] truncate block">{tool.team_id ?? "-"}</span>
</Tooltip>
</TT>
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.key_hash ?? "-"}>
<TT title={tool.key_hash ?? "-"}>
<span className="font-mono max-w-[15ch] truncate block text-blue-600">
{tool.key_hash ?? "-"}
</span>
</Tooltip>
</TT>
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.key_alias ?? "-"}>
<TT title={tool.key_alias ?? "-"}>
<span className="max-w-[15ch] truncate block">{tool.key_alias ?? "-"}</span>
</Tooltip>
</TT>
</TableCell>
<TableCell className="py-0.5 max-h-8 overflow-hidden whitespace-nowrap">
<Tooltip title={tool.user_agent ?? "-"}>
<TT title={tool.user_agent ?? "-"}>
<span className="font-mono max-w-[20ch] truncate block text-xs text-gray-500">{tool.user_agent ?? "-"}</span>
</Tooltip>
</TT>
</TableCell>
</TableRow>
))
@ -559,6 +583,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
</div>
</div>
</TooltipProvider>
);
};

View file

@ -1,30 +1,52 @@
import OrganizationFilters, { FilterState } from "@/app/(dashboard)/organizations/OrganizationFilters";
import { Info as InfoCircleOutlined } from "lucide-react";
import { ChevronDown as ChevronDownIcon, ChevronRight as ChevronRightIcon, RefreshCcw as RefreshIcon } from "lucide-react";
import {
ChevronDown as ChevronDownIcon,
ChevronRight as ChevronRightIcon,
RefreshCcw as RefreshIcon,
} from "lucide-react";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import {
Badge,
Button,
Card,
Col,
Grid,
Icon,
Tab,
TabGroup,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
TabList,
TabPanel,
TabPanels,
Text,
TextInput,
} from "@tremor/react";
import { Form, Input, Modal, Select as Select2, Tooltip } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import React, { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { formatNumberWithCommas } from "../utils/dataUtils";
import DeleteResourceModal from "./common_components/DeleteResourceModal";
import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
@ -50,6 +72,18 @@ interface OrganizationsTableProps {
premiumUser: boolean;
}
interface OrgFormValues {
organization_alias: string;
models: string[];
max_budget: number | null;
budget_duration: string;
tpm_limit: number | null;
rpm_limit: number | null;
allowed_vector_store_ids: string[];
allowed_mcp_servers_and_groups: { servers?: string[]; accessGroups?: string[] } | null;
metadata: string;
}
export const fetchOrganizations = async (
accessToken: string,
setOrganizations: (organizations: Organization[]) => void,
@ -72,13 +106,28 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
setOrganizations,
premiumUser,
}) => {
void userModels;
void currentOrg;
void guardrailsList;
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
const [editOrg, setEditOrg] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [orgToDelete, setOrgToDelete] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
const [form] = Form.useForm();
const form = useForm<OrgFormValues>({
defaultValues: {
organization_alias: "",
models: [],
max_budget: null,
budget_duration: "",
tpm_limit: null,
rpm_limit: null,
allowed_vector_store_ids: [],
allowed_mcp_servers_and_groups: null,
metadata: "",
},
});
const [expandedAccordions, setExpandedAccordions] = useState<Record<string, boolean>>({});
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState<FilterState>({
@ -91,7 +140,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
const handleFilterChange = (key: keyof FilterState, value: string) => {
const newFilters = { ...filters, [key]: value };
setFilters(newFilters);
// Call organizationListCall with the new filters
if (accessToken) {
organizationListCall(accessToken, newFilters.org_id || null, newFilters.org_alias || null)
.then((response) => {
@ -112,7 +160,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
sort_by: "created_at",
sort_order: "desc",
});
// Reset organizations list
if (accessToken) {
organizationListCall(accessToken, null, null)
.then((response) => {
@ -128,7 +175,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
const handleDelete = (orgId: string | null) => {
if (!orgId) return;
setOrgToDelete(orgId);
setIsDeleteModalOpen(true);
};
@ -143,7 +189,6 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
setIsDeleteModalOpen(false);
setOrgToDelete(null);
// Refresh organizations list
await fetchOrganizations(accessToken, setOrganizations, filters.org_id || null, filters.org_alias || null);
} catch (error) {
console.error("Error deleting organization:", error);
@ -157,69 +202,68 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
setOrgToDelete(null);
};
const handleCreate = async (values: any) => {
const handleCreate = form.handleSubmit(async (values) => {
try {
if (!accessToken) return;
console.log(`values in organizations new create call: ${JSON.stringify(values)}`);
const payload: Record<string, any> = { ...values };
// Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission
if (
(values.allowed_vector_store_ids && values.allowed_vector_store_ids.length > 0) ||
(values.allowed_mcp_servers_and_groups &&
(values.allowed_mcp_servers_and_groups.servers?.length > 0 ||
values.allowed_mcp_servers_and_groups.accessGroups?.length > 0))
((values.allowed_mcp_servers_and_groups.servers?.length ?? 0) > 0 ||
(values.allowed_mcp_servers_and_groups.accessGroups?.length ?? 0) > 0))
) {
values.object_permission = {};
payload.object_permission = {};
if (values.allowed_vector_store_ids && values.allowed_vector_store_ids.length > 0) {
values.object_permission.vector_stores = values.allowed_vector_store_ids;
delete values.allowed_vector_store_ids;
payload.object_permission.vector_stores = values.allowed_vector_store_ids;
delete payload.allowed_vector_store_ids;
}
if (values.allowed_mcp_servers_and_groups) {
if (values.allowed_mcp_servers_and_groups.servers?.length > 0) {
values.object_permission.mcp_servers = values.allowed_mcp_servers_and_groups.servers;
if ((values.allowed_mcp_servers_and_groups.servers?.length ?? 0) > 0) {
payload.object_permission.mcp_servers = values.allowed_mcp_servers_and_groups.servers;
}
if (values.allowed_mcp_servers_and_groups.accessGroups?.length > 0) {
values.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups;
if ((values.allowed_mcp_servers_and_groups.accessGroups?.length ?? 0) > 0) {
payload.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups;
}
delete values.allowed_mcp_servers_and_groups;
delete payload.allowed_mcp_servers_and_groups;
}
}
await organizationCreateCall(accessToken, values);
await organizationCreateCall(accessToken, payload);
NotificationsManager.success("Organization created successfully");
setIsOrgModalVisible(false);
form.resetFields();
// Refresh organizations list
form.reset();
fetchOrganizations(accessToken, setOrganizations, filters.org_id || null, filters.org_alias || null);
} catch (error) {
console.error("Error creating organization:", error);
}
};
});
const handleCancel = () => {
setIsOrgModalVisible(false);
form.resetFields();
form.reset();
};
if (!premiumUser) {
return (
<div>
<Text>
<p className="text-sm">
This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer">
here
</a>
.
</Text>
</p>
</div>
);
}
return (
<TooltipProvider>
<div className="w-full mx-4 h-[75vh]">
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
<Col numColSpan={1} className="flex flex-col gap-2">
<div className="grid grid-cols-1 gap-2 p-8 w-full mt-2">
<div className="col-span-1 flex flex-col gap-2">
{(userRole === "Admin" || userRole === "Org Admin") && (
<Button className="w-fit" onClick={() => setIsOrgModalVisible(true)}>
+ Create New Organization
@ -233,324 +277,429 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
setEditOrg(false);
}}
accessToken={accessToken}
is_org_admin={true} // You'll need to implement proper org admin check
is_org_admin={true}
is_proxy_admin={userRole === "Admin"}
userModels={userModels}
editOrg={editOrg}
/>
) : (
<TabGroup className="gap-2 h-[75vh] w-full">
<TabList className="flex justify-between mt-2 w-full items-center">
<Tabs defaultValue="your-organizations" className="gap-2 h-[75vh] w-full">
<TabsList className="flex justify-between mt-2 w-full items-center">
<div className="flex">
<Tab>Your Organizations</Tab>
<TabsTrigger value="your-organizations">Your Organizations</TabsTrigger>
</div>
<div className="flex items-center space-x-2">
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
<Icon
icon={RefreshIcon}
variant="shadow"
size="xs"
{lastRefreshed && (
<span className="text-sm text-muted-foreground">
Last Refreshed: {lastRefreshed}
</span>
)}
<Button
variant="ghost"
size="icon"
className="self-center"
onClick={handleRefreshClick}
/>
aria-label="Refresh"
>
<RefreshIcon className="h-4 w-4" />
</Button>
</div>
</TabList>
<TabPanels>
<TabPanel>
<Text>Click on &ldquo;Organization ID&rdquo; to view organization details.</Text>
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
<Col numColSpan={1}>
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
<div className="border-b px-6 py-4">
<div className="flex flex-col space-y-4">
<OrganizationFilters
filters={filters}
showFilters={showFilters}
onToggleFilters={setShowFilters}
onChange={handleFilterChange}
onReset={handleFilterReset}
/>
</div>
</TabsList>
<TabsContent value="your-organizations">
<p className="text-sm">
Click on &ldquo;Organization ID&rdquo; to view organization details.
</p>
<div className="grid grid-cols-1 gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
<div className="col-span-1">
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
<div className="border-b border-border px-6 py-4">
<div className="flex flex-col space-y-4">
<OrganizationFilters
filters={filters}
showFilters={showFilters}
onToggleFilters={setShowFilters}
onChange={handleFilterChange}
onReset={handleFilterReset}
/>
</div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Organization ID</TableHeaderCell>
<TableHeaderCell>Organization Name</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Spend (USD)</TableHeaderCell>
<TableHeaderCell>Budget (USD)</TableHeaderCell>
<TableHeaderCell>Models</TableHeaderCell>
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
<TableHeaderCell>Info</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
</div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Organization ID</TableHeaderCell>
<TableHeaderCell>Organization Name</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Spend (USD)</TableHeaderCell>
<TableHeaderCell>Budget (USD)</TableHeaderCell>
<TableHeaderCell>Models</TableHeaderCell>
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
<TableHeaderCell>Info</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</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}>
<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>
<TooltipTrigger asChild>
<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]"
size="sm"
variant="ghost"
/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */
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] h-auto"
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>{formatNumberWithCommas(org.spend, 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
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>
) : (
<>
<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>
</TooltipTrigger>
<TooltipContent>{org.organization_id}</TooltipContent>
</Tooltip>
</div>
</TableCell>
<TableCell>{org.organization_alias}</TableCell>
<TableCell>
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
</TableCell>
<TableCell>{formatNumberWithCommas(org.spend, 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
style={{
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 variant="destructive" className="mb-1 w-fit">
All Proxy Models
</Badge>
) : (
<div className="flex items-start">
{org.models.length > 3 && (
<Button
variant="ghost"
size="icon"
className="h-5 w-5"
onClick={() => {
setExpandedAccordions((prev) => ({
...prev,
[org.organization_id || ""]:
!prev[org.organization_id || ""],
}));
}}
aria-label="Toggle models"
>
{expandedAccordions[org.organization_id || ""] ? (
<ChevronDownIcon className="h-3 w-3" />
) : (
<ChevronRightIcon className="h-3 w-3" />
)}
</Button>
)}
<div className="flex flex-wrap gap-1">
{org.models.slice(0, 3).map((model, index) =>
model === "all-proxy-models" ? (
<Badge key={index} variant="destructive">
All Proxy Models
</Badge>
) : (
<Badge key={index} variant="secondary">
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Badge>
),
)}
{org.models.length > 3 &&
!expandedAccordions[org.organization_id || ""] && (
<Badge
variant="outline"
className="cursor-pointer"
>
+{org.models.length - 3}{" "}
{org.models.length - 3 === 1 ? "more model" : "more models"}
</Badge>
)}
{expandedAccordions[org.organization_id || ""] && (
<div className="flex flex-wrap gap-1">
{org.models.slice(0, 3).map((model, index) =>
{org.models.slice(3).map((model, index) =>
model === "all-proxy-models" ? (
<Badge key={index} size={"xs"} color="red">
<Text>All Proxy Models</Text>
<Badge key={index + 3} variant="destructive">
All Proxy Models
</Badge>
) : (
<Badge key={index} size={"xs"} color="blue">
<Text>
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
<Badge key={index + 3} variant="secondary">
{model.length > 30
? `${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</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>
</>
)}
</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" && (
<>
<TableIconActionButton
variant="Edit"
tooltipText="Edit organization"
onClick={() => {
setSelectedOrgId(org.organization_id);
setEditOrg(true);
}}
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete organization"
onClick={() => handleDelete(org.organization_id)}
/>
</>
)}
</TableCell>
</TableRow>
))
: null}
</TableBody>
</Table>
</Card>
</Col>
</Grid>
</TabPanel>
</TabPanels>
</TabGroup>
)}
</div>
</div>
)}
</div>
) : null}
</div>
</TableCell>
<TableCell>
<span className="text-sm">
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"}
</span>
</TableCell>
<TableCell>
<span className="text-sm">{org.members?.length || 0} Members</span>
</TableCell>
<TableCell>
{userRole === "Admin" && (
<>
<TableIconActionButton
variant="Edit"
tooltipText="Edit organization"
onClick={() => {
setSelectedOrgId(org.organization_id);
setEditOrg(true);
}}
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete organization"
onClick={() => handleDelete(org.organization_id)}
/>
</>
)}
</TableCell>
</TableRow>
))
: null}
</TableBody>
</Table>
</Card>
</div>
</div>
</TabsContent>
</Tabs>
)}
</Col>
</Grid>
<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">
<ModelSelect
options={{ showAllProxyModelsOverride: true, includeSpecialOptions: true }}
value={form.getFieldValue("models")}
onChange={(values) => form.setFieldValue("models", values)}
context="organization"
/>
</Form.Item>
</div>
</div>
<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>
<Dialog
open={isOrgModalVisible}
onOpenChange={(o) => {
if (!o) handleCancel();
}}
>
<DialogContent className="max-w-[800px]">
<DialogHeader>
<DialogTitle>Create Organization</DialogTitle>
</DialogHeader>
<FormProvider {...form}>
<form onSubmit={handleCreate} className="space-y-4">
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
<Label htmlFor="org-alias" className="mt-2 text-left">
Organization Name <span className="text-destructive">*</span>
</Label>
<div className="space-y-1">
<Input
id="org-alias"
{...form.register("organization_alias", {
required: "Please input an organization name",
})}
/>
{form.formState.errors.organization_alias && (
<p className="text-sm text-destructive">
{form.formState.errors.organization_alias.message as string}
</p>
)}
</div>
</div>
<Form.Item
label={
<span>
Allowed Vector Stores{" "}
<Tooltip title="Select which vector stores this organization can access by default. Leave empty for access to all vector stores">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_vector_store_ids"
className="mt-4"
help="Select vector stores this organization can access. Leave empty for access to all vector stores"
>
<VectorStoreSelector
onChange={(values) => form.setFieldValue("allowed_vector_store_ids", values)}
value={form.getFieldValue("allowed_vector_store_ids")}
accessToken={accessToken || ""}
placeholder="Select vector stores (optional)"
/>
</Form.Item>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
<Label className="mt-2 text-left">Models</Label>
<Controller
control={form.control}
name="models"
render={({ field }) => (
<ModelSelect
options={{ showAllProxyModelsOverride: true, includeSpecialOptions: true }}
value={field.value}
onChange={(values) => field.onChange(values)}
context="organization"
/>
)}
/>
</div>
<Form.Item
label={
<span>
Allowed MCP Servers{" "}
<Tooltip title="Select which MCP servers and access groups this organization can access by default.">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_mcp_servers_and_groups"
className="mt-4"
help="Select MCP servers and access groups this organization can access."
>
<MCPServerSelector
onChange={(values) => form.setFieldValue("allowed_mcp_servers_and_groups", values)}
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers and access groups (optional)"
/>
</Form.Item>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
<Label className="text-left">Max Budget (USD)</Label>
<Controller
control={form.control}
name="max_budget"
render={({ field }) => (
<NumericalInput
step={0.01}
width={200}
value={field.value as any}
onChange={field.onChange}
/>
)}
/>
</div>
<Form.Item label="Metadata" name="metadata">
<Input.TextArea rows={4} />
</Form.Item>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
<Label className="text-left">Reset Budget</Label>
<Controller
control={form.control}
name="budget_duration"
render={({ field }) => (
<Select value={field.value || ""} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="n/a" />
</SelectTrigger>
<SelectContent>
<SelectItem value="24h">daily</SelectItem>
<SelectItem value="7d">weekly</SelectItem>
<SelectItem value="30d">monthly</SelectItem>
</SelectContent>
</Select>
)}
/>
</div>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="submit">Create Organization</Button>
</div>
</Form>
</Modal>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
<Label className="text-left">Tokens per minute Limit (TPM)</Label>
<Controller
control={form.control}
name="tpm_limit"
render={({ field }) => (
<NumericalInput
step={1}
width={400}
value={field.value as any}
onChange={field.onChange}
/>
)}
/>
</div>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-center">
<Label className="text-left">Requests per minute Limit (RPM)</Label>
<Controller
control={form.control}
name="rpm_limit"
render={({ field }) => (
<NumericalInput
step={1}
width={400}
value={field.value as any}
onChange={field.onChange}
/>
)}
/>
</div>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start mt-4">
<Label className="mt-2 text-left inline-flex items-center gap-1">
Allowed Vector Stores
<Tooltip>
<TooltipTrigger asChild>
<InfoCircleOutlined className="h-3.5 w-3.5 ml-1" />
</TooltipTrigger>
<TooltipContent>
Select which vector stores this organization can access by default. Leave
empty for access to all vector stores
</TooltipContent>
</Tooltip>
</Label>
<div className="space-y-1">
<Controller
control={form.control}
name="allowed_vector_store_ids"
render={({ field }) => (
<VectorStoreSelector
onChange={(values) => field.onChange(values)}
value={field.value}
accessToken={accessToken || ""}
placeholder="Select vector stores (optional)"
/>
)}
/>
<p className="text-xs text-muted-foreground">
Select vector stores this organization can access. Leave empty for access to all
vector stores
</p>
</div>
</div>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start mt-4">
<Label className="mt-2 text-left inline-flex items-center gap-1">
Allowed MCP Servers
<Tooltip>
<TooltipTrigger asChild>
<InfoCircleOutlined className="h-3.5 w-3.5 ml-1" />
</TooltipTrigger>
<TooltipContent>
Select which MCP servers and access groups this organization can access by
default.
</TooltipContent>
</Tooltip>
</Label>
<div className="space-y-1">
<Controller
control={form.control}
name="allowed_mcp_servers_and_groups"
render={({ field }) => (
<MCPServerSelector
onChange={(values) => field.onChange(values)}
value={field.value as any}
accessToken={accessToken || ""}
placeholder="Select MCP servers and access groups (optional)"
/>
)}
/>
<p className="text-xs text-muted-foreground">
Select MCP servers and access groups this organization can access.
</p>
</div>
</div>
<div className="grid grid-cols-[1fr_2fr] gap-4 items-start">
<Label className="mt-2 text-left">Metadata</Label>
<Textarea rows={4} {...form.register("metadata")} />
</div>
<div className="text-right mt-3">
<Button type="submit">Create Organization</Button>
</div>
</form>
</FormProvider>
</DialogContent>
</Dialog>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
@ -563,6 +712,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
confirmLoading={isDeleting}
/>
</div>
</TooltipProvider>
);
};