From 7624327ab952c372eacb3e8f9e904760b2cd73ae Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 1 Sep 2026 17:17:54 -0400 Subject: [PATCH] refactor(impersonation): replace page control with modal Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../impersonation/ImpersonationControl.tsx | 55 -------- .../impersonation/ImpersonationModal.tsx | 127 ++++++++++++++++++ 2 files changed, 127 insertions(+), 55 deletions(-) delete mode 100644 src/components/frontend/impersonation/ImpersonationControl.tsx create mode 100644 src/components/frontend/impersonation/ImpersonationModal.tsx diff --git a/src/components/frontend/impersonation/ImpersonationControl.tsx b/src/components/frontend/impersonation/ImpersonationControl.tsx deleted file mode 100644 index 02d6031..0000000 --- a/src/components/frontend/impersonation/ImpersonationControl.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; - -type Target = { id: number | string; username?: string | null; displayName?: string | null }; - -export function ImpersonationControl({ users }: { users: Target[] }) { - const router = useRouter(); - const [userId, setUserId] = useState(""); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - - async function start() { - if (!userId) return; - setBusy(true); - setError(null); - try { - const response = await fetch("/api/impersonation/start", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ userId }), - }); - const body = await response.json().catch(() => null); - if (!response.ok) throw new Error(body?.error ?? "Unable to start impersonation."); - router.push("/"); - router.refresh(); - } catch (e) { - setError(e instanceof Error ? e.message : "Unable to start impersonation."); - } finally { - setBusy(false); - } - } - - return ( -
-

Impersonate a user

-

All requests will use the selected user's identity and permissions until you return to your account.

-
- - -
- {error &&

{error}

} - -
- ); -} diff --git a/src/components/frontend/impersonation/ImpersonationModal.tsx b/src/components/frontend/impersonation/ImpersonationModal.tsx new file mode 100644 index 0000000..ec2b5d0 --- /dev/null +++ b/src/components/frontend/impersonation/ImpersonationModal.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import type { ImpersonationUser } from "@/lib/impersonation"; +import { cn } from "@/lib/utils"; + +interface ImpersonationModalProps { + users: ImpersonationUser[]; + open: boolean; + onOpenChange: (open: boolean) => void; + onStart?: () => void; +} + +export function ImpersonationModal({ users, open, onOpenChange, onStart }: ImpersonationModalProps) { + const [userId, setUserId] = useState(""); + const [pickerOpen, setPickerOpen] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const selectedUser = useMemo( + () => users.find((user) => String(user.id) === userId) ?? null, + [users, userId], + ); + + async function startImpersonation() { + if (!userId) return; + setBusy(true); + setError(null); + try { + const response = await fetch("/api/impersonation/start", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ userId }), + }); + const body = await response.json().catch(() => null); + if (!response.ok) throw new Error(body?.error ?? "Unable to start impersonation."); + onOpenChange(false); + onStart?.(); + } catch (e) { + setError(e instanceof Error ? e.message : "Unable to start impersonation."); + } finally { + setBusy(false); + } + } + + return ( + + + + Impersonate a user + + All requests will use the selected user's identity and permissions until you return to your account. + + +
+
+ + + + + + + + + + No users found. + + {users.map((user) => ( + { + setUserId(String(user.id)); + setPickerOpen(false); + }} + > + + {user.displayName || user.username || `User ${user.id}`} + {user.username ? ` (${user.username})` : ""} + + + + ))} + + + + + +
+ {error &&

{error}

} + +
+
+
+ ); +}