Files
Ali Waseem 4205c4103d fix(studio): make detail panel close buttons clickable (#51455)
The close button on the Auth users detail panel (and the queue message
panel) is absolutely positioned, but `TabsList` is `position: relative`
and comes later in tree order — so it painted on top of the button and
swallowed every click. The button looked fine and did nothing.

Stacks the button above the tab list, adds an accessible name to the
users panel button, and covers it with an E2E test (Playwright does real
hit-testing, so it fails without the fix).

Fixes FE-4519
2026-10-08 13:43:02 -06:00

157 lines
5.4 KiB
TypeScript

import { X } from 'lucide-react'
import { parseAsString, useQueryState } from 'nuqs'
import { useState } from 'react'
import {
Button,
cn,
Input,
ResizableHandle,
ResizablePanel,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock'
import { UserLogs } from './UserLogs'
import { UserOverview } from './UserOverview'
import { PANEL_PADDING } from './Users.constants'
import { useUserQuery } from '@/data/auth/user-query'
import { User } from '@/data/auth/users-infinite-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const UserPanel = () => {
const { data: project } = useSelectedProjectQuery()
const showLogs = useIsFeatureEnabled('logs:all')
const [selectedId, setSelectedId] = useQueryState(
'show',
parseAsString.withOptions({ history: 'push', clearOnDefault: true })
)
const [view, setView] = useState<'overview' | 'raw' | 'logs'>('overview')
const [searchQuery, setSearchQuery] = useState('')
const { data: selectedUser, isPending } = useUserQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
userId: selectedId,
})
const filteredProperties = selectedUser
? Object.entries(selectedUser)
.filter(
([key, value]) =>
key.toLowerCase().includes(searchQuery.toLowerCase()) ||
(typeof value === 'string' && value.toLowerCase().includes(searchQuery.toLowerCase()))
)
.reduce((obj, [key, value]) => {
if (value !== undefined) {
obj[key as keyof User] = value as any
}
return obj
}, {} as Partial<User>)
: {}
return (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize="35" maxSize="45" minSize="35" className="bg-studio border-t">
<Button
variant="text"
className="absolute top-3 right-3 z-10 px-1"
icon={<X />}
onClick={() => setSelectedId(null)}
aria-label="Close panel"
/>
<Tabs
value={view}
className="flex flex-col h-full"
onValueChange={(value) => setView(value as 'overview' | 'raw' | 'logs')}
>
{isPending ? (
<div>
<div className="min-h-[46px] border-b" />
<div className="p-5">
<GenericSkeletonLoader />
</div>
</div>
) : !!selectedUser ? (
<>
<TabsList className="px-5 flex gap-x-4 min-h-[46px]">
<TabsTrigger
value="overview"
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent shadow-none!"
>
Overview
</TabsTrigger>
{showLogs && (
<TabsTrigger
value="logs"
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent shadow-none!"
>
Logs
</TabsTrigger>
)}
<TabsTrigger
value="raw"
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent shadow-none!"
>
Raw JSON
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className={cn('mt-0 grow min-h-0 overflow-y-auto')}>
{selectedUser && (
<UserOverview user={selectedUser} onDeleteSuccess={() => setSelectedId(null)} />
)}
</TabsContent>
{showLogs && (
<TabsContent value="logs" className={cn('mt-0 grow min-h-0 overflow-y-auto')}>
{selectedUser && <UserLogs user={selectedUser} />}
</TabsContent>
)}
<TabsContent
value="raw"
className={cn('mt-0 grow min-h-0 overflow-y-auto', PANEL_PADDING)}
>
<div className="flex items-center mb-2">
<Input
autoFocus
type="text"
placeholder="Filter..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="mr-2"
/>
<Button
variant="text"
disabled={!searchQuery}
onClick={() => setSearchQuery('')}
className="text-xs"
>
Clear
</Button>
</div>
<SimpleCodeBlock className="javascript" parentClassName="[&>*>span]:text-xs">
{JSON.stringify(filteredProperties, null, 2)}
</SimpleCodeBlock>
</TabsContent>
</>
) : (
<div className="flex items-center justify-center w-full h-full flex-col gap-y-2">
<p className="text-foreground-light text-sm">
Unable to find user with the following ID in project
</p>
<p className="text-foreground-lighter text-xs">ID: {selectedId}</p>
</div>
)}
</Tabs>
</ResizablePanel>
</>
)
}