import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alertDialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { resetDurationLabels } from "@/lib/constants/governance"; import { getErrorMessage, useDeleteCustomerMutation } from "@/lib/store"; import { Customer, Team, VirtualKey } from "@/lib/types/governance"; import { cn } from "@/lib/utils"; import { formatCurrency } from "@/lib/utils/governance"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { Input } from "@/components/ui/input"; import { ChevronLeft, ChevronRight, Edit, Plus, Search, Trash2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import CustomerDialog from "./customerDialog"; import { CustomersEmptyState } from "./customersEmptyState"; // Helper to format reset duration for display const formatResetDuration = (duration: string) => { return resetDurationLabels[duration] || duration; }; interface CustomersTableProps { customers: Customer[]; totalCount: number; teams: Team[]; virtualKeys: VirtualKey[]; search: string; debouncedSearch: string; onSearchChange: (value: string) => void; offset: number; limit: number; onOffsetChange: (offset: number) => void; } export default function CustomersTable({ customers, totalCount, teams, virtualKeys, search, debouncedSearch, onSearchChange, offset, limit, onOffsetChange, }: CustomersTableProps) { const [showCustomerDialog, setShowCustomerDialog] = useState(false); const [editingCustomer, setEditingCustomer] = useState(null); const hasCreateAccess = useRbac(RbacResource.Customers, RbacOperation.Create); const hasUpdateAccess = useRbac(RbacResource.Customers, RbacOperation.Update); const hasDeleteAccess = useRbac(RbacResource.Customers, RbacOperation.Delete); const [deleteCustomer, { isLoading: isDeleting }] = useDeleteCustomerMutation(); const handleDelete = async (customerId: string) => { try { await deleteCustomer(customerId).unwrap(); toast.success("Customer deleted successfully"); } catch (error) { toast.error(getErrorMessage(error)); } }; const handleAddCustomer = () => { setEditingCustomer(null); setShowCustomerDialog(true); }; const handleEditCustomer = (customer: Customer) => { setEditingCustomer(customer); setShowCustomerDialog(true); }; const handleCustomerSaved = () => { setShowCustomerDialog(false); setEditingCustomer(null); }; const getTeamsForCustomer = (customerId: string) => { return teams.filter((team) => team.customer_id === customerId); }; const getVirtualKeysForCustomer = (customerId: string) => { return virtualKeys.filter((vk) => vk.customer_id === customerId); }; const hasActiveFilters = debouncedSearch; // True empty state: no customers at all (not just filtered to zero) if (totalCount === 0 && !hasActiveFilters) { return ( <> {showCustomerDialog && ( setShowCustomerDialog(false)} /> )} ); } return ( <> {showCustomerDialog && ( setShowCustomerDialog(false)} /> )}

Customers

Manage customer accounts with their own teams, budgets, and access controls.

onSearchChange(e.target.value)} className="pl-9" data-testid="customers-search-input" />
Name Teams Budget Rate Limit Virtual Keys {customers.length === 0 ? ( No matching customers found. ) : ( customers.map((customer) => { const customerTeams = getTeamsForCustomer(customer.id); const vks = getVirtualKeysForCustomer(customer.id); // Budget calculations const isBudgetExhausted = customer.budget?.max_limit && customer.budget.max_limit > 0 && customer.budget.current_usage >= customer.budget.max_limit; const budgetPercentage = customer.budget?.max_limit && customer.budget.max_limit > 0 ? Math.min((customer.budget.current_usage / customer.budget.max_limit) * 100, 100) : 0; // Rate limit calculations const isTokenLimitExhausted = customer.rate_limit?.token_max_limit && customer.rate_limit.token_max_limit > 0 && customer.rate_limit.token_current_usage >= customer.rate_limit.token_max_limit; const isRequestLimitExhausted = customer.rate_limit?.request_max_limit && customer.rate_limit.request_max_limit > 0 && customer.rate_limit.request_current_usage >= customer.rate_limit.request_max_limit; const isRateLimitExhausted = isTokenLimitExhausted || isRequestLimitExhausted; const tokenPercentage = customer.rate_limit?.token_max_limit && customer.rate_limit.token_max_limit > 0 ? Math.min((customer.rate_limit.token_current_usage / customer.rate_limit.token_max_limit) * 100, 100) : 0; const requestPercentage = customer.rate_limit?.request_max_limit && customer.rate_limit.request_max_limit > 0 ? Math.min((customer.rate_limit.request_current_usage / customer.rate_limit.request_max_limit) * 100, 100) : 0; const isExhausted = isBudgetExhausted || isRateLimitExhausted; return (
{customer.name} {isExhausted && ( Limit Reached )}
{customerTeams?.length > 0 ? (
{customerTeams.length} {customerTeams.length === 1 ? "team" : "teams"} {customerTeams.map((team) => team.name).join(", ")}
) : ( - )}
{customer.budget ? (
{formatCurrency(customer.budget.max_limit)} {formatResetDuration(customer.budget.reset_duration)}
div]:bg-red-500/70" : budgetPercentage > 80 ? "[&>div]:bg-amber-500/70" : "[&>div]:bg-emerald-500/70", )} />

{formatCurrency(customer.budget.current_usage)} / {formatCurrency(customer.budget.max_limit)}

Resets {formatResetDuration(customer.budget.reset_duration)}

) : ( - )}
{customer.rate_limit ? (
{customer.rate_limit.token_max_limit && (
{customer.rate_limit.token_max_limit.toLocaleString()} tokens {formatResetDuration(customer.rate_limit.token_reset_duration || "1h")}
div]:bg-red-500/70" : tokenPercentage > 80 ? "[&>div]:bg-amber-500/70" : "[&>div]:bg-emerald-500/70", )} />

{customer.rate_limit.token_current_usage.toLocaleString()} /{" "} {customer.rate_limit.token_max_limit.toLocaleString()} tokens

Resets {formatResetDuration(customer.rate_limit.token_reset_duration || "1h")}

)} {customer.rate_limit.request_max_limit && (
{customer.rate_limit.request_max_limit.toLocaleString()} req {formatResetDuration(customer.rate_limit.request_reset_duration || "1h")}
div]:bg-red-500/70" : requestPercentage > 80 ? "[&>div]:bg-amber-500/70" : "[&>div]:bg-emerald-500/70", )} />

{customer.rate_limit.request_current_usage.toLocaleString()} /{" "} {customer.rate_limit.request_max_limit.toLocaleString()} requests

Resets {formatResetDuration(customer.rate_limit.request_reset_duration || "1h")}

)}
) : ( - )}
{vks?.length > 0 ? (
{vks.length} {vks.length === 1 ? "key" : "keys"} {vks.map((vk) => vk.name).join(", ")}
) : ( - )}
Delete Customer Are you sure you want to delete "{customer.name}"? This will also delete all associated teams and unassign any virtual keys. This action cannot be undone. Cancel handleDelete(customer.id)} disabled={isDeleting} className="bg-red-600 hover:bg-red-700" > {isDeleting ? "Deleting..." : "Delete"}
); }) )}
{/* Pagination */} {totalCount > 0 && (

Showing {offset + 1}-{Math.min(offset + limit, totalCount)} of {totalCount}

)}
); }