Files
supabase/apps/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx
Gildas Garcia b30db91d71 chore: cleanup UI patterns exports (#47406)
## Problem

We now export components under a subpath in ui-patterns to avoid barrel
files as they slow down every tools (from IDE to linters, etc.) and may
also affect bundles our users have to download.

## Solution

- Remove the UI patterns index file
- Fix invalid impors
2026-06-30 09:23:17 +02:00

264 lines
8.6 KiB
TypeScript

import { safeLocalStorage } from 'common'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
copyToClipboard,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Separator,
} from 'ui'
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
import { ErrorCodeDialog } from '../ErrorCodeDialog'
import type { LogSearchCallback, PreviewLogData } from '../Logs.types'
import { ResponseCodeFormatter } from '../LogsFormatters'
import { ErrorCodeTooltip } from '@/components/ui/ErrorCodeTooltip/ErrorCodeTooltip'
import { Service } from '@/data/graphql/graphql'
import { useLogsUrlState } from '@/hooks/analytics/useLogsUrlState'
const LogRowCodeBlock = ({ value, className }: { value: string; className?: string }) => (
<pre
className={cn(
'px-1 bg-surface-300 w-full pt-1 max-w-full border-none text-xs prose-sm transition-all overflow-auto rounded-md whitespace-pre-wrap',
className
)}
>
{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
</pre>
)
const LogRowSeparator = () => <Separator className="bg-border my-1" />
const PropertyRow = ({
keyName,
value,
dataTestId,
path,
}: {
keyName: string
value: any
dataTestId?: string
path?: string
}) => {
const { setSearch } = useLogsUrlState()
const [showErrorInfo, setShowErrorInfo] = useState(false)
const service = path?.startsWith('/auth/') ? Service.Auth : undefined
const handleSearch: LogSearchCallback = async (_event: string, { query }: { query?: string }) => {
setSearch(query || '')
}
const isTimestamp =
keyName === 'timestamp' || keyName === 'created_at' || keyName === 'updated_at'
const isObject = typeof value === 'object' && value !== null
const isStatus = keyName === 'status' || keyName === 'status_code'
const isMethod = keyName === 'method'
const isSearch = keyName === 'search'
const isUserAgent = keyName === 'user_agent'
const isEventMessage = keyName === 'event_message'
const isPath = keyName === 'path'
const isErrorCode = keyName === 'error_code'
function getSearchPairs() {
if (isSearch && typeof value === 'string') {
const str = value.startsWith('?') ? value.slice(1) : value
return str.split('&').filter(Boolean)
}
return []
}
const storageKey = `log-viewer-expanded-${keyName}`
const [isExpanded, setIsExpanded] = useState(() => {
try {
// Storing in local storage so users dont have to click expand every time they change selected log
return JSON.parse(safeLocalStorage.getItem(storageKey) ?? 'false')
} catch (_) {
return false
}
})
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
safeLocalStorage.setItem(storageKey, JSON.stringify(isExpanded))
}, [isExpanded, storageKey])
const handleCopy = () => {
copyToClipboard(String(value), () => {
setIsCopied(true)
toast.success('Copied to clipboard')
})
setTimeout(() => {
setIsCopied(false)
}, 1000)
}
if (isObject || isEventMessage) {
return (
<>
<div className="flex flex-col gap-1">
<h3 className="text-foreground-lighter text-sm pl-3 py-2">{keyName}</h3>
<div>
<LogRowCodeBlock
className={cn('px-2.5', {
'max-h-[80px]': !isExpanded,
'max-h-[400px]': isExpanded,
'py-2': isEventMessage,
})}
value={value}
/>
{!isEventMessage && (
<Button
className="mt-1 w-full"
size="tiny"
variant="outline"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? 'Collapse' : 'Expand'}
</Button>
)}
</div>
</div>
<LogRowSeparator />
</>
)
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger className="group w-full" data-testid={dataTestId}>
<div className="rounded-md w-full overflow-hidden">
<div
className={cn('flex h-(--header-height) w-full', {
'flex-col gap-1.5 h-auto': isExpanded,
'items-center group-hover:bg-surface-300 gap-4': !isExpanded,
})}
>
<h3
className={cn('pl-3 text-foreground-lighter text-sm text-left', {
'h-(--header-height) flex items-center': isExpanded,
})}
>
{keyName}
</h3>
<div
className={cn('text-xs flex-1 font-mono text-foreground pr-3', {
'max-w-full text-left rounded-md p-2 bg-surface-300 text-xs w-full': isExpanded,
'truncate text-right': !isExpanded,
'text-brand-600': isCopied,
})}
>
{isExpanded ? (
<LogRowCodeBlock value={value} />
) : isTimestamp ? (
<TimestampInfo className="text-sm" utcTimestamp={value} />
) : isStatus ? (
<div className="flex items-center gap-1 justify-end">
<ResponseCodeFormatter value={value} />
</div>
) : isMethod ? (
<div className="flex items-center gap-1 justify-end">
<ResponseCodeFormatter value={value} />
</div>
) : isErrorCode ? (
<ErrorCodeTooltip errorCode={String(value)} service={service}>
<div className="truncate">{value}</div>
</ErrorCodeTooltip>
) : (
<div className="truncate">{value}</div>
)}
</div>
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{keyName === 'error_code' && (
<DropdownMenuItem
onClick={() => {
setShowErrorInfo(true)
}}
>
More information
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={handleCopy}>Copy {keyName}</DropdownMenuItem>
{!isObject && (
<DropdownMenuItem
onClick={() => {
setIsExpanded(!isExpanded)
}}
>
{isExpanded ? 'Collapse' : 'Expand'} value
</DropdownMenuItem>
)}
{(isMethod || isUserAgent || isStatus || isPath) && (
<DropdownMenuItem
onClick={() => {
handleSearch('search-input-change', { query: value })
}}
>
Search by {keyName}
</DropdownMenuItem>
)}
{isSearch
? getSearchPairs().map((pair) => (
<DropdownMenuItem
key={pair}
onClick={() => {
handleSearch('search-input-change', { query: pair })
}}
>
Search by {pair}
</DropdownMenuItem>
))
: null}
</DropdownMenuContent>
<LogRowSeparator />
</DropdownMenu>
{keyName === 'error_code' && (
<ErrorCodeDialog
open={showErrorInfo}
onOpenChange={setShowErrorInfo}
errorCode={String(value)}
service={service}
/>
)}
</>
)
}
const DefaultPreviewSelectionRenderer = ({ log }: { log: PreviewLogData }) => {
const { timestamp, event_message, metadata, id, status, ...rest } = log
const path = typeof log.path === 'string' ? log.path : undefined
const log_file = log?.metadata?.[0]?.log_file
return (
<div data-testid="log-selection" className={`p-2 flex flex-col`}>
{log?.id && (
<PropertyRow dataTestId="log-selection-id" key={'id'} keyName={'id'} value={log.id} />
)}
{log?.status && <PropertyRow key={'status'} keyName={'status'} value={log.status} />}
{log?.timestamp && (
<PropertyRow key={'timestamp'} keyName={'timestamp'} value={log.timestamp} />
)}
{Object.entries(rest).map(([key, value]) => {
return <PropertyRow key={key} keyName={key} value={value} path={path} />
})}
{log?.event_message && (
<PropertyRow key="event_message" keyName="event_message" value={log.event_message} />
)}
{!!log_file && <PropertyRow key="log_file" keyName="log_file" value={log_file} />}
{log?.metadata && <PropertyRow key="metadata" keyName="metadata" value={log.metadata} />}
</div>
)
}
export default DefaultPreviewSelectionRenderer