ServiceFlowPanel.tsx264 lines · main
1import { useParams } from 'common'
2import { Check, Clock, Copy } from 'lucide-react'
3import { useEffect, useRef, useState } from 'react'
4import {
5 Button,
6 copyToClipboard,
7 ResizableHandle,
8 ResizablePanel,
9 Skeleton,
10 Tabs_Shadcn_ as Tabs,
11 TabsContent_Shadcn_ as TabsContent,
12 TabsList_Shadcn_ as TabsList,
13 TabsTrigger_Shadcn_ as TabsTrigger,
14} from 'ui'
15import { CodeBlock } from 'ui-patterns/CodeBlock'
16
17import { PostgresFlowDetail } from './ServiceFlow/components/blocks/PostgresFlowDetail'
18import {
19 MemoizedEdgeFunctionBlock,
20 MemoizedGoTrueBlock,
21 MemoizedNetworkBlock,
22 MemoizedPostgresBlock,
23 MemoizedPostgRESTBlock,
24 MemoizedStorageBlock,
25} from './ServiceFlow/components/ServiceBlocks'
26import { ServiceFlowPanelControls } from './ServiceFlow/components/ServiceFlowPanelControls'
27import { DetailSectionHeader } from './ServiceFlow/components/shared/DetailSection'
28import { ColumnSchema } from './UnifiedLogs.schema'
29import { QuerySearchParamsType } from './UnifiedLogs.types'
30import { getRowTimestampMs } from './UnifiedLogs.utils'
31import { useDataTable } from '@/components/ui/DataTable/providers/DataTableProvider'
32import {
33 ServiceFlowType,
34 useUnifiedLogInspectionQuery,
35} from '@/data/logs/unified-log-inspection-query'
36import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
37
38interface ServiceFlowPanelProps {
39 dock: 'bottom' | 'right'
40 setDock: (value: 'bottom' | 'right') => void
41 selectedRow?: ColumnSchema
42 selectedRowKey: string
43 searchParameters: QuerySearchParamsType
44}
45
46export function ServiceFlowPanel({
47 dock,
48 setDock,
49 selectedRow,
50 selectedRowKey,
51 searchParameters,
52}: ServiceFlowPanelProps) {
53 const { table, filterFields } = useDataTable()
54 const { ref: projectRef } = useParams()
55 const [activeTab, setActiveTab] = useState('overview')
56 const [jsonCopied, setJsonCopied] = useState(false)
57 const overviewScrollRef = useRef<HTMLDivElement>(null)
58 const jsonScrollRef = useRef<HTMLDivElement>(null)
59
60 useEffect(() => {
61 overviewScrollRef.current?.scrollTo({ top: 0 })
62 jsonScrollRef.current?.scrollTo({ top: 0 })
63 }, [selectedRowKey])
64
65 const logType = selectedRow?.log_type
66 const serviceFlowType: ServiceFlowType | undefined =
67 logType === 'edge function' ? 'edge-function' : (logType as ServiceFlowType)
68 const shouldShowServiceFlow = !!serviceFlowType
69
70 useEffect(() => {
71 if (!shouldShowServiceFlow && activeTab === 'overview') {
72 setActiveTab('raw-json')
73 }
74 }, [shouldShowServiceFlow, activeTab])
75
76 const { logsMetadata } = useIsFeatureEnabled(['logs:metadata'])
77
78 // Query the logs API directly
79 const {
80 data: serviceFlowData,
81 isPending: isLoading,
82 error,
83 } = useUnifiedLogInspectionQuery(
84 {
85 projectRef: projectRef,
86 logId: selectedRow?.id,
87 type: serviceFlowType,
88 search: searchParameters,
89 },
90 { enabled: Boolean(selectedRow?.id) && Boolean(serviceFlowType) }
91 )
92
93 if (!selectedRowKey || !selectedRow) return null
94
95 const timestampMs = getRowTimestampMs(selectedRow)
96 const formattedTime = timestampMs ? new Date(timestampMs).toLocaleString() : null
97
98 // Prepare JSON data for Raw JSON tab
99 const jsonData =
100 shouldShowServiceFlow && serviceFlowData?.result?.[0] ? serviceFlowData.result[0] : selectedRow
101
102 const formattedJsonData =
103 !logsMetadata && 'raw_log_data' in jsonData && 'metadata' in jsonData.raw_log_data
104 ? {
105 ...jsonData,
106 raw_log_data: { ...jsonData.raw_log_data, metadata: undefined },
107 }
108 : jsonData
109
110 return (
111 <>
112 <ResizableHandle withHandle />
113 <ResizablePanel
114 id="log-sidepanel"
115 defaultSize={400}
116 minSize={dock === 'bottom' ? 300 : 400}
117 className="bg-dash-sidebar"
118 >
119 <div className="flex h-full flex-col overflow-hidden">
120 <Tabs
121 defaultValue={shouldShowServiceFlow ? 'overview' : 'raw-json'}
122 value={activeTab}
123 onValueChange={setActiveTab}
124 className="flex h-full w-full flex-col"
125 >
126 <div className="flex items-center justify-between px-4 border-b border-border">
127 <TabsList className="flex h-auto gap-x-4 rounded-none border-none!">
128 {shouldShowServiceFlow && (
129 <TabsTrigger
130 value="overview"
131 className="border-b py-3 font-mono text-xs uppercase"
132 >
133 Overview
134 </TabsTrigger>
135 )}
136 <TabsTrigger value="raw-json" className="border-b py-3 font-mono text-xs uppercase">
137 Raw JSON
138 </TabsTrigger>
139 </TabsList>
140
141 <ServiceFlowPanelControls dock={dock} setDock={setDock} />
142 </div>
143
144 {shouldShowServiceFlow && (
145 <TabsContent
146 ref={overviewScrollRef}
147 value="overview"
148 className="mt-0 grow overflow-auto py-2"
149 >
150 {error ? (
151 <div className="py-8 text-center text-destructive">Error: {error.toString()}</div>
152 ) : serviceFlowType === 'postgres' ? (
153 <PostgresFlowDetail
154 data={selectedRow}
155 enrichedData={serviceFlowData?.result?.[0]}
156 isLoading={isLoading}
157 filterFields={filterFields}
158 table={table}
159 />
160 ) : (
161 <div className="[&>*:nth-child(even)]:bg-surface-100/50">
162 <DetailSectionHeader
163 title="Request started"
164 icon={Clock}
165 summary={formattedTime ?? undefined}
166 />
167
168 <MemoizedNetworkBlock
169 data={selectedRow}
170 enrichedData={serviceFlowData?.result?.[0]}
171 isLoading={isLoading}
172 filterFields={filterFields}
173 table={table}
174 />
175
176 {serviceFlowType === 'auth' ? (
177 <MemoizedGoTrueBlock
178 data={selectedRow}
179 enrichedData={serviceFlowData?.result?.[0]}
180 isLoading={isLoading}
181 filterFields={filterFields}
182 table={table}
183 />
184 ) : serviceFlowType === 'edge-function' ? (
185 <MemoizedEdgeFunctionBlock
186 data={selectedRow}
187 enrichedData={serviceFlowData?.result?.[0]}
188 isLoading={isLoading}
189 filterFields={filterFields}
190 table={table}
191 />
192 ) : serviceFlowType === 'storage' ? (
193 <MemoizedStorageBlock
194 data={selectedRow}
195 enrichedData={serviceFlowData?.result?.[0]}
196 isLoading={isLoading}
197 filterFields={filterFields}
198 table={table}
199 />
200 ) : (
201 <>
202 <MemoizedPostgRESTBlock
203 data={selectedRow}
204 enrichedData={serviceFlowData?.result?.[0]}
205 isLoading={isLoading}
206 filterFields={filterFields}
207 table={table}
208 />
209
210 <MemoizedPostgresBlock
211 data={selectedRow}
212 enrichedData={serviceFlowData?.result?.[0]}
213 isLoading={isLoading}
214 filterFields={filterFields}
215 table={table}
216 />
217 </>
218 )}
219 </div>
220 )}
221 </TabsContent>
222 )}
223
224 <TabsContent
225 ref={jsonScrollRef}
226 value="raw-json"
227 className="mt-0 grow overflow-auto bg-surface-100/50"
228 >
229 {isLoading && shouldShowServiceFlow && (
230 <div className="flex items-center gap-3 border-b border-border bg-surface-100 p-3 text-foreground-light">
231 <Skeleton className="h-4 w-4 animate-pulse rounded-full" />
232 <span className="text-sm">Enriching log...</span>
233 </div>
234 )}
235 <div className="sticky top-2 z-10 flex justify-end px-2 -mb-9 pointer-events-none">
236 <Button
237 size="tiny"
238 type="default"
239 className="pointer-events-auto px-1.5"
240 icon={jsonCopied ? <Check size={12} /> : <Copy size={12} />}
241 onClick={() => {
242 copyToClipboard(JSON.stringify(formattedJsonData, null, 2))
243 setJsonCopied(true)
244 setTimeout(() => setJsonCopied(false), 1000)
245 }}
246 >
247 {jsonCopied ? 'Copied' : ''}
248 </Button>
249 </div>
250 <CodeBlock
251 language="json"
252 hideCopy
253 wrapperClassName="!overflow-visible bg-surface-100/50 [&_pre]:!bg-surface-100/50"
254 className="rounded-none border-none [&_code]:!leading-tight [&_pre]:!leading-tight"
255 >
256 {JSON.stringify(formattedJsonData, null, 2)}
257 </CodeBlock>
258 </TabsContent>
259 </Tabs>
260 </div>
261 </ResizablePanel>
262 </>
263 )
264}