mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
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
157 lines
5.4 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|