ServiceFlowPanel.tsx 9.5 KB

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