| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- 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'
- 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(localStorage.getItem(storageKey) ?? 'false')
- } catch (_) {
- return false
- }
- })
- const [isCopied, setIsCopied] = useState(false)
- useEffect(() => {
- localStorage.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"
- type="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
|