feat: add customer dashboard to UI

Add a comprehensive customer management dashboard following the Teams page pattern.

Features:
- Customer list view with filtering and search
- Create new customers with budget, model, and region settings
- Edit customer details including blocking/unblocking
- Delete customers with confirmation
- View customer spend and budget information
- Filter by status (active/blocked) and region (US/EU)

Components:
- CustomersView: Main view component with state management
- CustomersTable: Table component displaying customer data
- CustomersFilters: Search and filter controls
- CustomersHeaderTabs: Tab navigation with refresh
- Modals: Create, Edit, and Delete customer modals
- useFetchCustomers: Custom hook for data fetching

API Integration:
- Added customerCreateCall, customerUpdateCall, customerDeleteCall
- Added Customer interface type definition
- Integrated with existing allEndUsersCall

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Krrish Dholakia 2026-02-18 18:20:36 -08:00
parent 936e04e0e1
commit 561c412b9e
11 changed files with 1063 additions and 0 deletions

View file

@ -0,0 +1,220 @@
import React, { useState } from "react";
import { Button, Card, Col, Grid, TabPanel, Text } from "@tremor/react";
import { RefreshCw } from "lucide-react";
import CustomersTable from "@/app/(dashboard)/customers/components/CustomersTable";
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";
interface CustomersViewProps {
customers: Customer[];
setCustomers: React.Dispatch<React.SetStateAction<Customer[]>>;
accessToken: string | null;
userID: string | null;
userRole: string | null;
isLoading: boolean;
}
interface FilterState {
user_id: string;
alias: string;
blocked: string;
region: string;
}
const CustomersView: React.FC<CustomersViewProps> = ({
customers,
setCustomers,
accessToken,
userID,
userRole,
isLoading,
}) => {
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState<FilterState>({
user_id: "",
alias: "",
blocked: "",
region: "",
});
const [showCreateModal, setShowCreateModal] = useState(false);
const [showInfoModal, setShowInfoModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
const [lastRefreshed, setLastRefreshed] = useState(new Date().toLocaleString("en-US"));
const handleCreateCustomer = async (data: NewCustomerData) => {
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 : []);
}
}
setShowCreateModal(false);
} catch (error) {
console.error("Error creating customer:", error);
}
};
const handleEditCustomer = (customer: Customer) => {
setSelectedCustomer(customer);
setShowInfoModal(true);
};
const handleViewInfo = (customer: Customer) => {
setSelectedCustomer(customer);
setShowInfoModal(true);
};
const handleSaveCustomer = async (updated: Customer) => {
if (!accessToken) return;
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 handleDeleteClick = (customer: Customer) => {
setSelectedCustomer(customer);
setShowDeleteModal(true);
};
const handleConfirmDelete = async () => {
if (!selectedCustomer || !accessToken) return;
try {
await customerDeleteCall(accessToken, selectedCustomer.user_id);
setCustomers(
customers.filter((c) => c.user_id !== selectedCustomer.user_id)
);
setShowDeleteModal(false);
setSelectedCustomer(null);
} catch (error) {
console.error("Error deleting customer:", error);
}
};
const handleRefresh = () => {
setLastRefreshed(new Date().toLocaleString("en-US"));
// Trigger a re-fetch if needed
};
const handleFilterChange = (key: keyof FilterState, value: string) => {
setFilters({ ...filters, [key]: value });
};
const handleFilterReset = () => {
setFilters({
user_id: "",
alias: "",
blocked: "",
region: "",
});
};
const filteredCustomers = customers.filter((c) => {
const matchesUserId = !filters.user_id || c.user_id.toLowerCase().includes(filters.user_id.toLowerCase());
const matchesAlias = !filters.alias || (c.alias && c.alias.toLowerCase().includes(filters.alias.toLowerCase()));
const matchesBlocked =
!filters.blocked ||
(filters.blocked === "active" && !c.blocked) ||
(filters.blocked === "blocked" && c.blocked);
const matchesRegion = !filters.region || c.allowed_model_region === filters.region;
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>
)}
<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>
{(userRole === "Admin" || userRole === "Org Admin") && (
<>
<CreateCustomerModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onCreate={handleCreateCustomer}
/>
<CustomerInfoModal
isOpen={showInfoModal}
onClose={() => {
setShowInfoModal(false);
setSelectedCustomer(null);
}}
customer={selectedCustomer}
onSave={handleSaveCustomer}
/>
<DeleteCustomerModal
isOpen={showDeleteModal}
onClose={() => {
setShowDeleteModal(false);
setSelectedCustomer(null);
}}
onConfirm={handleConfirmDelete}
customerName={selectedCustomer?.alias || ""}
customerId={selectedCustomer?.user_id || ""}
/>
</>
)}
</Col>
</Grid>
</div>
);
};
export default CustomersView;

View file

@ -0,0 +1,88 @@
import React from "react";
import { Button, TextInput } from "@tremor/react";
import { Filter, RotateCcw, Search } from "lucide-react";
import { Select } from "antd";
interface CustomersFiltersProps {
filters: {
user_id: string;
alias: string;
blocked: string;
region: string;
};
showFilters: boolean;
onToggleFilters: (show: boolean) => void;
onChange: (key: string, value: string) => void;
onReset: () => void;
}
const CustomersFilters: React.FC<CustomersFiltersProps> = ({
filters,
showFilters,
onToggleFilters,
onChange,
onReset,
}) => {
return (
<div className="flex flex-col space-y-4">
<div className="flex items-center gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<TextInput
placeholder="Search by Customer ID or Name..."
value={filters.user_id || filters.alias}
onChange={(e) => onChange("user_id", e.target.value)}
className="pl-10"
/>
</div>
<Button
variant="secondary"
onClick={() => onToggleFilters(!showFilters)}
icon={Filter}
>
Filters
</Button>
<Button
variant="secondary"
onClick={onReset}
icon={RotateCcw}
>
Reset
</Button>
</div>
{showFilters && (
<div className="flex items-center gap-4 p-3 bg-gray-50 rounded-md border border-gray-200">
<div className="flex items-center gap-2">
<label className="text-xs font-medium text-gray-600">Status:</label>
<Select
value={filters.blocked || "all"}
onChange={(value) => onChange("blocked", value)}
style={{ width: 120 }}
size="small"
>
<Select.Option value="">All</Select.Option>
<Select.Option value="active">Active</Select.Option>
<Select.Option value="blocked">Blocked</Select.Option>
</Select>
</div>
<div className="flex items-center gap-2">
<label className="text-xs font-medium text-gray-600">Region:</label>
<Select
value={filters.region || "all"}
onChange={(value) => onChange("region", value)}
style={{ width: 120 }}
size="small"
>
<Select.Option value="">All</Select.Option>
<Select.Option value="us">US</Select.Option>
<Select.Option value="eu">EU</Select.Option>
</Select>
</div>
</div>
)}
</div>
);
};
export default CustomersFilters;

View file

@ -0,0 +1,45 @@
import React from "react";
import { Tab, TabGroup, TabList, TabPanels, Text } from "@tremor/react";
import { RefreshCw } from "lucide-react";
interface CustomersHeaderTabsProps {
lastRefreshed: string;
onRefresh: () => void;
userRole: string | null;
children: React.ReactNode;
}
const CustomersHeaderTabs: React.FC<CustomersHeaderTabsProps> = ({
lastRefreshed,
onRefresh,
userRole,
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">
<Text>Last Refreshed: {lastRefreshed}</Text>
<button
onClick={onRefresh}
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
aria-label="Refresh"
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
};
export default CustomersHeaderTabs;

View file

@ -0,0 +1,142 @@
import React from "react";
import {
Button,
Icon,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Badge,
Text,
} from "@tremor/react";
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import type { Customer } from "@/app/(dashboard)/customers/types";
interface CustomersTableProps {
customers: Customer[];
userRole: string | null;
onEdit: (customer: Customer) => void;
onDelete: (customer: Customer) => void;
onViewInfo: (customer: Customer) => void;
isLoading: boolean;
}
const CustomersTable: React.FC<CustomersTableProps> = ({
customers,
userRole,
onEdit,
onDelete,
onViewInfo,
isLoading,
}) => {
const truncateId = (id: string) => {
if (id.length <= 10) return id;
return id.substring(0, 7) + "...";
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Text>Loading customers...</Text>
</div>
);
}
return (
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Customer Name</TableHeaderCell>
<TableHeaderCell>Customer ID</TableHeaderCell>
<TableHeaderCell>Spend (USD)</TableHeaderCell>
<TableHeaderCell>Budget (USD)</TableHeaderCell>
<TableHeaderCell>Default Model</TableHeaderCell>
<TableHeaderCell>Region</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
{(userRole === "Admin" || userRole === "Org Admin") && (
<TableHeaderCell>Actions</TableHeaderCell>
)}
</TableRow>
</TableHead>
<TableBody>
{customers.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-12">
<Text>No customers found.</Text>
</TableCell>
</TableRow>
) : (
customers.map((customer) => (
<TableRow key={customer.user_id}>
<TableCell>{customer.alias || "—"}</TableCell>
<TableCell>
<Tooltip title={customer.user_id}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100"
onClick={() => onViewInfo(customer)}
>
{truncateId(customer.user_id)}
</Button>
</Tooltip>
</TableCell>
<TableCell>{formatNumberWithCommas(customer.spend, 4)}</TableCell>
<TableCell>
{customer.litellm_budget_table?.max_budget !== null &&
customer.litellm_budget_table?.max_budget !== undefined
? customer.litellm_budget_table.max_budget.toString()
: "No limit"}
</TableCell>
<TableCell>
{customer.default_model ? (
<Badge color="gray">{customer.default_model}</Badge>
) : (
<Text></Text>
)}
</TableCell>
<TableCell>
{customer.allowed_model_region ? (
<Text>{customer.allowed_model_region.toUpperCase()}</Text>
) : (
<Text></Text>
)}
</TableCell>
<TableCell>
{customer.blocked ? (
<Badge color="red">Blocked</Badge>
) : (
<Badge color="green">Active</Badge>
)}
</TableCell>
{(userRole === "Admin" || userRole === "Org Admin") && (
<TableCell>
<div className="flex items-center gap-1">
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => onEdit(customer)}
className="cursor-pointer"
/>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => onDelete(customer)}
className="cursor-pointer"
/>
</div>
</TableCell>
)}
</TableRow>
))
)}
</TableBody>
</Table>
);
};
export default CustomersTable;

View file

@ -0,0 +1,137 @@
import React, { useState } from "react";
import { Modal, Form, Input, InputNumber, Select as AntSelect } from "antd";
import type { NewCustomerData } from "@/app/(dashboard)/customers/types";
interface CreateCustomerModalProps {
isOpen: boolean;
onClose: () => void;
onCreate: (customer: NewCustomerData) => void;
}
const CreateCustomerModal: React.FC<CreateCustomerModalProps> = ({
isOpen,
onClose,
onCreate,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async () => {
try {
const values = await form.validateFields();
setLoading(true);
onCreate(values);
form.resetFields();
} catch (error) {
console.error("Validation failed:", error);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onClose();
};
return (
<Modal
title="Create New Customer"
open={isOpen}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={loading}
okText="Create Customer"
width={600}
>
<Form
form={form}
layout="vertical"
className="mt-4"
>
<Form.Item
label="User ID"
name="user_id"
rules={[{ required: true, message: "Please enter a user ID" }]}
>
<Input placeholder="e.g. customer-007" />
</Form.Item>
<Form.Item
label="Alias"
name="alias"
>
<Input placeholder="e.g. Acme Corp" />
</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>
<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>
</Form>
</Modal>
);
};
export default CreateCustomerModal;

View file

@ -0,0 +1,167 @@
import React, { useEffect, useState } from "react";
import { Modal, Form, Input, InputNumber, Select as AntSelect, Switch } from "antd";
import type { Customer } from "@/app/(dashboard)/customers/types";
interface CustomerInfoModalProps {
isOpen: boolean;
onClose: () => void;
customer: Customer | null;
onSave: (customer: Customer) => void;
}
const CustomerInfoModal: React.FC<CustomerInfoModalProps> = ({
isOpen,
onClose,
customer,
onSave,
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (customer) {
form.setFieldsValue({
...customer,
max_budget: customer.litellm_budget_table?.max_budget,
budget_duration: customer.litellm_budget_table?.budget_duration,
});
}
}, [customer, form]);
const handleOk = async () => {
if (!customer) return;
try {
const values = await form.validateFields();
setLoading(true);
onSave({
...customer,
...values,
});
} catch (error) {
console.error("Validation failed:", error);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
form.resetFields();
onClose();
};
if (!customer) return null;
return (
<Modal
title="Customer Details"
open={isOpen}
onOk={handleOk}
onCancel={handleCancel}
confirmLoading={loading}
okText="Save Changes"
width={600}
>
<Form
form={form}
layout="vertical"
className="mt-4"
>
<Form.Item label="Customer ID">
<Input
value={customer.user_id}
disabled
className="font-mono bg-gray-50"
/>
</Form.Item>
<Form.Item
label="Alias"
name="alias"
>
<Input placeholder="Customer alias" />
</Form.Item>
<div className="grid grid-cols-2 gap-4">
<Form.Item label="Spend (USD)">
<Input
value={customer.spend.toFixed(4)}
disabled
className="bg-gray-50"
/>
</Form.Item>
<Form.Item
label="Max Budget"
name="max_budget"
>
<InputNumber
className="w-full"
placeholder="No limit"
min={0}
/>
</Form.Item>
</div>
<Form.Item
label="Budget ID"
name="budget_id"
>
<Input placeholder="e.g. free_tier" />
</Form.Item>
<div className="grid grid-cols-2 gap-4">
<Form.Item
label="Default Model"
name="default_model"
>
<AntSelect placeholder="None">
<AntSelect.Option value="">None</AntSelect.Option>
<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" />
</Form.Item>
<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>
</Form>
</Modal>
);
};
export default CustomerInfoModal;

View file

@ -0,0 +1,52 @@
import React from "react";
import { Modal } from "antd";
import { AlertTriangle } from "lucide-react";
interface DeleteCustomerModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
customerName: string;
customerId: string;
}
const DeleteCustomerModal: React.FC<DeleteCustomerModalProps> = ({
isOpen,
onClose,
onConfirm,
customerName,
customerId,
}) => {
return (
<Modal
title={
<div className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-red-600" />
<span>Delete Customer</span>
</div>
}
open={isOpen}
onOk={onConfirm}
onCancel={onClose}
okText="Delete"
okButtonProps={{ danger: true }}
cancelText="Cancel"
>
<div className="py-4">
<p className="text-sm text-gray-900 mb-2">
Are you sure you want to delete this customer?
</p>
<p className="text-sm text-gray-500">
This will permanently delete{" "}
<span className="font-medium text-gray-700">
{customerName || customerId}
</span>{" "}
<span className="text-gray-400">({customerId})</span>. This action
cannot be undone.
</p>
</div>
</Modal>
);
};
export default DeleteCustomerModal;

View file

@ -0,0 +1,36 @@
import { useCallback, useState } from "react";
import { allEndUsersCall } from "@/components/networking";
interface UseFetchCustomersProps {
setCustomers: (customers: any[]) => void;
}
const useFetchCustomers = ({ setCustomers }: UseFetchCustomersProps) => {
const [lastRefreshed, setLastRefreshed] = useState(
new Date().toLocaleString("en-US")
);
const onRefreshClick = useCallback(
async (accessToken: string | null) => {
if (!accessToken) return;
try {
const data = await allEndUsersCall(accessToken);
if (data) {
setCustomers(data);
}
setLastRefreshed(new Date().toLocaleString("en-US"));
} catch (error) {
console.error("Error fetching customers:", error);
}
},
[setCustomers]
);
return {
lastRefreshed,
onRefreshClick,
};
};
export default useFetchCustomers;

View file

@ -0,0 +1,45 @@
"use client";
import CustomersView from "@/app/(dashboard)/customers/CustomersView";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useEffect, useState } from "react";
import { allEndUsersCall, type Customer } from "@/components/networking";
const CustomersPage = () => {
const { accessToken, userId, userRole } = useAuthorized();
const [customers, setCustomers] = useState<Customer[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchCustomers = async () => {
if (!accessToken) return;
setIsLoading(true);
try {
const response = await allEndUsersCall(accessToken);
if (response) {
setCustomers(Array.isArray(response) ? response : []);
}
} catch (error) {
console.error("Error fetching customers:", error);
} finally {
setIsLoading(false);
}
};
fetchCustomers();
}, [accessToken]);
return (
<CustomersView
customers={customers}
setCustomers={setCustomers}
accessToken={accessToken}
userID={userId}
userRole={userRole}
isLoading={isLoading}
/>
);
};
export default CustomersPage;

View file

@ -0,0 +1,26 @@
export interface Customer {
user_id: string;
alias: string | null;
spend: number;
allowed_model_region: string | null;
default_model: string | null;
budget_id: string | null;
blocked: boolean;
max_budget?: number | null;
budget_duration?: string | null;
litellm_budget_table?: {
max_budget: number | null;
budget_duration: string | null;
} | null;
}
export interface NewCustomerData {
user_id: string;
alias?: string;
max_budget?: string;
budget_id?: string;
default_model?: string;
allowed_model_region?: string;
budget_duration?: string;
metadata?: string;
}

View file

@ -194,6 +194,20 @@ export interface Organization {
};
}
export interface Customer {
user_id: string;
alias: string | null;
spend: number;
allowed_model_region: string | null;
default_model: string | null;
budget_id: string | null;
blocked: boolean;
litellm_budget_table?: {
max_budget: number | null;
budget_duration: string | null;
} | null;
}
export interface CredentialItem {
credential_name: string;
credential_values: any;
@ -2511,6 +2525,97 @@ export const allEndUsersCall = async (accessToken: string) => {
}
};
export const customerListCall = async (accessToken: string) => {
return allEndUsersCall(accessToken);
};
export const customerCreateCall = async (accessToken: string, data: any) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/customer/new` : `/customer/new`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const responseData = await response.json();
console.log("Customer created:", responseData);
return responseData;
} catch (error) {
console.error("Failed to create customer:", error);
throw error;
}
};
export const customerUpdateCall = async (accessToken: string, data: any) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/customer/update` : `/customer/update`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const responseData = await response.json();
console.log("Customer updated:", responseData);
return responseData;
} catch (error) {
console.error("Failed to update customer:", error);
throw error;
}
};
export const customerDeleteCall = async (accessToken: string, userId: string) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/customer/delete` : `/customer/delete`;
const response = await fetch(url, {
method: "POST",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ user_ids: [userId] }),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const responseData = await response.json();
console.log("Customer deleted:", responseData);
return responseData;
} catch (error) {
console.error("Failed to delete customer:", error);
throw error;
}
};
export const userFilterUICall = async (accessToken: string, params: URLSearchParams) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`;