QueryInsightsTable.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. import { safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
  2. import { wrapWithRollback } from '@supabase/pg-meta/src/query'
  3. import { useParams } from 'common'
  4. import { Search, TextSearch, X } from 'lucide-react'
  5. import { useRouter } from 'next/router'
  6. import { parseAsArrayOf, parseAsString, useQueryStates } from 'nuqs'
  7. import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
  8. // eslint-disable-next-line no-restricted-imports
  9. import DataGrid, { DataGridHandle, Row } from 'react-data-grid'
  10. import { Button, cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
  11. import { Input } from 'ui-patterns/DataInputs/Input'
  12. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  13. import { buildQueryInsightFixPrompt } from '../../QueryPerformance/QueryPerformance.ai'
  14. import type { QueryPerformanceRow } from '../../QueryPerformance/QueryPerformance.types'
  15. import { useQueryInsightsIssues } from '../hooks/useQueryInsightsIssues'
  16. import { useQueryInsightsTableColumns } from '../hooks/useQueryInsightsTableColumns'
  17. import type { ClassifiedQuery } from '../QueryInsightsHealth/QueryInsightsHealth.types'
  18. import { QueryInsightsDetailSheet } from './QueryInsightsDetailSheet'
  19. import type { IssueFilter, Mode } from './QueryInsightsTable.types'
  20. import {
  21. formatDuration,
  22. getColumnName,
  23. getQueryType,
  24. getTableName,
  25. } from './QueryInsightsTable.utils'
  26. import type { QueryPlanRow } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.types'
  27. import { FilterPill } from '@/components/interfaces/QueryPerformance/components/FilterPill'
  28. import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
  29. import { FilterPopover } from '@/components/ui/FilterPopover'
  30. import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle'
  31. import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
  32. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  33. import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
  34. import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
  35. interface QueryInsightsTableProps {
  36. data: QueryPerformanceRow[]
  37. isLoading: boolean
  38. currentSelectedQuery?: string | null
  39. onCurrentSelectQuery?: (query: string | null) => void
  40. }
  41. export const QueryInsightsTable = ({
  42. data,
  43. isLoading,
  44. currentSelectedQuery,
  45. onCurrentSelectQuery,
  46. }: QueryInsightsTableProps) => {
  47. const [mode, setMode] = useState<Mode>('triage')
  48. const [filter, setFilter] = useState<IssueFilter>('all')
  49. const [
  50. { search: urlSearch, sort: urlSortCol, order: urlSortOrder, source: urlSource },
  51. setQueryStates,
  52. ] = useQueryStates({
  53. search: parseAsString.withDefault(''),
  54. sort: parseAsString,
  55. order: parseAsString,
  56. source: parseAsArrayOf(parseAsString).withDefault([]),
  57. })
  58. const [searchQuery, setSearchQuery] = useState(urlSearch || '')
  59. const appNameFilter = urlSource
  60. const setAppNameFilter = (names: string[]) =>
  61. setQueryStates({ source: names.length ? names : null })
  62. const appNameOptions = useMemo(() => {
  63. const names = Array.from(
  64. new Set(data.map((r) => r.application_name).filter(Boolean))
  65. ) as string[]
  66. return names.map((name) => ({ value: name, label: name }))
  67. }, [data])
  68. const filteredData = useMemo(() => {
  69. if (appNameFilter.length === 0) return data
  70. return data.filter((r) => appNameFilter.includes(r.application_name ?? ''))
  71. }, [data, appNameFilter])
  72. const { classified, errors, indexIssues, slowQueries } = useQueryInsightsIssues(filteredData)
  73. const [selectedRow, setSelectedRow] = useState<number>()
  74. const [selectedTriageRow, setSelectedTriageRow] = useState<number | undefined>()
  75. const [sheetView, setSheetView] = useState<'details' | 'indexes' | 'explain'>('details')
  76. const gridRef = useRef<DataGridHandle>(null)
  77. const triageGridRef = useRef<DataGridHandle>(null)
  78. const dataGridContainerRef = useRef<HTMLDivElement>(null)
  79. const triageContainerRef = useRef<HTMLDivElement>(null)
  80. const scrollContainerRef = useRef<HTMLDivElement>(null)
  81. const [triageContainerWidth, setTriageContainerWidth] = useState(0)
  82. const sort = useMemo<{ column: string; order: 'asc' | 'desc' }>(
  83. () =>
  84. urlSortCol && urlSortOrder && ['asc', 'desc'].includes(urlSortOrder)
  85. ? { column: urlSortCol, order: urlSortOrder as 'asc' | 'desc' }
  86. : { column: 'prop_total_time', order: 'desc' },
  87. [urlSortCol, urlSortOrder]
  88. )
  89. const setSort = useCallback(
  90. (config: { column: string; order: 'asc' | 'desc' } | null) =>
  91. setQueryStates(
  92. config ? { sort: config.column, order: config.order } : { sort: null, order: null }
  93. ),
  94. [setQueryStates]
  95. )
  96. const [explainResults, setExplainResults] = useState<Record<string, QueryPlanRow[]>>({})
  97. const [explainLoadingQuery, setExplainLoadingQuery] = useState<string | null>(null)
  98. const { ref } = useParams()
  99. const router = useRouter()
  100. const { openSidebar } = useSidebarManagerSnapshot()
  101. const aiSnap = useAiAssistantStateSnapshot()
  102. const { data: project } = useSelectedProjectQuery()
  103. const { mutate: executeExplain } = useExecuteSqlMutation()
  104. const triageItems = useMemo(() => classified.filter((q) => q.issueType !== null), [classified])
  105. const filteredTriageItems = useMemo(() => {
  106. const filtered =
  107. filter === 'all' ? triageItems : triageItems.filter((q) => q.issueType === filter)
  108. return filtered.map((item) => ({
  109. ...item,
  110. queryType: getQueryType(item.query),
  111. }))
  112. }, [triageItems, filter])
  113. const explorerItems = useMemo(() => {
  114. let items = [...classified]
  115. if (searchQuery.trim()) {
  116. const searchLower = searchQuery.toLowerCase()
  117. items = items.filter((item) => {
  118. const queryType = getQueryType(item.query) ?? ''
  119. const tableName = getTableName(item.query) ?? ''
  120. const columnName = getColumnName(item.query) ?? ''
  121. const appName = item.application_name ?? ''
  122. const query = item.query ?? ''
  123. return (
  124. queryType.toLowerCase().includes(searchLower) ||
  125. tableName.toLowerCase().includes(searchLower) ||
  126. columnName.toLowerCase().includes(searchLower) ||
  127. appName.toLowerCase().includes(searchLower) ||
  128. query.toLowerCase().includes(searchLower)
  129. )
  130. })
  131. }
  132. if (sort) {
  133. items.sort((a, b) => {
  134. if (sort.column === 'query') {
  135. const aDate = a.first_seen ? new Date(a.first_seen).getTime() : 0
  136. const bDate = b.first_seen ? new Date(b.first_seen).getTime() : 0
  137. return sort.order === 'asc' ? aDate - bDate : bDate - aDate
  138. }
  139. const aValue: unknown = a[sort.column as keyof typeof a]
  140. const bValue: unknown = b[sort.column as keyof typeof b]
  141. if (typeof aValue === 'number' && typeof bValue === 'number') {
  142. return sort.order === 'asc' ? aValue - bValue : bValue - aValue
  143. }
  144. return 0
  145. })
  146. }
  147. return items
  148. }, [classified, searchQuery, sort])
  149. const activeSheetRow: ClassifiedQuery | undefined = useMemo(() => {
  150. if (mode === 'triage') {
  151. return selectedTriageRow !== undefined ? filteredTriageItems[selectedTriageRow] : undefined
  152. }
  153. return selectedRow !== undefined ? (explorerItems[selectedRow] as ClassifiedQuery) : undefined
  154. }, [mode, selectedTriageRow, selectedRow, filteredTriageItems, explorerItems])
  155. const runExplain = useCallback(
  156. (query: SafeSqlFragment) => {
  157. if (explainResults[query]) return
  158. if (explainLoadingQuery) return
  159. const requestQuery = query
  160. setExplainLoadingQuery(requestQuery)
  161. executeExplain(
  162. {
  163. projectRef: project?.ref,
  164. connectionString: project?.connectionString,
  165. sql: wrapWithRollback(safeSql`EXPLAIN ANALYZE ${requestQuery}`),
  166. },
  167. {
  168. onSuccess(data) {
  169. setExplainResults((prev) => ({ ...prev, [requestQuery]: data.result }))
  170. setExplainLoadingQuery(null)
  171. },
  172. onError() {
  173. setExplainLoadingQuery(null)
  174. },
  175. }
  176. )
  177. },
  178. [explainResults, explainLoadingQuery, executeExplain, project]
  179. )
  180. const handleGoToLogs = useCallback(() => {
  181. router.push(`/project/${ref}/logs/postgres-logs`)
  182. }, [router, ref])
  183. const handleAiSuggestedFix = useCallback(
  184. (item: ClassifiedQuery) => {
  185. const { query, prompt } = buildQueryInsightFixPrompt(item)
  186. openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
  187. aiSnap.newChat({
  188. sqlSnippets: [{ label: 'Query', content: query }],
  189. initialMessage: prompt,
  190. })
  191. },
  192. [openSidebar, aiSnap]
  193. )
  194. useEffect(() => {
  195. if (sheetView === 'explain' && activeSheetRow?.query) {
  196. runExplain(activeSheetRow.query)
  197. }
  198. // eslint-disable-next-line react-hooks/exhaustive-deps
  199. }, [sheetView, activeSheetRow?.query])
  200. useEffect(() => {
  201. const currentPath = router.asPath.split('?')[0]
  202. const handleRouteChange = (url: string) => {
  203. if (url.split('?')[0] !== currentPath) {
  204. setQueryStates({ search: null })
  205. onCurrentSelectQuery?.(null)
  206. }
  207. }
  208. router.events.on('routeChangeStart', handleRouteChange)
  209. return () => router.events.off('routeChangeStart', handleRouteChange)
  210. // eslint-disable-next-line react-hooks/exhaustive-deps
  211. }, [])
  212. const timeConsumedWidth = useMemo(() => {
  213. if (!explorerItems.length) return 150
  214. let maxWidth = 150
  215. explorerItems.forEach((row) => {
  216. const pct = row.prop_total_time || 0
  217. const total = row.total_time || 0
  218. if (pct && total) {
  219. const text = `${pct.toFixed(1)}% / ${formatDuration(total)}`
  220. maxWidth = Math.max(maxWidth, text.length * 8 + 40)
  221. }
  222. })
  223. return Math.min(maxWidth, 300)
  224. }, [explorerItems])
  225. const triageQueryColWidth = useMemo(() => {
  226. if (!triageContainerWidth) return 380
  227. const fixed = timeConsumedWidth + 100 + 300 + 4
  228. return Math.max(380, triageContainerWidth - fixed - 120)
  229. }, [triageContainerWidth, timeConsumedWidth])
  230. const { columns, triageColumns } = useQueryInsightsTableColumns({
  231. sort,
  232. setSort,
  233. timeConsumedWidth,
  234. triageQueryColWidth,
  235. gridRef,
  236. setSelectedRow,
  237. setSelectedTriageRow,
  238. setSheetView,
  239. handleGoToLogs,
  240. handleAiSuggestedFix,
  241. })
  242. const handleKeyDown = useCallback(
  243. (event: KeyboardEvent) => {
  244. if (!explorerItems.length || selectedRow === undefined) return
  245. if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
  246. event.stopPropagation()
  247. let nextIndex = selectedRow
  248. if (event.key === 'ArrowUp' && selectedRow > 0) {
  249. nextIndex = selectedRow - 1
  250. } else if (event.key === 'ArrowDown' && selectedRow < explorerItems.length - 1) {
  251. nextIndex = selectedRow + 1
  252. }
  253. if (nextIndex !== selectedRow) {
  254. setSelectedRow(nextIndex)
  255. gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex })
  256. }
  257. },
  258. [explorerItems, selectedRow]
  259. )
  260. useEffect(() => {
  261. window.addEventListener('keydown', handleKeyDown, true)
  262. return () => {
  263. window.removeEventListener('keydown', handleKeyDown, true)
  264. }
  265. }, [handleKeyDown])
  266. useEffect(() => {
  267. setSelectedRow(undefined)
  268. }, [searchQuery, sort])
  269. useEffect(() => {
  270. if (mode === 'triage') {
  271. triageGridRef.current?.scrollToCell({ idx: 0, rowIdx: 0 })
  272. } else {
  273. gridRef.current?.scrollToCell({ idx: 0, rowIdx: 0 })
  274. }
  275. }, [mode])
  276. useEffect(() => {
  277. const el = triageContainerRef.current
  278. if (!el) return
  279. const observer = new ResizeObserver(([entry]) => {
  280. setTriageContainerWidth(entry.contentRect.width)
  281. })
  282. observer.observe(el)
  283. return () => observer.disconnect()
  284. }, [])
  285. useEffect(() => {
  286. if (urlSearch !== searchQuery) {
  287. setQueryStates({ search: searchQuery || null })
  288. }
  289. // eslint-disable-next-line react-hooks/exhaustive-deps
  290. }, [searchQuery])
  291. const errorCount = errors.length
  292. const indexCount = indexIssues.length
  293. const slowCount = slowQueries.length
  294. return (
  295. <div className="flex flex-col flex-1 min-h-0">
  296. <div className="overflow-x-auto shrink-0 bg-surface-100 border-b">
  297. <div className="flex items-center justify-between px-6 h-10 min-w-max">
  298. <div className="flex items-center gap-x-1.5">
  299. <TwoOptionToggle
  300. width={75}
  301. options={['explorer', 'triage']}
  302. activeOption={mode}
  303. borderOverride="border"
  304. onClickOption={(mode) => setMode(mode as Mode)}
  305. />
  306. {appNameFilter.length > 0 ? (
  307. <FilterPill
  308. label="Source"
  309. value={appNameFilter.join(', ')}
  310. onClear={() => setAppNameFilter([])}
  311. />
  312. ) : (
  313. <FilterPopover
  314. name="Source"
  315. options={appNameOptions}
  316. activeOptions={appNameFilter}
  317. valueKey="value"
  318. labelKey="label"
  319. onSaveFilters={setAppNameFilter}
  320. showOnlyButton={false}
  321. />
  322. )}
  323. </div>
  324. <div className="flex items-center">
  325. {mode === 'triage' ? (
  326. <Tabs_Shadcn_ value={filter} onValueChange={(v) => setFilter(v as IssueFilter)}>
  327. <TabsList_Shadcn_ className="flex gap-x-4 rounded-none mt-0! pt-0 border-none!">
  328. <TabsTrigger_Shadcn_
  329. value="all"
  330. className="text-xs py-3 border-b font-mono uppercase"
  331. >
  332. All{triageItems.length > 0 && ` (${triageItems.length})`}
  333. </TabsTrigger_Shadcn_>
  334. <TabsTrigger_Shadcn_
  335. value="error"
  336. className="text-xs py-3 border-b font-mono uppercase"
  337. >
  338. Errors{errorCount > 0 && ` (${errorCount})`}
  339. </TabsTrigger_Shadcn_>
  340. <TabsTrigger_Shadcn_
  341. value="index"
  342. className="text-xs py-3 border-b font-mono uppercase"
  343. >
  344. Index{indexCount > 0 && ` (${indexCount})`}
  345. </TabsTrigger_Shadcn_>
  346. <TabsTrigger_Shadcn_
  347. value="slow"
  348. className="text-xs py-3 border-b font-mono uppercase"
  349. >
  350. Slow{slowCount > 0 && ` (${slowCount})`}
  351. </TabsTrigger_Shadcn_>
  352. </TabsList_Shadcn_>
  353. </Tabs_Shadcn_>
  354. ) : (
  355. <Input
  356. size="tiny"
  357. autoComplete="off"
  358. icon={<Search className="h-4 w-4" />}
  359. value={searchQuery}
  360. onChange={(e) => setSearchQuery(e.target.value)}
  361. name="search"
  362. id="search"
  363. placeholder="Search queries..."
  364. className="w-64"
  365. actions={[
  366. searchQuery && (
  367. <Button
  368. key="clear"
  369. size="tiny"
  370. type="text"
  371. icon={<X className="h-4 w-4" />}
  372. onClick={() => setSearchQuery('')}
  373. className="p-0 h-5 w-5"
  374. />
  375. ),
  376. ]}
  377. />
  378. )}
  379. </div>
  380. </div>
  381. </div>
  382. <div
  383. ref={scrollContainerRef}
  384. className="relative flex-1 min-h-0 flex flex-col overflow-hidden"
  385. >
  386. <div
  387. className={[
  388. 'absolute bottom-8 left-0 right-0 flex justify-center z-10 pointer-events-none',
  389. 'transition-all duration-200',
  390. currentSelectedQuery
  391. ? 'opacity-100 translate-y-0 pointer-events-auto'
  392. : 'opacity-0 translate-y-4 pointer-events-none',
  393. ].join(' ')}
  394. >
  395. <Button
  396. type="default"
  397. size="tiny"
  398. className="rounded-full shadow-md"
  399. onClick={() => onCurrentSelectQuery?.(null)}
  400. >
  401. Clear query
  402. </Button>
  403. </div>
  404. {isLoading ? (
  405. <div className="px-6 py-4">
  406. <GenericSkeletonLoader />
  407. </div>
  408. ) : mode === 'triage' ? (
  409. <div ref={triageContainerRef} className="flex-1 min-h-0 min-w-0 overflow-x-auto">
  410. <DataGrid
  411. ref={triageGridRef}
  412. style={{ height: '100%' }}
  413. className="flex-1 grow h-full"
  414. rowHeight={60}
  415. headerRowHeight={36}
  416. columns={triageColumns}
  417. rows={filteredTriageItems}
  418. rowClass={(_, idx) => {
  419. const isSelected = idx === selectedTriageRow
  420. const query = filteredTriageItems[idx]?.query
  421. const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false
  422. return [
  423. `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : isCharted ? 'bg-surface-200 dark:bg-surface-200' : 'bg-200 hover:bg-surface-200'} cursor-pointer`,
  424. '[&>div:first-child]:border-l-4 [&>div:first-child]:pl-5 [&>div:last-child]:pr-6',
  425. `${isSelected || isCharted ? '[&>div:first-child]:border-l-foreground' : '[&>div:first-child]:border-l-transparent'}`,
  426. '[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none [&>.rdg-cell]:py-3',
  427. '[&>.rdg-cell.column-prop_total_time]:relative',
  428. ].join(' ')
  429. }}
  430. renderers={{
  431. renderRow(idx, props) {
  432. return (
  433. <Row
  434. {...props}
  435. key={`triage-row-${props.rowIdx}`}
  436. onClick={(event) => {
  437. event.stopPropagation()
  438. if (typeof idx === 'number' && idx >= 0) {
  439. const query = filteredTriageItems[idx]?.query
  440. if (query && onCurrentSelectQuery) {
  441. onCurrentSelectQuery(currentSelectedQuery === query ? null : query)
  442. }
  443. }
  444. }}
  445. />
  446. )
  447. },
  448. noRowsFallback: (
  449. <div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
  450. <TextSearch className="text-foreground-muted" strokeWidth={1} />
  451. <div className="text-center">
  452. <p className="text-foreground">No issues found</p>
  453. <p className="text-foreground-light">
  454. {data.length === 0
  455. ? 'No query data available yet'
  456. : 'No issues detected for the selected filter'}
  457. </p>
  458. </div>
  459. </div>
  460. ),
  461. }}
  462. />
  463. </div>
  464. ) : (
  465. <div ref={dataGridContainerRef} className="flex-1 min-h-0 min-w-0 overflow-x-auto">
  466. <DataGrid
  467. ref={gridRef}
  468. style={{ height: '100%' }}
  469. className={cn('flex-1 grow h-full')}
  470. rowHeight={44}
  471. headerRowHeight={36}
  472. columns={columns}
  473. rows={explorerItems}
  474. rowClass={(_, idx) => {
  475. const isSelected = idx === selectedRow
  476. const query = explorerItems[idx]?.query
  477. const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false
  478. return [
  479. `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : isCharted ? 'bg-surface-200 dark:bg-surface-200' : 'bg-200 hover:bg-surface-200'} cursor-pointer`,
  480. '[&>div:first-child]:border-l-4 [&>div:first-child]:pl-5 [&>div:last-child]:pr-6',
  481. `${isSelected || isCharted ? '[&>div:first-child]:border-l-foreground' : '[&>div:first-child]:border-l-transparent'}`,
  482. '[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none [&>.rdg-cell]:py-3',
  483. '[&>.rdg-cell.column-prop_total_time]:relative',
  484. ].join(' ')
  485. }}
  486. renderers={{
  487. renderRow(idx, props) {
  488. return (
  489. <Row
  490. {...props}
  491. key={`explorer-row-${props.rowIdx}`}
  492. onClick={(event) => {
  493. event.stopPropagation()
  494. if (typeof idx === 'number' && idx >= 0) {
  495. const query = explorerItems[idx]?.query
  496. if (query && onCurrentSelectQuery) {
  497. onCurrentSelectQuery(currentSelectedQuery === query ? null : query)
  498. }
  499. }
  500. }}
  501. />
  502. )
  503. },
  504. noRowsFallback: (
  505. <div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
  506. <TextSearch className="text-foreground-muted" strokeWidth={1} />
  507. <div className="text-center">
  508. <p className="text-foreground">No queries found</p>
  509. <p className="text-foreground-light">
  510. {searchQuery.trim()
  511. ? 'No queries match your search criteria'
  512. : 'No query data available yet'}
  513. </p>
  514. </div>
  515. </div>
  516. ),
  517. }}
  518. />
  519. </div>
  520. )}
  521. </div>
  522. <QueryInsightsDetailSheet
  523. open={activeSheetRow !== undefined}
  524. onOpenChange={(open) => {
  525. if (!open) {
  526. setSelectedTriageRow(undefined)
  527. setSelectedRow(undefined)
  528. }
  529. }}
  530. activeSheetRow={activeSheetRow}
  531. sheetView={sheetView}
  532. onSheetViewChange={setSheetView}
  533. onClose={() => {
  534. setSelectedTriageRow(undefined)
  535. setSelectedRow(undefined)
  536. }}
  537. dataGridContainerRef={dataGridContainerRef}
  538. triageContainerRef={triageContainerRef}
  539. explainLoadingQuery={explainLoadingQuery}
  540. explainResults={explainResults}
  541. />
  542. </div>
  543. )
  544. }