LogTable.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { ContextMenuContent } from '@ui/components/shadcn/ui/context-menu'
  3. import { IS_PLATFORM, useParams } from 'common'
  4. import { Copy, Eye, EyeOff, Play } from 'lucide-react'
  5. import { Key, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
  6. import DataGrid, { Column, RenderRowProps, Row } from 'react-data-grid'
  7. import { toast } from 'sonner'
  8. import {
  9. Button,
  10. Checkbox,
  11. cn,
  12. ContextMenu,
  13. ContextMenuItem,
  14. ContextMenuTrigger,
  15. copyToClipboard,
  16. ResizableHandle,
  17. ResizablePanel,
  18. ResizablePanelGroup,
  19. } from 'ui'
  20. import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer'
  21. import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
  22. import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
  23. import DefaultPreviewColumnRenderer from './LogColumnRenderers/DefaultPreviewColumnRenderer'
  24. import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender'
  25. import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
  26. import type { LogData, LogQueryError, QueryType } from './Logs.types'
  27. import {
  28. formatLogsAsCsv,
  29. formatLogsAsJson,
  30. formatLogsAsMarkdown,
  31. isDefaultLogPreviewFormat,
  32. } from './Logs.utils'
  33. import LogSelection from './LogSelection'
  34. import { DefaultErrorRenderer } from './LogsErrorRenderers/DefaultErrorRenderer'
  35. import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer'
  36. import { LogsTableEmptyState } from './LogsTableEmptyState'
  37. import { MultiSelectActionBar, type LogCopyFormat } from './MultiSelectActionBar'
  38. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  39. import { DownloadResultsButton } from '@/components/ui/DownloadResultsButton'
  40. import { useSelectedLog } from '@/hooks/analytics/useSelectedLog'
  41. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  42. import { useProfile } from '@/lib/profile'
  43. import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
  44. import { useShortcut } from '@/state/shortcuts/useShortcut'
  45. import type { ResponseError } from '@/types'
  46. interface Props {
  47. data?: LogData[]
  48. onHistogramToggle?: () => void
  49. isHistogramShowing?: boolean
  50. isLoading?: boolean
  51. isSaving?: boolean
  52. error?: LogQueryError | null
  53. showDownload?: boolean
  54. queryType?: QueryType
  55. projectRef: string
  56. onRun?: () => void
  57. onSave?: () => void
  58. hasEditorValue?: boolean
  59. className?: string
  60. EmptyState?: ReactNode
  61. showHeader?: boolean
  62. showHistogramToggle?: boolean
  63. selectedLog?: LogData
  64. isSelectedLogLoading?: boolean
  65. selectedLogError?: LogQueryError | ResponseError
  66. onSelectedLogChange?: (log: LogData | null) => void
  67. sqlQuery?: string
  68. }
  69. type LogMap = { [id: string]: LogData }
  70. /**
  71. * Logs table view with focus side panel
  72. *
  73. * When in custom data display mode, the side panel will not open when focusing on logs.
  74. */
  75. export const LogTable = ({
  76. data = [],
  77. queryType,
  78. onHistogramToggle,
  79. isHistogramShowing,
  80. isLoading,
  81. isSaving,
  82. error,
  83. projectRef,
  84. onRun,
  85. onSave,
  86. hasEditorValue,
  87. className,
  88. EmptyState,
  89. showHeader = true,
  90. showHistogramToggle = true,
  91. selectedLog,
  92. isSelectedLogLoading,
  93. selectedLogError,
  94. onSelectedLogChange,
  95. sqlQuery,
  96. }: Props) => {
  97. const { ref } = useParams()
  98. const { profile } = useProfile()
  99. const [selectedLogId] = useSelectedLog()
  100. const [selectedRow, setSelectedRow] = useState<LogData | null>(null)
  101. const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set())
  102. const [copiedFormat, setCopiedFormat] = useState<LogCopyFormat | null>(null)
  103. const triggerRef = useRef<HTMLDivElement>(null)
  104. const [activeRow, setActiveRow] = useState<LogData | null>(null)
  105. const [contextMenuKey, setContextMenuKey] = useState(0)
  106. const handleRowContextMenu = useCallback((e: React.MouseEvent, row: LogData) => {
  107. e.preventDefault()
  108. setActiveRow(row)
  109. // Force re-render of ContextMenuContent to update the current position.
  110. setContextMenuKey((prev) => prev + 1)
  111. const trigger = triggerRef.current
  112. if (!trigger) return
  113. trigger.style.left = `${e.clientX}px`
  114. trigger.style.top = `${e.clientY}px`
  115. trigger.dispatchEvent(
  116. new MouseEvent('contextmenu', {
  117. bubbles: true,
  118. clientX: e.clientX,
  119. clientY: e.clientY,
  120. })
  121. )
  122. }, [])
  123. const { can: canCreateLogQuery } = useAsyncCheckPermissions(
  124. PermissionAction.CREATE,
  125. 'user_content',
  126. {
  127. resource: { type: 'log_sql', owner_id: profile?.id },
  128. subject: { id: profile?.id },
  129. }
  130. )
  131. const firstRow = data[0]
  132. function getFirstRow() {
  133. if (!firstRow) return {}
  134. const { timestamp, ...rest } = firstRow
  135. if (!timestamp) return firstRow
  136. return { timestamp, ...rest }
  137. }
  138. const columnNames = Object.keys(getFirstRow() || {})
  139. const hasId = columnNames.includes('id')
  140. const hasTimestamp = columnNames.includes('timestamp')
  141. const panelContentMinSize = 40
  142. const panelContentMaxSize = 60
  143. const getRowKey = useCallback(
  144. (row: LogData): string => {
  145. if (!hasId) return JSON.stringify(row)
  146. return (row as LogData).id
  147. },
  148. [hasId]
  149. )
  150. const [dedupedData, logMap] = useMemo<[LogData[], LogMap]>(() => {
  151. const deduped = [...new Set(data)] as LogData[]
  152. if (!hasId) return [deduped, {}]
  153. const map = deduped.reduce((acc: LogMap, d: LogData) => {
  154. acc[d.id] = d
  155. return acc
  156. }, {})
  157. return [deduped, map]
  158. }, [data, hasId])
  159. const logDataRows = useMemo(() => {
  160. if (hasId && hasTimestamp) {
  161. return Object.values(logMap).sort((a, b) => b.timestamp - a.timestamp)
  162. } else {
  163. return dedupedData
  164. }
  165. }, [dedupedData, hasId, hasTimestamp, logMap])
  166. // Side panel is open only when a single row is selected via regular click (not multi-select)
  167. const selectionOpen = Boolean((selectedLog || isSelectedLogLoading) && selectedRows.size === 0)
  168. const selectedRowsData = useMemo(
  169. () => logDataRows.filter((r) => selectedRows.has(getRowKey(r))),
  170. [logDataRows, selectedRows, getRowKey]
  171. )
  172. const checkboxColumn: Column<LogData> = {
  173. key: 'multi-select',
  174. name: '',
  175. width: 32,
  176. maxWidth: 32,
  177. minWidth: 32,
  178. renderCell: ({ row }) => {
  179. const key = getRowKey(row)
  180. const toggle = () => {
  181. const next = new Set(selectedRows)
  182. if (next.has(key)) {
  183. next.delete(key)
  184. } else {
  185. next.add(key)
  186. }
  187. setSelectedRows(next)
  188. if (next.size > 0) {
  189. setSelectedRow(null)
  190. onSelectedLogChange?.(null)
  191. }
  192. }
  193. return (
  194. <div
  195. className="absolute group inset-0 flex justify-center px-2 items-center cursor-pointer"
  196. onClick={(e) => {
  197. e.stopPropagation()
  198. toggle()
  199. }}
  200. >
  201. <Checkbox
  202. className="group-hover:border-foreground-muted"
  203. checked={selectedRows.has(key)}
  204. onClick={(e: React.MouseEvent) => e.stopPropagation()}
  205. onCheckedChange={toggle}
  206. />
  207. </div>
  208. )
  209. },
  210. }
  211. const DEFAULT_COLUMNS = columnNames.map((v: keyof LogData, idx) => {
  212. const column = `logs-column-${idx}`
  213. const result: Column<LogData> = {
  214. key: column,
  215. name: v as string,
  216. resizable: true,
  217. renderCell: ({ row }) => {
  218. return <span>{formatCellValue(row?.[v])}</span>
  219. },
  220. renderHeaderCell: () => {
  221. return <div className="flex items-center">{v}</div>
  222. },
  223. minWidth: 128,
  224. }
  225. return result
  226. })
  227. let columns = DEFAULT_COLUMNS
  228. if (!queryType) {
  229. columns
  230. } else {
  231. switch (queryType) {
  232. case 'api':
  233. columns = DatabaseApiColumnRender
  234. break
  235. case 'database':
  236. columns = DatabasePostgresColumnRender
  237. break
  238. case 'fn_edge':
  239. columns = FunctionsEdgeColumnRender
  240. break
  241. case 'functions':
  242. columns = FunctionsLogsColumnRender
  243. break
  244. case 'auth':
  245. columns = AuthColumnRenderer
  246. break
  247. case 'pg_cron':
  248. columns = DatabasePostgresColumnRender
  249. break
  250. default:
  251. if (firstRow && isDefaultLogPreviewFormat(firstRow)) {
  252. columns = DefaultPreviewColumnRenderer
  253. } else {
  254. columns = DEFAULT_COLUMNS
  255. }
  256. break
  257. }
  258. }
  259. if (columns.length > 0) {
  260. columns = [checkboxColumn, ...columns]
  261. }
  262. const onRowClick = useCallback(
  263. (row: LogData) => {
  264. // Regular single click — clear multi-select, open side panel
  265. setSelectedRows(new Set())
  266. setSelectedRow(row)
  267. onSelectedLogChange?.(row)
  268. },
  269. [onSelectedLogChange]
  270. )
  271. const RowRenderer = useCallback<(key: Key, props: RenderRowProps<LogData, unknown>) => ReactNode>(
  272. (key, props) => {
  273. const handleClick = (e: React.MouseEvent) => {
  274. // Check if clicking on the checkbox column - let that handler handle it
  275. const target = e.target as HTMLElement
  276. if (target.closest('[data-column-key="multi-select"]')) return
  277. onRowClick(props.row)
  278. }
  279. return (
  280. <Row
  281. key={key}
  282. {...props}
  283. isRowSelected={false}
  284. selectedCellIdx={undefined}
  285. onClick={handleClick}
  286. onContextMenu={(e) => handleRowContextMenu(e, props.row)}
  287. />
  288. )
  289. },
  290. [handleRowContextMenu, onRowClick]
  291. )
  292. const formatCellValue = (value: any) => {
  293. return value && typeof value === 'object'
  294. ? JSON.stringify(value)
  295. : value === null
  296. ? 'NULL'
  297. : String(value)
  298. }
  299. // Arrow-key navigation. Unlike mouse-click (`onRowClick`), keyboard nav must
  300. // preserve any existing multi-select checkmarks — clearing `selectedRows`
  301. // here would wipe the user's checked rows the moment they press an arrow.
  302. const navigate = (direction: 'down' | 'up') => {
  303. if (logDataRows.length === 0) return
  304. const focusRow = (row: LogData) => {
  305. setSelectedRow(row)
  306. onSelectedLogChange?.(row)
  307. }
  308. if (!selectedRow) {
  309. focusRow(logDataRows[0])
  310. return
  311. }
  312. const selectedKey = getRowKey(selectedRow)
  313. const currentIdx = logDataRows.findIndex((row) => getRowKey(row) === selectedKey)
  314. if (currentIdx === -1) {
  315. focusRow(logDataRows[0])
  316. return
  317. }
  318. if (direction === 'down' && currentIdx < logDataRows.length - 1) {
  319. focusRow(logDataRows[currentIdx + 1])
  320. } else if (direction === 'up' && currentIdx > 0) {
  321. focusRow(logDataRows[currentIdx - 1])
  322. }
  323. }
  324. useShortcut(SHORTCUT_IDS.LOGS_PREVIEW_START_NAV_DOWN, () => navigate('down'), {
  325. enabled: logDataRows.length > 0,
  326. })
  327. useShortcut(SHORTCUT_IDS.LOGS_PREVIEW_START_NAV_UP, () => navigate('up'), {
  328. enabled: logDataRows.length > 0,
  329. })
  330. useShortcut(
  331. SHORTCUT_IDS.LOGS_PREVIEW_TOGGLE_ALL_SELECTION,
  332. () => {
  333. if (selectedRows.size === logDataRows.length) {
  334. setSelectedRows(new Set())
  335. } else {
  336. setSelectedRows(new Set(logDataRows.map((row) => getRowKey(row))))
  337. setSelectedRow(null)
  338. onSelectedLogChange?.(null)
  339. }
  340. },
  341. { enabled: logDataRows.length > 0 }
  342. )
  343. useShortcut(
  344. SHORTCUT_IDS.LOGS_PREVIEW_TOGGLE_ROW_SELECTION,
  345. () => {
  346. if (!selectedRow) return
  347. const key = getRowKey(selectedRow)
  348. const next = new Set(selectedRows)
  349. if (next.has(key)) {
  350. next.delete(key)
  351. } else {
  352. next.add(key)
  353. }
  354. setSelectedRows(next)
  355. },
  356. { enabled: selectedRow !== null }
  357. )
  358. useShortcut(
  359. SHORTCUT_IDS.LOGS_PREVIEW_CLOSE_PANEL,
  360. () => {
  361. onSelectedLogChange?.(null)
  362. setSelectedRow(null)
  363. },
  364. { enabled: selectionOpen }
  365. )
  366. useShortcut(
  367. SHORTCUT_IDS.LOGS_PREVIEW_EXIT_SELECTION,
  368. () => {
  369. setSelectedRows(new Set())
  370. ;(document.activeElement as HTMLElement | null)?.blur()
  371. },
  372. { enabled: !selectionOpen && selectedRows.size > 0 }
  373. )
  374. useEffect(() => {
  375. if (!isSelectedLogLoading && !selectedLog) {
  376. setSelectedRow(null)
  377. }
  378. }, [selectedLog, isSelectedLogLoading])
  379. useEffect(() => {
  380. if (!isLoading && !selectedRow) {
  381. const logData = data.find((x) => x.id === selectedLogId)
  382. if (logData) setSelectedRow(logData)
  383. }
  384. }, [isLoading, data, selectedRow, selectedLogId])
  385. // Clear multi-select when a new query starts loading
  386. useEffect(() => {
  387. if (isLoading) {
  388. setSelectedRows(new Set())
  389. }
  390. }, [isLoading])
  391. // Copy feedback timeout
  392. useEffect(() => {
  393. if (!copiedFormat) return
  394. const timer = setTimeout(() => setCopiedFormat(null), 2000)
  395. return () => clearTimeout(timer)
  396. }, [copiedFormat])
  397. function handleCopySelectedRows(format: LogCopyFormat) {
  398. let text = ''
  399. if (format === 'json') text = formatLogsAsJson(selectedRowsData)
  400. if (format === 'markdown') text = formatLogsAsMarkdown(selectedRowsData)
  401. if (format === 'csv') text = formatLogsAsCsv(selectedRowsData)
  402. copyToClipboard(text, () => {
  403. setCopiedFormat(format)
  404. toast.success(
  405. `Copied ${selectedRowsData.length} log${selectedRowsData.length !== 1 ? 's' : ''} as ${format.toUpperCase()}`
  406. )
  407. })
  408. }
  409. useShortcut(SHORTCUT_IDS.RESULTS_COPY_JSON, () => handleCopySelectedRows('json'), {
  410. enabled: selectedRowsData.length > 0,
  411. conflictBehavior: 'allow',
  412. })
  413. useShortcut(SHORTCUT_IDS.RESULTS_COPY_MARKDOWN, () => handleCopySelectedRows('markdown'), {
  414. enabled: selectedRowsData.length > 0,
  415. conflictBehavior: 'allow',
  416. })
  417. useShortcut(SHORTCUT_IDS.RESULTS_COPY_CSV, () => handleCopySelectedRows('csv'), {
  418. enabled: selectedRowsData.length > 0,
  419. conflictBehavior: 'allow',
  420. })
  421. const logsExplorerTableHeader = (
  422. <div
  423. className={cn(
  424. 'flex w-full items-center justify-between border-t bg-surface-100 px-5 py-2',
  425. className,
  426. { hidden: !showHeader }
  427. )}
  428. >
  429. <div className="flex items-center gap-2">
  430. <DownloadResultsButton
  431. type="text"
  432. text={`Results ${data && data.length ? `(${data.length})` : ''}`}
  433. results={data}
  434. fileName={`briven-logs-${ref}.csv`}
  435. enableCopyShortcuts={selectedRowsData.length === 0}
  436. />
  437. </div>
  438. {showHistogramToggle && (
  439. <div className="flex items-center gap-2">
  440. <Button
  441. type="default"
  442. icon={isHistogramShowing ? <Eye /> : <EyeOff />}
  443. onClick={onHistogramToggle}
  444. >
  445. Histogram
  446. </Button>
  447. </div>
  448. )}
  449. <div className="space-x-2">
  450. {IS_PLATFORM && (
  451. <ButtonTooltip
  452. type="default"
  453. onClick={onSave}
  454. loading={isSaving}
  455. disabled={!canCreateLogQuery || !hasEditorValue}
  456. tooltip={{
  457. content: {
  458. side: 'bottom',
  459. text: !canCreateLogQuery
  460. ? 'You need additional permissions to save your query'
  461. : undefined,
  462. },
  463. }}
  464. >
  465. Save query
  466. </ButtonTooltip>
  467. )}
  468. <Button
  469. title="run-logs-query"
  470. type={hasEditorValue ? 'primary' : 'alternative'}
  471. disabled={!hasEditorValue}
  472. onClick={onRun}
  473. iconRight={<Play size={12} />}
  474. loading={isLoading}
  475. >
  476. Run
  477. </Button>
  478. </div>
  479. </div>
  480. )
  481. const renderErrorAlert = () => {
  482. if (!error) return null
  483. const childProps = {
  484. isCustomQuery: queryType ? false : true,
  485. error: error!,
  486. }
  487. if (
  488. typeof error === 'object' &&
  489. error.error?.errors.find((err) => err.reason === 'resourcesExceeded')
  490. ) {
  491. return <ResourcesExceededErrorRenderer {...childProps} />
  492. }
  493. return (
  494. <div className="text-foreground flex gap-2 font-mono p-4">
  495. <DefaultErrorRenderer {...childProps} />
  496. </div>
  497. )
  498. }
  499. const renderNoResultAlert = () => {
  500. if (EmptyState) return EmptyState
  501. return <LogsTableEmptyState />
  502. }
  503. if (!data) return null
  504. return (
  505. <section className={'h-full flex w-full flex-col flex-1'}>
  506. {!queryType && logsExplorerTableHeader}
  507. <ResizablePanelGroup orientation="horizontal">
  508. <ResizablePanel
  509. id="log-table-content"
  510. minSize={`${panelContentMinSize}`}
  511. maxSize={`${panelContentMaxSize}`}
  512. defaultSize={`${panelContentMaxSize}`}
  513. >
  514. <div className="flex flex-col h-full">
  515. <div
  516. style={{
  517. maxHeight: selectedRows.size > 0 ? 40 : 0,
  518. overflow: 'hidden',
  519. transition: 'max-height 150ms ease',
  520. }}
  521. >
  522. <MultiSelectActionBar
  523. selectedRows={selectedRows}
  524. selectedRowsData={selectedRowsData}
  525. copiedFormat={copiedFormat}
  526. onCopy={handleCopySelectedRows}
  527. queryType={queryType}
  528. sqlQuery={sqlQuery}
  529. onClear={() => {
  530. setSelectedRows(new Set())
  531. }}
  532. />
  533. </div>
  534. <ContextMenu modal={false}>
  535. <ContextMenuTrigger asChild>
  536. <div ref={triggerRef} className="fixed pointer-events-none w-0 h-0" />
  537. </ContextMenuTrigger>
  538. <ContextMenuContent key={contextMenuKey}>
  539. <ContextMenuItem
  540. className="gap-x-2"
  541. onSelect={() => {
  542. const eventMessage = activeRow?.event_message
  543. if (eventMessage) {
  544. copyToClipboard(eventMessage, () => {
  545. toast.success('Copied to clipboard')
  546. })
  547. }
  548. }}
  549. >
  550. <Copy size={14} />
  551. <span className="text-xs">Copy event message</span>
  552. </ContextMenuItem>
  553. </ContextMenuContent>
  554. </ContextMenu>
  555. <DataGrid
  556. role="table"
  557. style={{ flex: '1 1 0%', minHeight: 0 }}
  558. className={cn('border-0', {
  559. 'data-grid--simple-logs': queryType,
  560. 'data-grid--logs-explorer': !queryType,
  561. })}
  562. rowHeight={40}
  563. headerRowHeight={queryType ? 0 : 28}
  564. columns={columns}
  565. rowClass={(row: LogData) => {
  566. const key = getRowKey(row)
  567. const isMultiSelected = selectedRows.has(key)
  568. const isSingleSelected = selectedRow !== null && getRowKey(selectedRow) === key
  569. return cn(
  570. 'font-mono tracking-tight bg-studio! hover:bg-surface-100! cursor-pointer',
  571. {
  572. 'bg-surface-200! rdg-row--focused': isSingleSelected || isMultiSelected,
  573. }
  574. )
  575. }}
  576. rows={logDataRows}
  577. rowKeyGetter={(r) => {
  578. if (!hasId) return JSON.stringify(r)
  579. const row = r as LogData
  580. return row.id
  581. }}
  582. renderers={{
  583. renderRow: RowRenderer,
  584. noRowsFallback: !isLoading ? (
  585. // gridColumn: '1 / -1' makes the fallback span all CSS grid columns,
  586. // including the checkbox column we prepend, so it fills the full width.
  587. <div style={{ gridColumn: '1 / -1' }}>
  588. {logDataRows.length === 0 && !error && renderNoResultAlert()}
  589. {error && renderErrorAlert()}
  590. </div>
  591. ) : null,
  592. }}
  593. />
  594. </div>
  595. </ResizablePanel>
  596. {selectionOpen && (
  597. <>
  598. <ResizableHandle withHandle />
  599. <ResizablePanel
  600. id="log-table-panel"
  601. minSize={`${100 - panelContentMaxSize}`}
  602. maxSize={`${100 - panelContentMinSize}`}
  603. defaultSize={`${100 - panelContentMaxSize}`}
  604. >
  605. <LogSelection
  606. isLoading={isSelectedLogLoading || false}
  607. projectRef={projectRef}
  608. onClose={() => {
  609. onSelectedLogChange?.(null)
  610. }}
  611. log={selectedLog}
  612. error={selectedLogError}
  613. queryType={queryType}
  614. />
  615. </ResizablePanel>
  616. </>
  617. )}
  618. </ResizablePanelGroup>
  619. </section>
  620. )
  621. }