LogSelection.tsx188 lines · main
1import { Check, Copy, MousePointerClick, X } from 'lucide-react'
2import { useEffect, useState } from 'react'
3import {
4 Button,
5 cn,
6 copyToClipboard,
7 Tabs_Shadcn_,
8 TabsContent_Shadcn_,
9 TabsList_Shadcn_,
10 TabsTrigger_Shadcn_,
11} from 'ui'
12import { CodeBlock } from 'ui-patterns/CodeBlock'
13import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
14
15import type { LogData, QueryType } from './Logs.types'
16import { apiKey, role as extractRole, jwtAPIKey } from './Logs.utils'
17import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer'
18import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
19
20export interface LogSelectionProps {
21 log?: LogData
22 onClose: () => void
23 queryType?: QueryType
24 projectRef: string
25 isLoading: boolean
26 error?: string | object
27}
28
29const 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
151export default LogSelection
152
153function 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
186function LogErrorState({ error }: { error?: string | object }) {
187 return <pre>{JSON.stringify(error, null, 2)}</pre>
188}