mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
fix mobile auth user details
This commit is contained in:
1 parent
91a394fc83
commit
32f4fc692b
4 files changed
+170
-108
No files matched your search
@@ -16,26 +16,28 @@ export const UserHeader = ({ user }: { user: User }) => {
|
||||
return (
|
||||
<div className={cn(PANEL_PADDING)}>
|
||||
{isPhoneAuth ? (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p>{user.phone}</p>
|
||||
<div className="flex min-w-0 items-start gap-x-1">
|
||||
<p className="min-w-0 [overflow-wrap:anywhere]">{user.phone}</p>
|
||||
<CopyButton
|
||||
iconOnly
|
||||
variant="text"
|
||||
icon={<Copy />}
|
||||
className="px-1"
|
||||
className="shrink-0 px-1"
|
||||
text={user?.phone ?? ''}
|
||||
/>
|
||||
</div>
|
||||
) : isAnonUser ? (
|
||||
<>
|
||||
<p>Anonymous user</p>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p className="text-foreground-light text-sm">{user.id}</p>
|
||||
<div className="flex min-w-0 items-start gap-x-1">
|
||||
<p className="text-foreground-light min-w-0 text-sm [overflow-wrap:anywhere]">
|
||||
{user.id}
|
||||
</p>
|
||||
<CopyButton
|
||||
iconOnly
|
||||
variant="text"
|
||||
icon={<Copy />}
|
||||
className="px-1"
|
||||
className="shrink-0 px-1"
|
||||
text={user?.id ?? ''}
|
||||
/>
|
||||
</div>
|
||||
@@ -43,15 +45,20 @@ export const UserHeader = ({ user }: { user: User }) => {
|
||||
) : (
|
||||
<>
|
||||
{hasDisplayName && <p>{displayName}</p>}
|
||||
<div className="flex items-center gap-x-1">
|
||||
<p className={cn(hasDisplayName ? 'text-foreground-light text-sm' : 'text-foreground')}>
|
||||
<div className="flex min-w-0 items-start gap-x-1">
|
||||
<p
|
||||
className={cn(
|
||||
'min-w-0 [overflow-wrap:anywhere]',
|
||||
hasDisplayName ? 'text-foreground-light text-sm' : 'text-foreground'
|
||||
)}
|
||||
>
|
||||
{user.email}
|
||||
</p>
|
||||
<CopyButton
|
||||
iconOnly
|
||||
variant="text"
|
||||
icon={<Copy />}
|
||||
className="px-1"
|
||||
className="shrink-0 px-1"
|
||||
text={user?.email ?? ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,9 @@ import { timeout } from '@/lib/helpers'
|
||||
|
||||
const DATE_FORMAT = 'DD MMM, YYYY HH:mm'
|
||||
const CONTAINER_CLASS = cn(
|
||||
'bg-surface-100 border-default text-foreground flex items-center justify-between',
|
||||
'gap-x-4 border px-5 py-4 text-sm first:rounded-tr first:rounded-tl last:rounded-br last:rounded-bl'
|
||||
'bg-surface-100 border-default text-foreground flex flex-col items-stretch justify-between',
|
||||
'gap-y-3 border px-5 py-4 text-sm sm:flex-row sm:items-center sm:gap-x-4 sm:gap-y-0',
|
||||
'first:rounded-tr first:rounded-tl last:rounded-br last:rounded-bl'
|
||||
)
|
||||
|
||||
interface UserOverviewProps {
|
||||
@@ -485,10 +486,10 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
|
||||
export const RowData = ({ property, value }: { property: string; value?: string | boolean }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-x-2 group justify-between">
|
||||
<p className=" text-foreground-lighter text-xs">{property}</p>
|
||||
<div className="group flex items-start justify-between gap-x-2">
|
||||
<p className="text-foreground-lighter shrink-0 py-1.5 text-xs">{property}</p>
|
||||
{typeof value === 'boolean' ? (
|
||||
<div className="h-[26px] flex items-center justify-center min-w-[70px]">
|
||||
<div className="flex min-h-[26px] min-w-[70px] items-center justify-center">
|
||||
{value ? (
|
||||
<div className="rounded-full w-4 h-4 dark:bg-white bg-black flex items-center justify-center">
|
||||
<Check size={10} className="text-contrast" strokeWidth={4} />
|
||||
@@ -500,14 +501,16 @@ export const RowData = ({ property, value }: { property: string; value?: string
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-x-2 h-[26px] font-mono min-w-[40px]">
|
||||
<p className="text-xs">{!value ? '-' : value}</p>
|
||||
<div className="flex min-h-[26px] min-w-0 items-start justify-end gap-x-2 py-1.5 font-mono">
|
||||
<p className="min-w-0 text-right text-xs [overflow-wrap:anywhere]">
|
||||
{!value ? '-' : value}
|
||||
</p>
|
||||
{!!value && (
|
||||
<CopyButton
|
||||
iconOnly
|
||||
variant="text"
|
||||
icon={<Copy />}
|
||||
className="transition opacity-0 group-hover:opacity-100 px-1"
|
||||
className="shrink-0 px-1 opacity-100 transition sm:opacity-0 sm:group-hover:opacity-100"
|
||||
text={value}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { ResizablePanelGroup } from 'ui'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { UserPanel } from './UserPanel'
|
||||
import type { User } from '@/data/auth/users-infinite-query'
|
||||
@@ -69,10 +69,16 @@ const renderPanel = (disabledFeatures: string[] = []) => {
|
||||
}
|
||||
|
||||
describe('UserPanel', () => {
|
||||
const desktopWidth = window.innerWidth
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: desktopWidth })
|
||||
})
|
||||
|
||||
it('shows the Logs tab when logs:all is enabled', async () => {
|
||||
renderPanel([])
|
||||
|
||||
@@ -88,4 +94,14 @@ describe('UserPanel', () => {
|
||||
expect(screen.queryByRole('tab', { name: 'Logs' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('tab', { name: 'Raw JSON' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('uses a full-width dialog on mobile', async () => {
|
||||
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 390 })
|
||||
|
||||
renderPanel([])
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: 'User details' })
|
||||
expect(dialog).toHaveClass('w-screen')
|
||||
expect(screen.getByRole('button', { name: 'Close user details' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { X } from 'lucide-react'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { Dialog, VisuallyHidden } from 'radix-ui'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
@@ -7,10 +8,12 @@ import {
|
||||
Input,
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
SidePanel,
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
useIsMobile,
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { SimpleCodeBlock } from 'ui-patterns/SimpleCodeBlock'
|
||||
@@ -26,6 +29,7 @@ import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
export const UserPanel = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const showLogs = useIsFeatureEnabled('logs:all')
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const [selectedId, setSelectedId] = useQueryState(
|
||||
'show',
|
||||
@@ -56,99 +60,131 @@ export const UserPanel = () => {
|
||||
}, {} as Partial<User>)
|
||||
: {}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Button
|
||||
variant="text"
|
||||
className="absolute top-3 right-3 z-10 px-1"
|
||||
icon={<X />}
|
||||
aria-label="Close user details"
|
||||
onClick={() => setSelectedId(null)}
|
||||
/>
|
||||
<Tabs
|
||||
value={view}
|
||||
className="flex h-full flex-col"
|
||||
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="flex min-h-[46px] gap-x-4 px-5 pr-12">
|
||||
<TabsTrigger
|
||||
value="overview"
|
||||
className="h-full px-0 pb-0 text-xs shadow-none! data-[state=active]:bg-transparent"
|
||||
>
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
{showLogs && (
|
||||
<TabsTrigger
|
||||
value="logs"
|
||||
className="h-full px-0 pb-0 text-xs shadow-none! data-[state=active]:bg-transparent"
|
||||
>
|
||||
Logs
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger
|
||||
value="raw"
|
||||
className="h-full px-0 pb-0 text-xs shadow-none! data-[state=active]:bg-transparent"
|
||||
>
|
||||
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="mb-2 flex items-center">
|
||||
<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 h-full w-full flex-col items-center justify-center gap-y-2 px-5 text-center">
|
||||
<p className="text-foreground-light text-sm">
|
||||
Unable to find user with the following ID in project
|
||||
</p>
|
||||
<p className="text-foreground-lighter break-all text-xs">ID: {selectedId}</p>
|
||||
</div>
|
||||
)}
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<SidePanel
|
||||
visible
|
||||
hideFooter
|
||||
size="large"
|
||||
aria-label="User details"
|
||||
aria-describedby={undefined}
|
||||
className="shadow-none"
|
||||
onCancel={() => setSelectedId(null)}
|
||||
>
|
||||
<VisuallyHidden.VisuallyHidden>
|
||||
<Dialog.Title>User details</Dialog.Title>
|
||||
</VisuallyHidden.VisuallyHidden>
|
||||
{content}
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize="35" maxSize="45" minSize="35" className="bg-studio border-t">
|
||||
<Button
|
||||
variant="text"
|
||||
className="absolute top-3 right-3 px-1"
|
||||
icon={<X />}
|
||||
onClick={() => setSelectedId(null)}
|
||||
/>
|
||||
<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
|
||||
defaultSize="35"
|
||||
maxSize="45"
|
||||
minSize="35"
|
||||
aria-label="User details"
|
||||
className="bg-studio border-t"
|
||||
>
|
||||
{content}
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user