feat(ui/): add new customer dashboard to the ui

allows seeing all the end users/customers that have been created on litellm

visibility
This commit is contained in:
Krrish Dholakia 2026-02-18 19:47:37 -08:00
parent 561c412b9e
commit 7ef4753bb2
8 changed files with 677 additions and 170 deletions

View file

@ -1,14 +1,14 @@
import React, { useState } from "react";
import { Button, Card, Col, Grid, TabPanel, Text } from "@tremor/react";
import { Button, Card, TabPanel, Text } from "@tremor/react";
import { RefreshCw } from "lucide-react";
import CustomersTable from "@/app/(dashboard)/customers/components/CustomersTable";
import CustomerInfo from "@/app/(dashboard)/customers/components/CustomerInfo";
import CreateCustomerModal from "@/app/(dashboard)/customers/components/modals/CreateCustomerModal";
import CustomerInfoModal from "@/app/(dashboard)/customers/components/modals/CustomerInfoModal";
import DeleteCustomerModal from "@/app/(dashboard)/customers/components/modals/DeleteCustomerModal";
import CustomersHeaderTabs from "@/app/(dashboard)/customers/components/CustomersHeaderTabs";
import CustomersFilters from "@/app/(dashboard)/customers/components/CustomersFilters";
import type { Customer, NewCustomerData } from "@/app/(dashboard)/customers/types";
import { customerCreateCall, customerDeleteCall, customerUpdateCall } from "@/components/networking";
import { customerDeleteCall } from "@/components/networking";
interface CustomersViewProps {
customers: Customer[];
@ -43,53 +43,48 @@ const CustomersView: React.FC<CustomersViewProps> = ({
});
const [showCreateModal, setShowCreateModal] = useState(false);
const [showInfoModal, setShowInfoModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
const [selectedCustomerId, setSelectedCustomerId] = useState<string | null>(null);
const [customerDetailDefaultTab, setCustomerDetailDefaultTab] = useState<"overview" | "settings">("overview");
const [lastRefreshed, setLastRefreshed] = useState(new Date().toLocaleString("en-US"));
const handleCreateCustomer = async (data: NewCustomerData) => {
const handleCreateCustomer = async () => {
if (!accessToken) return;
try {
const response = await customerCreateCall(accessToken, data);
if (response) {
// Refresh the customer list
const { allEndUsersCall } = await import("@/components/networking");
const listData = await allEndUsersCall(accessToken);
if (listData) {
setCustomers(Array.isArray(listData) ? listData : []);
}
const { allEndUsersCall } = await import("@/components/networking");
const listData = await allEndUsersCall(accessToken);
if (listData) {
setCustomers(Array.isArray(listData) ? listData : []);
}
setShowCreateModal(false);
} catch (error) {
console.error("Error creating customer:", error);
console.error("Error refreshing customers:", error);
}
};
const handleEditCustomer = (customer: Customer) => {
setSelectedCustomerId(customer.user_id);
setSelectedCustomer(customer);
setShowInfoModal(true);
setCustomerDetailDefaultTab("settings");
};
const handleViewInfo = (customer: Customer) => {
setSelectedCustomerId(customer.user_id);
setSelectedCustomer(customer);
setShowInfoModal(true);
setCustomerDetailDefaultTab("overview");
};
const handleSaveCustomer = async (updated: Customer) => {
if (!accessToken) return;
const handleCloseCustomerInfo = () => {
setSelectedCustomerId(null);
setSelectedCustomer(null);
};
try {
await customerUpdateCall(accessToken, updated);
setCustomers(
customers.map((c) => (c.user_id === updated.user_id ? updated : c))
);
setShowInfoModal(false);
setSelectedCustomer(null);
} catch (error) {
console.error("Error updating customer:", error);
}
const handleUpdateCustomer = (updated: Customer) => {
setCustomers((prev) =>
prev.map((c) => (c.user_id === updated.user_id ? updated : c))
);
setSelectedCustomer(updated);
};
const handleDeleteClick = (customer: Customer) => {
@ -142,46 +137,67 @@ const CustomersView: React.FC<CustomersViewProps> = ({
return matchesUserId && matchesAlias && matchesBlocked && matchesRegion;
});
return (
<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">
{(userRole === "Admin" || userRole === "Org Admin") && (
<Button className="w-fit" onClick={() => setShowCreateModal(true)}>
+ Create New Customer
</Button>
)}
const initialCustomerForDetail =
selectedCustomerId && selectedCustomer?.user_id === selectedCustomerId
? selectedCustomer
: customers.find((c) => c.user_id === selectedCustomerId) ?? null;
<CustomersHeaderTabs lastRefreshed={lastRefreshed} onRefresh={handleRefresh} userRole={userRole}>
<TabPanel>
<Text>
Click on &ldquo;Customer ID&rdquo; to view customer details and manage settings.
</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">
<CustomersFilters
filters={filters}
showFilters={showFilters}
onToggleFilters={setShowFilters}
onChange={handleFilterChange}
onReset={handleFilterReset}
/>
</div>
<CustomersTable
customers={filteredCustomers}
userRole={userRole}
onEdit={handleEditCustomer}
onDelete={handleDeleteClick}
onViewInfo={handleViewInfo}
isLoading={isLoading}
/>
</Card>
</Col>
</Grid>
</TabPanel>
</CustomersHeaderTabs>
if (selectedCustomerId) {
return (
<div className="w-full max-w-full px-4 py-4 md:px-6">
<CustomerInfo
customerId={selectedCustomerId}
initialCustomer={initialCustomerForDetail}
onClose={handleCloseCustomerInfo}
onUpdate={handleUpdateCustomer}
accessToken={accessToken}
userRole={userRole}
defaultTab={customerDetailDefaultTab}
/>
</div>
);
}
return (
<div className="w-full max-w-full px-4 py-4 md:px-6">
<div className="flex flex-col gap-4">
{(userRole === "Admin" || userRole === "Org Admin") && (
<Button className="w-fit" onClick={() => setShowCreateModal(true)}>
+ Create New Customer
</Button>
)}
<Text className="text-sm text-gray-500">
Customers are end-users of an AI application (e.g. users of your internal chat UI).
</Text>
<CustomersHeaderTabs lastRefreshed={lastRefreshed} onRefresh={handleRefresh} userRole={userRole}>
<TabPanel>
<Text className="block mb-3">
Click on &ldquo;Customer ID&rdquo; to view customer details and manage settings.
</Text>
<Card className="w-full overflow-hidden flex flex-col min-h-[400px]">
<div className="border-b px-4 sm:px-6 py-4 shrink-0">
<CustomersFilters
filters={filters}
showFilters={showFilters}
onToggleFilters={setShowFilters}
onChange={(key, value) => handleFilterChange(key as keyof FilterState, value)}
onReset={handleFilterReset}
/>
</div>
<div className="overflow-auto flex-1 min-h-0">
<CustomersTable
customers={filteredCustomers}
userRole={userRole}
onEdit={handleEditCustomer}
onDelete={handleDeleteClick}
onViewInfo={handleViewInfo}
isLoading={isLoading}
/>
</div>
</Card>
</TabPanel>
</CustomersHeaderTabs>
{(userRole === "Admin" || userRole === "Org Admin") && (
<>
@ -190,15 +206,6 @@ const CustomersView: React.FC<CustomersViewProps> = ({
onClose={() => setShowCreateModal(false)}
onCreate={handleCreateCustomer}
/>
<CustomerInfoModal
isOpen={showInfoModal}
onClose={() => {
setShowInfoModal(false);
setSelectedCustomer(null);
}}
customer={selectedCustomer}
onSave={handleSaveCustomer}
/>
<DeleteCustomerModal
isOpen={showDeleteModal}
onClose={() => {
@ -211,8 +218,7 @@ const CustomersView: React.FC<CustomersViewProps> = ({
/>
</>
)}
</Col>
</Grid>
</div>
</div>
);
};

View file

@ -0,0 +1,91 @@
import React from "react";
import { Form, Input, InputNumber, Select as AntSelect, Switch } from "antd";
import type { FormInstance } from "antd";
interface CustomerFormFieldsProps {
form: FormInstance;
mode: "create" | "edit";
disabledFields?: string[];
}
const defaultModelOptions = [
{ value: "", label: "None" },
{ value: "gpt-4o", label: "gpt-4o" },
{ value: "gpt-4o-mini", label: "gpt-4o-mini" },
{ value: "gpt-4-turbo", label: "gpt-4-turbo" },
{ value: "claude-3-sonnet", label: "claude-3-sonnet" },
{ value: "claude-3-opus", label: "claude-3-opus" },
{ value: "claude-3-haiku", label: "claude-3-haiku" },
];
const regionOptions = [
{ value: "", label: "Any region" },
{ value: "us", label: "US" },
{ value: "eu", label: "EU" },
];
const CustomerFormFields: React.FC<CustomerFormFieldsProps> = ({
form,
mode,
disabledFields = [],
}) => {
const isDisabled = (field: string) => disabledFields.includes(field);
return (
<>
<Form.Item label="Alias" name="alias">
<Input placeholder={mode === "create" ? "e.g. Acme Corp" : "Customer alias"} />
</Form.Item>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="Max Budget" name="max_budget">
<InputNumber
className="w-full"
placeholder={mode === "create" ? "e.g. 500" : "No limit"}
min={0}
disabled={isDisabled("max_budget")}
/>
</Form.Item>
<Form.Item label="Budget ID" name="budget_id">
<Input placeholder="e.g. free_tier" />
</Form.Item>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="Default Model" name="default_model">
<AntSelect
placeholder={mode === "create" ? "Select model..." : "None"}
allowClear={mode === "edit"}
options={defaultModelOptions}
/>
</Form.Item>
<Form.Item label="Allowed Region" name="allowed_model_region">
<AntSelect
placeholder="Any region"
allowClear={mode === "edit"}
options={regionOptions}
/>
</Form.Item>
</div>
<Form.Item label="Budget Duration" name="budget_duration">
<Input placeholder={mode === "create" ? "e.g. 30d, 24h, 60m" : "e.g. 30d, 24h"} />
</Form.Item>
{mode === "edit" && (
<Form.Item label="Blocked" name="blocked" valuePropName="checked">
<div className="flex items-center gap-3">
<Switch />
<span className="text-sm text-gray-500">
{form.getFieldValue("blocked")
? "This customer is currently blocked from making requests"
: "This customer can make requests normally"}
</span>
</div>
</Form.Item>
)}
</>
);
};
export default CustomerFormFields;

View file

@ -0,0 +1,326 @@
import React, { useEffect, useState } from "react";
import { Button, Form, Input, Tabs } from "antd";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Badge, Card, Grid, Text, Title } from "@tremor/react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { customerUpdateCall } from "@/components/networking";
import type { Customer } from "@/app/(dashboard)/customers/types";
import CustomerFormFields from "@/app/(dashboard)/customers/components/CustomerFormFields";
import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector";
import AgentSelector from "@/components/agent_management/AgentSelector";
import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";
import ObjectPermissionsView from "@/components/object_permissions_view";
export interface CustomerInfoProps {
customerId: string;
initialCustomer: Customer | null;
onClose: () => void;
onUpdate: (customer: Customer) => void;
accessToken: string | null;
userRole: string | null;
defaultTab?: "overview" | "settings";
}
const CustomerInfo: React.FC<CustomerInfoProps> = ({
customerId,
initialCustomer,
onClose,
onUpdate,
accessToken,
userRole,
defaultTab = "overview",
}) => {
const [customer, setCustomer] = useState<Customer | null>(initialCustomer);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
const [activeTab, setActiveTab] = useState(defaultTab);
useEffect(() => {
setCustomer(initialCustomer);
}, [initialCustomer]);
useEffect(() => {
if (customer) {
const op = customer.object_permission;
form.setFieldsValue({
...customer,
max_budget: customer.litellm_budget_table?.max_budget,
budget_duration: customer.litellm_budget_table?.budget_duration,
allowed_mcp_servers_and_groups: {
servers: op?.mcp_servers ?? [],
accessGroups: op?.mcp_access_groups ?? [],
},
allowed_agents_and_groups: {
agents: op?.agents ?? [],
accessGroups: op?.agent_access_groups ?? [],
},
mcp_tool_permissions: op?.mcp_tool_permissions ?? {},
});
}
}, [customer, form]);
const copyToClipboard = async () => {
if (!customerId) return;
const success = await utilCopyToClipboard(customerId);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const buildObjectPermission = (values: Record<string, any>) => {
const objPerm: {
mcp_servers?: string[];
mcp_access_groups?: string[];
mcp_tool_permissions?: Record<string, string[]>;
agents?: string[];
agent_access_groups?: string[];
} = {};
const mcp = values.allowed_mcp_servers_and_groups;
if (mcp && (mcp.servers?.length > 0 || mcp.accessGroups?.length > 0)) {
if (mcp.servers?.length) objPerm.mcp_servers = mcp.servers;
if (mcp.accessGroups?.length) objPerm.mcp_access_groups = mcp.accessGroups;
}
if (values.mcp_tool_permissions && Object.keys(values.mcp_tool_permissions).length > 0) {
objPerm.mcp_tool_permissions = values.mcp_tool_permissions;
}
const agents = values.allowed_agents_and_groups;
if (agents && (agents.agents?.length > 0 || agents.accessGroups?.length > 0)) {
if (agents.agents?.length) objPerm.agents = agents.agents;
if (agents.accessGroups?.length) objPerm.agent_access_groups = agents.accessGroups;
}
return Object.keys(objPerm).length > 0 ? objPerm : undefined;
};
const handleSave = async () => {
if (!customer || !accessToken) return;
try {
const values = await form.validateFields();
setLoading(true);
const {
allowed_mcp_servers_and_groups,
allowed_agents_and_groups,
mcp_tool_permissions,
...rest
} = values;
const object_permission = buildObjectPermission(values);
const updated: Customer = {
...customer,
...rest,
...(object_permission ? { object_permission } : {}),
};
await customerUpdateCall(accessToken, updated);
setCustomer(updated);
onUpdate(updated);
} catch (error) {
console.error("Validation failed:", error);
} finally {
setLoading(false);
}
};
const canEdit = userRole === "Admin" || userRole === "Org Admin";
if (!customer && !initialCustomer) {
return (
<div className="p-4">
<Button type="text" icon={<ArrowLeftIcon className="h-4 w-4" />} onClick={onClose} className="mb-4">
Back to Customers
</Button>
<Text>Customer not found.</Text>
</div>
);
}
const c = customer ?? initialCustomer!;
const displayName = c.alias || c.user_id;
return (
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<Button
type="text"
icon={<ArrowLeftIcon className="h-4 w-4" />}
onClick={onClose}
className="mb-4"
>
Back to Customers
</Button>
<Title>{displayName}</Title>
<div className="flex items-center gap-2">
<Text className="text-gray-500 font-mono text-sm">{c.user_id}</Text>
<Button
type="text"
size="small"
icon={copied ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={copyToClipboard}
className={copied ? "text-green-600" : "text-gray-500 hover:text-gray-700"}
/>
</div>
</div>
</div>
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as "overview" | "settings")}
className="mb-4"
items={[
{
key: "overview",
label: "Overview",
children: (
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Spend (USD)</Text>
<div className="mt-2">
<Title>${formatNumberWithCommas(c.spend, 4)}</Title>
</div>
</Card>
<Card>
<Text>Budget (USD)</Text>
<div className="mt-2">
<Text>
{c.litellm_budget_table?.max_budget != null
? `$${formatNumberWithCommas(c.litellm_budget_table.max_budget, 4)}`
: "No limit"}
</Text>
{c.litellm_budget_table?.budget_duration && (
<Text className="text-gray-500 block">Duration: {c.litellm_budget_table.budget_duration}</Text>
)}
</div>
</Card>
<Card>
<Text>Default Model</Text>
<div className="mt-2">
{c.default_model ? (
<Badge color="gray">{c.default_model}</Badge>
) : (
<Text className="text-gray-500"></Text>
)}
</div>
</Card>
<Card>
<Text>Region</Text>
<div className="mt-2">
<Text>{c.allowed_model_region ? c.allowed_model_region.toUpperCase() : "Any"}</Text>
</div>
</Card>
<Card>
<Text>Status</Text>
<div className="mt-2">
{c.blocked ? (
<Badge color="red">Blocked</Badge>
) : (
<Badge color="green">Active</Badge>
)}
</div>
</Card>
<ObjectPermissionsView
objectPermission={{
object_permission_id: "",
vector_stores: [],
mcp_servers: c.object_permission?.mcp_servers ?? [],
mcp_access_groups: c.object_permission?.mcp_access_groups ?? [],
mcp_tool_permissions: c.object_permission?.mcp_tool_permissions ?? {},
agents: c.object_permission?.agents ?? [],
agent_access_groups: c.object_permission?.agent_access_groups ?? [],
}}
variant="card"
accessToken={accessToken}
/>
</Grid>
),
},
...(canEdit
? [
{
key: "settings",
label: "Settings",
children: (
<Card className="overflow-y-auto max-h-[65vh]">
<div className="flex justify-between items-center mb-4">
<Title>Customer Settings</Title>
</div>
<Form form={form} layout="vertical" onFinish={handleSave}>
<Form.Item label="Customer ID">
<Input value={c.user_id} disabled className="font-mono bg-gray-50" />
</Form.Item>
<Form.Item label="Spend (USD)">
<Input value={c.spend.toFixed(4)} disabled className="bg-gray-50" />
</Form.Item>
<CustomerFormFields form={form} mode="edit" />
<div className="pt-6 mt-6 border-t border-gray-200">
<Text className="font-semibold text-gray-900 block mb-3">MCP Servers / Access Groups</Text>
<Form.Item
name="allowed_mcp_servers_and_groups"
help="Select MCP servers or access groups this customer can access"
>
<MCPServerSelector
onChange={(val: any) => form.setFieldValue("allowed_mcp_servers_and_groups", val)}
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers or access groups (optional)"
/>
</Form.Item>
<Form.Item name="mcp_tool_permissions" initialValue={{}} hidden>
<Input type="hidden" />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups ||
prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
}
>
{() => (
<div className="mb-6">
<MCPToolPermissions
accessToken={accessToken || ""}
selectedServers={form.getFieldValue("allowed_mcp_servers_and_groups")?.servers || []}
toolPermissions={form.getFieldValue("mcp_tool_permissions") || {}}
onChange={(toolPerms) => form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
/>
</div>
)}
</Form.Item>
</div>
<div className="pt-6 mt-6 border-t border-gray-200">
<Text className="font-semibold text-gray-900 block mb-3">Agents / Access Groups</Text>
<Form.Item
name="allowed_agents_and_groups"
help="Select agents or access groups this customer can access"
>
<AgentSelector
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
value={form.getFieldValue("allowed_agents_and_groups")}
accessToken={accessToken || ""}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
</div>
<Form.Item className="mt-6 mb-0">
<Button type="primary" htmlType="submit" loading={loading}>
Save Changes
</Button>
</Form.Item>
</Form>
</Card>
),
},
]
: []),
]}
/>
</div>
);
};
export default CustomerInfo;

View file

@ -16,18 +16,15 @@ const CustomersHeaderTabs: React.FC<CustomersHeaderTabsProps> = ({
children,
}) => {
return (
<div>
<div className="flex items-center justify-between mb-2">
<TabGroup>
<TabList className="mt-2">
<Tab>Your Customers</Tab>
{(userRole === "Admin" || userRole === "Org Admin") && (
<Tab>Customer Settings</Tab>
)}
</TabList>
<TabPanels>{children}</TabPanels>
</TabGroup>
<div className="flex items-center gap-2 text-xs text-gray-500">
<TabGroup className="w-full">
<div className="flex items-center justify-between gap-4 mb-2">
<TabList className="mt-2">
<Tab>Your Customers</Tab>
{(userRole === "Admin" || userRole === "Org Admin") && (
<Tab>Customer Settings</Tab>
)}
</TabList>
<div className="flex items-center gap-2 text-xs text-gray-500 shrink-0">
<Text>Last Refreshed: {lastRefreshed}</Text>
<button
onClick={onRefresh}
@ -38,7 +35,8 @@ const CustomersHeaderTabs: React.FC<CustomersHeaderTabsProps> = ({
</button>
</div>
</div>
</div>
<TabPanels>{children}</TabPanels>
</TabGroup>
);
};

View file

@ -1,11 +1,20 @@
import React, { useState } from "react";
import { Modal, Form, Input, InputNumber, Select as AntSelect } from "antd";
import React, { useEffect, useState } from "react";
import { Modal, Form, Input } from "antd";
import { Accordion, AccordionBody, AccordionHeader } from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Tooltip } from "antd";
import type { NewCustomerData } from "@/app/(dashboard)/customers/types";
import CustomerFormFields from "@/app/(dashboard)/customers/components/CustomerFormFields";
import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector";
import AgentSelector from "@/components/agent_management/AgentSelector";
import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { customerCreateCall, fetchMCPAccessGroups } from "@/components/networking";
interface CreateCustomerModalProps {
isOpen: boolean;
onClose: () => void;
onCreate: (customer: NewCustomerData) => void;
onCreate: (customer?: NewCustomerData) => void;
}
const CreateCustomerModal: React.FC<CreateCustomerModalProps> = ({
@ -13,15 +22,59 @@ const CreateCustomerModal: React.FC<CreateCustomerModalProps> = ({
onClose,
onCreate,
}) => {
const { accessToken } = useAuthorized();
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadMCPAccessGroups = async () => {
if (!accessToken) return;
try {
await fetchMCPAccessGroups(accessToken);
} catch (error) {
console.error("Failed to fetch MCP access groups:", error);
}
};
if (isOpen) loadMCPAccessGroups();
}, [accessToken, isOpen]);
const buildObjectPermission = (values: Record<string, any>) => {
const objPerm: {
mcp_servers?: string[];
mcp_access_groups?: string[];
mcp_tool_permissions?: Record<string, string[]>;
agents?: string[];
agent_access_groups?: string[];
} = {};
const mcp = values.allowed_mcp_servers_and_groups;
if (mcp && (mcp.servers?.length > 0 || mcp.accessGroups?.length > 0)) {
if (mcp.servers?.length) objPerm.mcp_servers = mcp.servers;
if (mcp.accessGroups?.length) objPerm.mcp_access_groups = mcp.accessGroups;
}
if (values.mcp_tool_permissions && Object.keys(values.mcp_tool_permissions).length > 0) {
objPerm.mcp_tool_permissions = values.mcp_tool_permissions;
}
const agents = values.allowed_agents_and_groups;
if (agents && (agents.agents?.length > 0 || agents.accessGroups?.length > 0)) {
if (agents.agents?.length) objPerm.agents = agents.agents;
if (agents.accessGroups?.length) objPerm.agent_access_groups = agents.accessGroups;
}
return Object.keys(objPerm).length > 0 ? objPerm : undefined;
};
const handleOk = async () => {
if (!accessToken) return;
try {
const values = await form.validateFields();
setLoading(true);
onCreate(values);
const { allowed_mcp_servers_and_groups, allowed_agents_and_groups, mcp_tool_permissions, ...rest } = values;
const payload: any = { ...rest };
const object_permission = buildObjectPermission(values);
if (object_permission) payload.object_permission = object_permission;
await customerCreateCall(accessToken, payload);
form.resetFields();
onCreate();
onClose();
} catch (error) {
console.error("Validation failed:", error);
} finally {
@ -44,11 +97,7 @@ const CreateCustomerModal: React.FC<CreateCustomerModalProps> = ({
okText="Create Customer"
width={600}
>
<Form
form={form}
layout="vertical"
className="mt-4"
>
<Form form={form} layout="vertical" className="mt-4">
<Form.Item
label="User ID"
name="user_id"
@ -57,78 +106,90 @@ const CreateCustomerModal: React.FC<CreateCustomerModalProps> = ({
<Input placeholder="e.g. customer-007" />
</Form.Item>
<Form.Item
label="Alias"
name="alias"
>
<Input placeholder="e.g. Acme Corp" />
<CustomerFormFields form={form} mode="create" />
<Form.Item label="Metadata (JSON)" name="metadata" initialValue="{}">
<Input.TextArea rows={3} className="font-mono" placeholder="{}" />
</Form.Item>
<div className="grid grid-cols-2 gap-4">
<Form.Item
label="Max Budget"
name="max_budget"
>
<InputNumber
className="w-full"
placeholder="e.g. 500"
min={0}
/>
</Form.Item>
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<b>MCP Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item
label={
<span>
Allowed MCP Servers{" "}
<Tooltip title="Select which MCP servers or access groups this customer can access">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_mcp_servers_and_groups"
initialValue={{ servers: [], accessGroups: [] }}
className="mt-4"
help="Select MCP servers or access groups this customer can access"
>
<MCPServerSelector
onChange={(val: any) => form.setFieldValue("allowed_mcp_servers_and_groups", val)}
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers or access groups (optional)"
/>
</Form.Item>
<Form.Item name="mcp_tool_permissions" initialValue={{}} hidden>
<Input type="hidden" />
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups ||
prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
}
>
{() => (
<div className="mt-6">
<MCPToolPermissions
accessToken={accessToken || ""}
selectedServers={form.getFieldValue("allowed_mcp_servers_and_groups")?.servers || []}
toolPermissions={form.getFieldValue("mcp_tool_permissions") || {}}
onChange={(toolPerms) => form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
/>
</div>
)}
</Form.Item>
</AccordionBody>
</Accordion>
<Form.Item
label="Budget ID"
name="budget_id"
>
<Input placeholder="e.g. free_tier" />
</Form.Item>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Item
label="Default Model"
name="default_model"
>
<AntSelect placeholder="Select model...">
<AntSelect.Option value="gpt-4o">gpt-4o</AntSelect.Option>
<AntSelect.Option value="gpt-4o-mini">gpt-4o-mini</AntSelect.Option>
<AntSelect.Option value="gpt-4-turbo">gpt-4-turbo</AntSelect.Option>
<AntSelect.Option value="claude-3-sonnet">claude-3-sonnet</AntSelect.Option>
<AntSelect.Option value="claude-3-opus">claude-3-opus</AntSelect.Option>
<AntSelect.Option value="claude-3-haiku">claude-3-haiku</AntSelect.Option>
</AntSelect>
</Form.Item>
<Form.Item
label="Allowed Region"
name="allowed_model_region"
>
<AntSelect placeholder="Any region">
<AntSelect.Option value="">Any region</AntSelect.Option>
<AntSelect.Option value="us">US</AntSelect.Option>
<AntSelect.Option value="eu">EU</AntSelect.Option>
</AntSelect>
</Form.Item>
</div>
<Form.Item
label="Budget Duration"
name="budget_duration"
>
<Input placeholder="e.g. 30d, 24h, 60m" />
</Form.Item>
<Form.Item
label="Metadata (JSON)"
name="metadata"
initialValue="{}"
>
<Input.TextArea
rows={3}
className="font-mono"
placeholder="{}"
/>
</Form.Item>
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<b>Agent Settings</b>
</AccordionHeader>
<AccordionBody>
<Form.Item
label={
<span>
Allowed Agents{" "}
<Tooltip title="Select which agents or access groups this customer can access">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_agents_and_groups"
initialValue={{ agents: [], accessGroups: [] }}
className="mt-4"
help="Select agents or access groups this customer can access"
>
<AgentSelector
onChange={(val: any) => form.setFieldValue("allowed_agents_and_groups", val)}
value={form.getFieldValue("allowed_agents_and_groups")}
accessToken={accessToken || ""}
placeholder="Select agents or access groups (optional)"
/>
</Form.Item>
</AccordionBody>
</Accordion>
</Form>
</Modal>
);

View file

@ -1,3 +1,11 @@
export interface CustomerObjectPermission {
mcp_servers?: string[];
mcp_access_groups?: string[];
mcp_tool_permissions?: Record<string, string[]>;
agents?: string[];
agent_access_groups?: string[];
}
export interface Customer {
user_id: string;
alias: string | null;
@ -12,6 +20,7 @@ export interface Customer {
max_budget: number | null;
budget_duration: string | null;
} | null;
object_permission?: CustomerObjectPermission | null;
}
export interface NewCustomerData {
@ -23,4 +32,5 @@ export interface NewCustomerData {
allowed_model_region?: string;
budget_duration?: string;
metadata?: string;
object_permission?: CustomerObjectPermission;
}

View file

@ -4,6 +4,7 @@ import APIReferenceView from "@/app/(dashboard)/api-reference/APIReferenceView";
import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider";
import OldModelDashboard from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView";
import PlaygroundPage from "@/app/(dashboard)/playground/page";
import CustomersPage from "@/app/(dashboard)/customers/page";
import AdminPanel from "@/components/AdminPanel";
import AgentsPanel from "@/components/agents";
import BudgetPanel from "@/components/budgets/budget_panel";
@ -459,6 +460,8 @@ function CreateKeyPageContent() {
premiumUser={premiumUser}
searchParams={searchParams}
/>
) : page == "customers" ? (
<CustomersPage />
) : page == "organizations" ? (
<Organizations
organizations={organizations}

View file

@ -9,6 +9,7 @@ import {
BgColorsOutlined,
BlockOutlined,
BookOutlined,
ContactsOutlined,
CreditCardOutlined,
DatabaseOutlined,
ExperimentOutlined,
@ -172,6 +173,17 @@ const menuGroups: MenuGroup[] = [
label: "Teams",
icon: <TeamOutlined />,
},
{
key: "customers",
page: "customers",
label: (
<span className="flex items-center gap-2">
Customers <NewBadge />
</span>
),
icon: <ContactsOutlined />,
roles: all_admin_roles,
},
{
key: "organizations",
page: "organizations",