LogSelection.tsx188 lines · main
| 1 | import { Check, Copy, MousePointerClick, X } from 'lucide-react' |
| 2 | import { useEffect, useState } from 'react' |
| 3 | import { |
| 4 | Button, |
| 5 | cn, |
| 6 | copyToClipboard, |
| 7 | Tabs_Shadcn_, |
| 8 | TabsContent_Shadcn_, |
| 9 | TabsList_Shadcn_, |
| 10 | TabsTrigger_Shadcn_, |
| 11 | } from 'ui' |
| 12 | import { CodeBlock } from 'ui-patterns/CodeBlock' |
| 13 | import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' |
| 14 | |
| 15 | import type { LogData, QueryType } from './Logs.types' |
| 16 | import { apiKey, role as extractRole, jwtAPIKey } from './Logs.utils' |
| 17 | import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer' |
| 18 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 19 | |
| 20 | export interface LogSelectionProps { |
| 21 | log?: LogData |
| 22 | onClose: () => void |
| 23 | queryType?: QueryType |
| 24 | projectRef: string |
| 25 | isLoading: boolean |
| 26 | error?: string | object |
| 27 | } |
| 28 | |
| 29 | const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectionProps) => { |
| 30 | const [showCopied, setShowCopied] = useState(false) |
| 31 | |
| 32 | useEffect(() => { |
| 33 | if (!showCopied) return |
| 34 | const timer = setTimeout(() => setShowCopied(false), 2000) |
| 35 | return () => clearTimeout(timer) |
| 36 | }, [showCopied]) |
| 37 | |
| 38 | const LogDetails = () => { |
| 39 | if (error) return <LogErrorState error={error} /> |
| 40 | if (!log) return <LogDetailEmptyState /> |
| 41 | |
| 42 | switch (queryType) { |
| 43 | case 'api': |
| 44 | const status = log?.metadata?.[0]?.response?.[0]?.status_code |
| 45 | const method = log?.metadata?.[0]?.request?.[0]?.method |
| 46 | const path = log?.metadata?.[0]?.request?.[0]?.path |
| 47 | const search = log?.metadata?.[0]?.request?.[0]?.search |
| 48 | const user_agent = log?.metadata?.[0]?.request?.[0]?.headers[0].user_agent |
| 49 | const error_code = log?.metadata?.[0]?.response?.[0]?.headers?.[0]?.x_sb_error_code |
| 50 | const apikey = jwtAPIKey(log?.metadata) ?? apiKey(log?.metadata) |
| 51 | const role = extractRole(log?.metadata) |
| 52 | |
| 53 | const { id, metadata, timestamp, event_message, ...rest } = log |
| 54 | |
| 55 | const apiLog = { |
| 56 | id, |
| 57 | status, |
| 58 | method, |
| 59 | path, |
| 60 | search, |
| 61 | user_agent, |
| 62 | timestamp, |
| 63 | event_message, |
| 64 | metadata, |
| 65 | ...(apikey ? { apikey } : null), |
| 66 | ...(error_code ? { error_code } : null), |
| 67 | ...(role ? { role } : null), |
| 68 | ...rest, |
| 69 | } |
| 70 | |
| 71 | return <DefaultPreviewSelectionRenderer log={apiLog} /> |
| 72 | |
| 73 | case 'database': |
| 74 | const hint = log?.metadata?.[0]?.parsed?.[0]?.hint |
| 75 | const detail = log?.metadata?.[0]?.parsed?.[0]?.detail |
| 76 | const query = log?.metadata?.[0]?.parsed?.[0]?.query |
| 77 | const postgresLog = { |
| 78 | ...(hint && { hint }), |
| 79 | ...(detail && { detail }), |
| 80 | ...(query && { query }), |
| 81 | ...log, |
| 82 | } |
| 83 | return <DefaultPreviewSelectionRenderer log={postgresLog} /> |
| 84 | default: |
| 85 | return <DefaultPreviewSelectionRenderer log={log} /> |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | return ( |
| 90 | <div className="relative flex h-full grow flex-col overflow-y-scroll bg-surface-100 border-t"> |
| 91 | <div className="relative grow flex flex-col h-full"> |
| 92 | <Tabs_Shadcn_ defaultValue="details" className="flex flex-col h-full"> |
| 93 | <TabsList_Shadcn_ className="px-2 pt-2 relative"> |
| 94 | <TabsTrigger_Shadcn_ className="px-3" value="details"> |
| 95 | Details |
| 96 | </TabsTrigger_Shadcn_> |
| 97 | <TabsTrigger_Shadcn_ disabled={!log} className="px-3" value="raw"> |
| 98 | Raw |
| 99 | </TabsTrigger_Shadcn_> |
| 100 | |
| 101 | <div className="*:px-1.5 *:text-foreground-lighter ml-auto flex gap-1 absolute right-2 top-2"> |
| 102 | <ButtonTooltip |
| 103 | disabled={!log || isLoading} |
| 104 | type="text" |
| 105 | tooltip={{ |
| 106 | content: { |
| 107 | side: 'left', |
| 108 | text: isLoading ? 'Loading log...' : 'Copy as JSON', |
| 109 | }, |
| 110 | }} |
| 111 | icon={showCopied ? <Check /> : <Copy />} |
| 112 | onClick={() => { |
| 113 | setShowCopied(true) |
| 114 | copyToClipboard(JSON.stringify(log, null, 2)) |
| 115 | }} |
| 116 | /> |
| 117 | |
| 118 | <Button type="text" onClick={onClose}> |
| 119 | <X size={14} strokeWidth={2} /> |
| 120 | </Button> |
| 121 | </div> |
| 122 | </TabsList_Shadcn_> |
| 123 | <div className="flex-1 h-full"> |
| 124 | {isLoading ? ( |
| 125 | <div className="p-4"> |
| 126 | <GenericSkeletonLoader /> |
| 127 | </div> |
| 128 | ) : ( |
| 129 | <> |
| 130 | <TabsContent_Shadcn_ className="space-y-6 h-full" value="details"> |
| 131 | <LogDetails /> |
| 132 | </TabsContent_Shadcn_> |
| 133 | <TabsContent_Shadcn_ value="raw"> |
| 134 | <CodeBlock |
| 135 | hideLineNumbers |
| 136 | language="json" |
| 137 | className="prose w-full pt-0 max-w-full border-none" |
| 138 | > |
| 139 | {JSON.stringify(log, null, 2)} |
| 140 | </CodeBlock> |
| 141 | </TabsContent_Shadcn_> |
| 142 | </> |
| 143 | )} |
| 144 | </div> |
| 145 | </Tabs_Shadcn_> |
| 146 | </div> |
| 147 | </div> |
| 148 | ) |
| 149 | } |
| 150 | |
| 151 | export default LogSelection |
| 152 | |
| 153 | function LogDetailEmptyState({ |
| 154 | title = 'Select an Event', |
| 155 | message = 'Select an Event to view the complete JSON payload', |
| 156 | }: { |
| 157 | title?: string |
| 158 | message?: string |
| 159 | }) { |
| 160 | return ( |
| 161 | <div |
| 162 | className={cn( |
| 163 | 'flex h-full w-full flex-col items-center justify-center gap-2 overflow-y-scroll text-center transition-all px-4' |
| 164 | )} |
| 165 | > |
| 166 | <div |
| 167 | className={cn( |
| 168 | 'flex w-full max-w-sm flex-col items-center justify-center gap-6 text-center transition-all delay-300 duration-500' |
| 169 | )} |
| 170 | > |
| 171 | <div className="relative flex h-4 w-32 items-center rounded-sm border border-control px-2"> |
| 172 | <div className="h-0.5 w-2/3 rounded-full bg-surface-300"></div> |
| 173 | <div className="absolute right-1 -bottom-4"> |
| 174 | <MousePointerClick size="24" strokeWidth={1} /> |
| 175 | </div> |
| 176 | </div> |
| 177 | <div className="flex flex-col gap-1"> |
| 178 | <h3 className="text-sm text-foreground">{title}</h3> |
| 179 | <p className="text-xs text-foreground-lighter">{message}</p> |
| 180 | </div> |
| 181 | </div> |
| 182 | </div> |
| 183 | ) |
| 184 | } |
| 185 | |
| 186 | function LogErrorState({ error }: { error?: string | object }) { |
| 187 | return <pre>{JSON.stringify(error, null, 2)}</pre> |
| 188 | } |