ServiceFlowPanel.tsx264 lines · main
| 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 | |
| 17 | import { PostgresFlowDetail } from './ServiceFlow/components/blocks/PostgresFlowDetail' |
| 18 | import { |
| 19 | MemoizedEdgeFunctionBlock, |
| 20 | MemoizedGoTrueBlock, |
| 21 | MemoizedNetworkBlock, |
| 22 | MemoizedPostgresBlock, |
| 23 | MemoizedPostgRESTBlock, |
| 24 | MemoizedStorageBlock, |
| 25 | } from './ServiceFlow/components/ServiceBlocks' |
| 26 | import { ServiceFlowPanelControls } from './ServiceFlow/components/ServiceFlowPanelControls' |
| 27 | import { DetailSectionHeader } from './ServiceFlow/components/shared/DetailSection' |
| 28 | import { ColumnSchema } from './UnifiedLogs.schema' |
| 29 | import { QuerySearchParamsType } from './UnifiedLogs.types' |
| 30 | import { getRowTimestampMs } from './UnifiedLogs.utils' |
| 31 | import { useDataTable } from '@/components/ui/DataTable/providers/DataTableProvider' |
| 32 | import { |
| 33 | ServiceFlowType, |
| 34 | useUnifiedLogInspectionQuery, |
| 35 | } from '@/data/logs/unified-log-inspection-query' |
| 36 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 37 | |
| 38 | interface ServiceFlowPanelProps { |
| 39 | dock: 'bottom' | 'right' |
| 40 | setDock: (value: 'bottom' | 'right') => void |
| 41 | selectedRow?: ColumnSchema |
| 42 | selectedRowKey: string |
| 43 | searchParameters: QuerySearchParamsType |
| 44 | } |
| 45 | |
| 46 | export 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 | } |