diff --git a/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CreateCustomerDialog.tsx b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CreateCustomerDialog.tsx index 5af21b90d3a..42f42aedd83 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CreateCustomerDialog.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CreateCustomerDialog.tsx @@ -1,26 +1,22 @@ "use client"; import { useState } from "react"; -import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { $api } from "@/lib/http/api"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { uiHref } from "@/utils/uiHref"; -export default function CreateCustomerDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (o: boolean) => void; onCreated: () => void }) { - const router = useRouter(); +export default function CreateCustomerDialog({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (o: boolean) => void; onCreated: (userId: string) => void }) { const [userId, setUserId] = useState(""); const [alias, setAlias] = useState(""); const [budget, setBudget] = useState(""); const create = $api.useMutation("post", "/customer/new", { onSuccess: () => { toast.success(`Created ${alias || userId}`); - onCreated(); + onCreated(userId.trim()); onOpenChange(false); - router.push(uiHref(`customers/${encodeURIComponent(userId)}`)); }, onError: (e: unknown) => toast.error(`Failed to create customer: ${e instanceof Error ? e.message : String(e)}`), }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/customers/[id]/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerDetail.tsx similarity index 90% rename from ui/litellm-dashboard/src/app/(dashboard)/customers/[id]/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerDetail.tsx index c85c7dbb32f..d6f57f485a5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/customers/[id]/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerDetail.tsx @@ -1,8 +1,6 @@ "use client"; import { useMemo, useState } from "react"; -import { useParams } from "next/navigation"; -import Link from "next/link"; import { Ban, Pencil, Plus, SlidersHorizontal } from "lucide-react"; import { toast } from "sonner"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; @@ -10,10 +8,10 @@ import { $api } from "@/lib/http/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { uiHref } from "@/utils/uiHref"; -import { customerStatus, formatUSD, useCustomerSpendByCustomer } from "../_components/customerPortalData"; -import { customerLimitsOnKey, useAllKeys, useCustomerTrafficKeys, type PortalKey } from "../_components/customerKeys"; -import KeyLimitDialog from "../_components/KeyLimitDialog"; +import { customerStatus, formatUSD, useCustomerSpendByCustomer } from "./customerPortalData"; +import { customerLimitsOnKey, useAllKeys, useCustomerTrafficKeys, type PortalKey } from "./customerKeys"; +import KeyLimitDialog from "./KeyLimitDialog"; +import EditCustomerDialog from "./EditCustomerDialog"; const STATUS_BADGE = { blocked: { variant: "outline", label: "Blocked" }, @@ -21,15 +19,14 @@ const STATUS_BADGE = { active: { variant: "secondary", label: "Active" }, } as const; -export default function CustomerDetailPage() { - const params = useParams<{ id: string }>(); - const customerId = decodeURIComponent(params.id); +export default function CustomerDetail({ customerId, onBack }: { customerId: string; onBack: () => void }) { const { accessToken } = useAuthorized(); const info = $api.useQuery("get", "/customer/info", { params: { query: { end_user_id: customerId } } }, { enabled: Boolean(accessToken) }); const spendQuery = useCustomerSpendByCustomer(accessToken); const keysQuery = useAllKeys(accessToken); const trafficQuery = useCustomerTrafficKeys(accessToken, customerId); const [dialog, setDialog] = useState<{ open: boolean; editKey: PortalKey | null }>({ open: false, editKey: null }); + const [editOpen, setEditOpen] = useState(false); const block = $api.useMutation("post", "/customer/block", { onSuccess: () => { toast.success("Customer blocked"); info.refetch(); } }); const unblock = $api.useMutation("post", "/customer/update", { onSuccess: () => { toast.success("Customer unblocked"); info.refetch(); } }); @@ -63,7 +60,7 @@ export default function CustomerDetailPage() {

- + {c?.blocked ? ( ) : ( @@ -115,7 +112,8 @@ export default function CustomerDetailPage() { )}
-

Back to customers

+ + {c && info.refetch()} />} {accessToken && ( void; + customer: EndUser; + onSaved: () => void; +} + +function EditCustomerForm({ onOpenChange, customer, onSaved }: Omit) { + const [alias, setAlias] = useState(customer.alias ?? ""); + const currentBudget = customer.litellm_budget_table?.max_budget; + const [budget, setBudget] = useState(currentBudget === null || currentBudget === undefined ? "" : String(currentBudget)); + const update = $api.useMutation("post", "/customer/update", { + onSuccess: () => { + toast.success("Customer updated"); + onSaved(); + onOpenChange(false); + }, + onError: (e) => toast.error(`Failed to update customer: ${String(e)}`), + }); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + update.mutate({ + body: { + user_id: customer.user_id, + alias: alias.trim() || null, + blocked: Boolean(customer.blocked), + max_budget: budget ? Number(budget) : null, + }, + }); + }; + return ( + +
+ + Edit customer + {customer.user_id} + +
+ + setAlias(e.target.value)} autoFocus /> +
+
+ + setBudget(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="Leave empty for no budget" /> +
+ + + + +
+
+ ); +} + +export default function EditCustomerDialog({ open, ...form }: Props) { + return ( + + {open && } + + ); +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/customers/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/customers/page.tsx index fe7a14a81ce..499b83dc201 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/customers/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/customers/page.tsx @@ -2,7 +2,7 @@ import { customerLimitsOnKey, useAllKeys } from "./_components/customerKeys"; import { useMemo, useState } from "react"; -import { useRouter } from "next/navigation"; +import { parseAsString, useQueryState } from "nuqs"; import { Plus } from "lucide-react"; import { toast } from "sonner"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; @@ -12,7 +12,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { uiHref } from "@/utils/uiHref"; +import CustomerDetail from "./_components/CustomerDetail"; import CreateCustomerDialog from "./_components/CreateCustomerDialog"; import { customerStatus, formatUSD, useCustomerSpendByCustomer, type CustomerStatus } from "./_components/customerPortalData"; @@ -24,7 +24,7 @@ const STATUS_LABEL: Record { toast.success("Customer blocked"); customersQuery.refetch(); } }); const unblock = $api.useMutation("post", "/customer/update", { onSuccess: () => { toast.success("Customer unblocked"); customersQuery.refetch(); } }); + if (selectedId) return setSelectedId(null)} />; + return (
@@ -78,7 +80,7 @@ export default function CustomersPage() { {rows.map(({ c, spend, keys, status }) => { const max = c.litellm_budget_table?.max_budget ?? null; return ( - router.push(uiHref(`customers/${encodeURIComponent(c.user_id)}`))}> + setSelectedId(c.user_id, { history: "push" })}>

{c.alias || c.user_id}

{c.user_id}

@@ -100,7 +102,7 @@ export default function CustomersPage() {
- customersQuery.refetch()} /> + { customersQuery.refetch(); setSelectedId(id, { history: "push" }); }} />
); }