From 28f9c8746d28f6ca60ec2e3950558da65f021e6f Mon Sep 17 00:00:00 2001
From: kerry
Date: Fri, 6 Nov 2026 22:30:01 +0000
Subject: [PATCH] feat(ui): customer detail as query state, edit customer
dialog, lint fixes
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
.../_components/CreateCustomerDialog.tsx | 8 +--
.../CustomerDetail.tsx} | 20 +++---
.../_components/EditCustomerDialog.tsx | 72 +++++++++++++++++++
.../src/app/(dashboard)/customers/page.tsx | 12 ++--
4 files changed, 90 insertions(+), 22 deletions(-)
rename ui/litellm-dashboard/src/app/(dashboard)/customers/{[id]/page.tsx => _components/CustomerDetail.tsx} (90%)
create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/customers/_components/EditCustomerDialog.tsx
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 (
+
+
+
+ );
+}
+
+export default function EditCustomerDialog({ open, ...form }: Props) {
+ return (
+
+ );
+}
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" }); }} />
);
}