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