Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions frontend/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Dialog as DialogPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { useFloatingLayerDismissGuard } from "@/components/ui/use-floating-layer-dismiss-guard"

function Dialog({
...props
Expand Down Expand Up @@ -51,10 +52,15 @@ function DialogContent({
className,
children,
showCloseButton = true,
onFocusOutside,
onInteractOutside,
onPointerDownOutside,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
const guardDismiss = useFloatingLayerDismissGuard()

return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
Expand All @@ -64,6 +70,18 @@ function DialogContent({
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-hidden rounded-lg border p-6 shadow-lg duration-200 outline-none overscroll-contain sm:max-w-lg",
className
)}
onFocusOutside={(event) => {
onFocusOutside?.(event)
guardDismiss(event)
}}
onInteractOutside={(event) => {
onInteractOutside?.(event)
guardDismiss(event)
}}
onPointerDownOutside={(event) => {
onPointerDownOutside?.(event)
guardDismiss(event)
}}
{...props}
>
{children}
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { XIcon } from "lucide-react"
import { Dialog as SheetPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
import { useFloatingLayerDismissGuard } from "@/components/ui/use-floating-layer-dismiss-guard"

function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
Expand Down Expand Up @@ -47,16 +48,35 @@ function SheetContent({
children,
side = "right",
showCloseButton = true,
onFocusOutside,
onInteractOutside,
onPointerDownOutside,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
showCloseButton?: boolean
}) {
// Keep the sheet open when an outside interaction is really the user
// dismissing a nested floating layer (Select / DropdownMenu / Popover).
const guardDismiss = useFloatingLayerDismissGuard()

return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
onFocusOutside={(event) => {
onFocusOutside?.(event)
guardDismiss(event)
}}
onInteractOutside={(event) => {
onInteractOutside?.(event)
guardDismiss(event)
}}
onPointerDownOutside={(event) => {
onPointerDownOutside?.(event)
guardDismiss(event)
}}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out overscroll-contain data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/ui/use-floating-layer-dismiss-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"use client"

import * as React from "react"

// Floating layers (Radix Select / DropdownMenu / Popover, or anything rendered
// through a Radix popper) that can appear inside a modal. Clicking inside one —
// or outside one while it is open — must dismiss the LAYER, never the modal.
const FLOATING_LAYER_SELECTOR = [
"[data-slot='select-content']",
"[data-slot='dropdown-menu-content']",
"[data-slot='dropdown-menu-sub-content']",
"[data-slot='popover-content']",
"[data-radix-popper-content-wrapper]",
].join(", ")

const OPEN_FLOATING_LAYER_SELECTOR = [
"[data-slot='select-content'][data-state='open']",
"[data-slot='dropdown-menu-content'][data-state='open']",
"[data-slot='dropdown-menu-sub-content'][data-state='open']",
"[data-slot='popover-content'][data-state='open']",
].join(", ")

function isFloatingLayerTarget(target: EventTarget | null): boolean {
return target instanceof Element && target.closest(FLOATING_LAYER_SELECTOR) !== null
}

function isFloatingLayerOpen(): boolean {
return (
typeof document !== "undefined" &&
document.querySelector(OPEN_FLOATING_LAYER_SELECTOR) !== null
)
}

// Minimal shape shared by Radix's FocusOutside / PointerDownOutside /
// InteractOutside events.
type DismissEvent = {
target: EventTarget | null
defaultPrevented: boolean
preventDefault: () => void
}

/**
* Guard for a modal's onFocusOutside / onInteractOutside / onPointerDownOutside:
* keeps the modal open when the "outside" interaction is really the user
* dismissing a nested floating layer (Select / DropdownMenu / Popover / custom
* multi-select), instead of closing the modal.
*
* The captured-at-pointerdown flag is essential: a mouse click outside an OPEN
* dropdown makes Radix close that dropdown FIRST (its data-state flips to
* "closed") before the modal's outside handler runs, so a live DOM check would
* already miss it and the modal would wrongly close. We capture the open-state
* in a document pointerdown listener on the CAPTURE phase, which runs before
* Radix's own bubble-phase dismiss listeners.
*/
export function useFloatingLayerDismissGuard() {
const floatingLayerOpenAtPointerDownRef = React.useRef(false)

React.useEffect(() => {
const onPointerDownCapture = () => {
floatingLayerOpenAtPointerDownRef.current = isFloatingLayerOpen()
}
document.addEventListener("pointerdown", onPointerDownCapture, true)
return () =>
document.removeEventListener("pointerdown", onPointerDownCapture, true)
}, [])

return React.useCallback((event: DismissEvent) => {
if (
!event.defaultPrevented &&
(isFloatingLayerTarget(event.target) ||
floatingLayerOpenAtPointerDownRef.current ||
isFloatingLayerOpen())
) {
event.preventDefault()
}
}, [])
}
9 changes: 5 additions & 4 deletions frontend/src/features/accounts/components/account-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export function AccountActions({
return (
<div className="space-y-3 border-t pt-4">
{!showOperatorRecoveryAction ? (
<div className="flex flex-wrap items-center gap-3 rounded-md border bg-muted/30 p-3">
<div className="flex min-w-36 items-center gap-2 text-sm font-medium">
<div className="flex flex-col gap-2 rounded-md border bg-muted/30 p-3 sm:flex-row sm:items-center sm:gap-3">
<div className="flex min-w-0 items-center gap-2 text-sm font-medium sm:min-w-36">
<Route className="h-4 w-4 text-muted-foreground" />
Routing policy
</div>
Expand All @@ -82,7 +82,7 @@ export function AccountActions({
<SelectTrigger
aria-label="Routing policy"
size="sm"
className="h-8 min-w-32 flex-1 text-xs"
className="h-8 w-full min-w-0 text-xs sm:min-w-32 sm:flex-1"
>
<SelectValue />
</SelectTrigger>
Expand All @@ -97,14 +97,15 @@ export function AccountActions({

<label
htmlFor={`security-work-authorized-${account.accountId}`}
className="flex items-center justify-between gap-3 rounded-md border px-3 py-2"
className="flex min-w-0 items-center justify-between gap-3 rounded-md border px-3 py-2"
>
<span className="flex min-w-0 items-center gap-2 text-xs font-medium">
<ShieldCheck className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span className="truncate">Trusted Access for Cyber</span>
</span>
<Switch
id={`security-work-authorized-${account.accountId}`}
className="shrink-0"
checked={account.securityWorkAuthorized ?? false}
disabled={busy || readOnly}
onCheckedChange={(checked) =>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/features/accounts/components/account-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function AccountDetail({
return (
<div
key={account.accountId}
className="animate-fade-in-up space-y-4 rounded-xl border bg-card p-5"
className="animate-fade-in-up min-w-0 space-y-4 rounded-xl border bg-card p-4 sm:p-5"
>
{/* Account header */}
<div>
Expand Down Expand Up @@ -245,7 +245,7 @@ function AccountNameField({
}

return (
<div className="flex items-center gap-1.5">
<div className="flex min-w-0 items-center gap-1.5">
<h2 className="min-w-0 truncate text-base font-semibold">
{labelIsEmail ? (
<>
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/features/accounts/components/account-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ export function AccountListItem({
type="button"
onClick={() => onSelect(account.accountId)}
className={cn(
"w-full rounded-lg px-3 py-2.5 text-left transition-colors",
"min-w-0 w-full rounded-lg px-3 py-2.5 text-left transition-colors",
selected ? "bg-primary/8 ring-1 ring-primary/25" : "hover:bg-muted/50",
)}
>
<div className="flex items-center gap-2.5">
<div className="flex items-start gap-2.5">
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">
{titleIsEmail && blurred ? (
Expand Down Expand Up @@ -111,7 +111,7 @@ export function AccountListItem({
<div
className={cn(
"mt-2 grid gap-2",
visibleQuotaRows > 1 ? "grid-cols-2" : "grid-cols-1",
visibleQuotaRows > 1 ? "grid-cols-1 sm:grid-cols-2" : "grid-cols-1",
)}
>
{showMonthlyRow ? (
Expand All @@ -136,9 +136,9 @@ export function AccountListItem({
/>
) : null}
</div>
<div className="mt-2 flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
<span>{warmupLabel}</span>
<span className="truncate">{warmupMeta}</span>
<div className="mt-2 flex min-w-0 items-center justify-between gap-2 text-[10px] text-muted-foreground">
<span className="shrink-0">{warmupLabel}</span>
<span className="min-w-0 truncate">{warmupMeta}</span>
</div>
</button>
);
Expand Down
13 changes: 8 additions & 5 deletions frontend/src/features/accounts/components/account-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export function AccountList({
}, [accounts, quotaDisplay, search, statusFilter, activeSortMode]);

return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<div className="relative col-span-2 min-w-0">
<div className="min-w-0 space-y-3">
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div className="relative min-w-0 sm:col-span-2">
<Search className="pointer-events-none absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/60" aria-hidden />
<Input
placeholder="Search accounts..."
Expand Down Expand Up @@ -121,7 +121,7 @@ export function AccountList({
</Select>
</div>

<div className="flex items-center justify-between gap-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<Button
type="button"
variant="link"
Expand All @@ -146,7 +146,10 @@ export function AccountList({

{helpOpen ? <WindowsOauthHelp /> : null}

<div className="max-h-[calc(100vh-16rem)] space-y-1 overflow-y-auto p-1" data-testid="account-list-scroll-region">
<div
className="max-h-[min(32rem,calc(100dvh-16rem))] space-y-1 overflow-y-auto p-1"
data-testid="account-list-scroll-region"
>
{filtered.length === 0 ? (
<div className="flex flex-col items-center gap-2 rounded-lg border border-dashed p-6 text-center">
<p className="text-sm font-medium text-muted-foreground">No matching accounts</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ export function AccountProxyBinding({ account, admin, busy, readOnly = false, on
}

return (
<section className="rounded-lg border bg-muted/30 p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2.5">
<section className="min-w-0 rounded-lg border bg-muted/30 p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
<div className="flex min-w-0 items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
<Network className="h-4 w-4 text-primary" aria-hidden="true" />
</div>
<div>
<div className="min-w-0">
<h3 className="text-sm font-semibold">Account proxy binding</h3>
<p className="text-xs text-muted-foreground">
Route this account's ChatGPT upstream traffic through a specific proxy pool.
Expand All @@ -43,6 +43,7 @@ export function AccountProxyBinding({ account, admin, busy, readOnly = false, on
</div>
<Switch
aria-label="Enable account proxy binding"
className="shrink-0"
checked={active}
disabled={busy || readOnly || !binding}
onCheckedChange={(checked) => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/features/accounts/components/account-token-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ export function AccountTokenInfo({ account }: AccountTokenInfoProps) {
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Token Status</h3>
<dl className="space-y-2 text-xs">
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center justify-between gap-2">
<dt className="text-muted-foreground">Access</dt>
<dd className="font-medium">{formatAccessTokenLabel(account.auth)}</dd>
<dd className="min-w-0 break-words text-right font-medium">{formatAccessTokenLabel(account.auth)}</dd>
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center justify-between gap-2">
<dt className="text-muted-foreground">Refresh</dt>
<dd className="font-medium">{formatRefreshTokenLabel(account.auth)}</dd>
<dd className="min-w-0 break-words text-right font-medium">{formatRefreshTokenLabel(account.auth)}</dd>
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center justify-between gap-2">
<dt className="text-muted-foreground">ID token</dt>
<dd className="font-medium">{formatIdTokenLabel(account.auth)}</dd>
<dd className="min-w-0 break-words text-right font-medium">{formatIdTokenLabel(account.auth)}</dd>
</div>
</dl>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ export function AccountUsagePanel({ account, trends }: AccountUsagePanelProps) {
primaryTrendPoints.length > 0 || secondaryTrendPoints.length > 0 || secondaryScheduledTrendPoints.length > 0;

return (
<div className="space-y-4 rounded-lg border bg-muted/30 p-4">
<div className="min-w-0 space-y-4 rounded-lg border bg-muted/30 p-4">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Usage</h3>
<div className={cn("grid gap-4", weeklyOnly || monthlyOnly ? "grid-cols-1" : "grid-cols-2")}>
<div className={cn("grid gap-4", weeklyOnly || monthlyOnly ? "grid-cols-1" : "grid-cols-1 sm:grid-cols-2")}>
{monthlyOnly ? (
<QuotaRow label="Monthly" percent={monthly} resetAt={account.resetAtMonthly} />
) : (
Expand All @@ -168,7 +168,7 @@ export function AccountUsagePanel({ account, trends }: AccountUsagePanelProps) {
<div className="rounded-md border bg-background/60 px-3 py-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Request logs total</p>
{hasRequestUsage ? (
<p className="mt-1 text-xs tabular-nums text-muted-foreground">
<p className="mt-1 break-words text-xs tabular-nums text-muted-foreground">
{formatCompactNumber(requestUsage?.totalTokens)} tok | {formatCompactNumber(requestUsage?.cachedInputTokens)} cached |{" "}
{formatCompactNumber(requestUsage?.requestCount)} req | {formatCurrency(requestUsage?.totalCostUsd)}
</p>
Expand Down Expand Up @@ -212,9 +212,9 @@ export function AccountUsagePanel({ account, trends }: AccountUsagePanelProps) {
) : null}
{hasTrends && (
<div className="pt-3">
<div className="mb-2 flex items-center justify-between">
<div className="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">7-day trend</h4>
<div className="flex items-center gap-3 text-[10px] text-muted-foreground">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-muted-foreground">
<span className="flex items-center gap-1.5">
<span className="inline-block h-2 w-2 rounded-full bg-chart-1" />
5h
Expand Down
Loading