fix mobile auth user details

This commit is contained in:
Danny White committed 2026-08-07 14:11:07 +10:00
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>
</>
)