import { ArrowDown, ArrowRight, ArrowUp, ChevronDown, TextSearch } from 'lucide-react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid' import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { Button, CodeBlock, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Sheet, SheetContent, SheetTitle, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_, cn, } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' import { hasIndexRecommendations } from './IndexAdvisor/index-advisor.utils' import { IndexSuggestionIcon } from './IndexAdvisor/IndexSuggestionIcon' import { QueryDetail } from './QueryDetail' import { QueryIndexes } from './QueryIndexes' import { QUERY_PERFORMANCE_COLUMNS, QUERY_PERFORMANCE_REPORT_TYPES, QUERY_PERFORMANCE_ROLE_DESCRIPTION, } from './QueryPerformance.constants' import { QueryPerformanceRow } from './QueryPerformance.types' import { formatDuration } from './QueryPerformance.utils' interface QueryPerformanceGridProps { aggregatedData: QueryPerformanceRow[] isLoading: boolean currentSelectedQuery?: string | null // Make optional onCurrentSelectQuery?: (query: string) => void // Make optional } const calculateTimeConsumedWidth = (data: QueryPerformanceRow[]) => { if (!data || data.length === 0) return 150 let maxWidth = 150 data.forEach((row) => { const percentage = row.prop_total_time || 0 const totalTime = row.total_time || 0 if (percentage && totalTime) { const percentageText = `${percentage.toFixed(1)}%` const durationText = formatDuration(totalTime) const fullText = `${percentageText} / ${durationText}` const estimatedWidth = fullText.length * 8 + 40 maxWidth = Math.max(maxWidth, estimatedWidth) } }) return Math.min(maxWidth, 300) } export const QueryPerformanceGrid = ({ aggregatedData, isLoading, currentSelectedQuery, onCurrentSelectQuery, }: QueryPerformanceGridProps) => { const { sort, setSortConfig } = useQueryPerformanceSort() const gridRef = useRef(null) const { sort: urlSort, order, roles, search, minCalls } = useParams() const dataGridContainerRef = useRef(null) const [view, setView] = useState<'details' | 'suggestion'>('details') const [selectedRow, setSelectedRow] = useState() const reportType = QUERY_PERFORMANCE_REPORT_TYPES.UNIFIED const columns = QUERY_PERFORMANCE_COLUMNS.map((col) => { const nonSortableColumns = ['query'] const result: Column = { key: col.id, name: col.name, cellClass: `column-${col.id}`, resizable: true, minWidth: col.id === 'prop_total_time' ? calculateTimeConsumedWidth((aggregatedData as any) ?? []) : col.minWidth ?? 120, sortable: !nonSortableColumns.includes(col.id), headerCellClass: 'first:pl-6 cursor-pointer', renderHeaderCell: () => { const isSortable = !nonSortableColumns.includes(col.id) return (

{col.name}

{col.description && (

{col.description}

)}
{isSortable && (
) }, renderCell: (props) => { const value = props.row?.[col.id] if (col.id === 'query') { return (
{hasIndexRecommendations(props.row.index_advisor_result, true) && ( { setSelectedRow(props.rowIdx) setView('suggestion') gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx }) }} /> )}
{onCurrentSelectQuery && ( } size="tiny" type="default" onClick={(e) => { e.stopPropagation() setSelectedRow(props.rowIdx) setView('details') gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx }) }} className="p-1 flex-shrink-0 -translate-x-2 group-hover:flex hidden" /> )}
) } const isTime = col.name.includes('time') const formattedValue = !!value && typeof value === 'number' && !isNaN(value) && isFinite(value) ? isTime ? `${value.toFixed(0).toLocaleString()}ms` : value.toLocaleString() : '' if (col.id === 'prop_total_time') { const percentage = props.row.prop_total_time || 0 const totalTime = props.row.total_time || 0 const fillWidth = Math.min(percentage, 100) return (
{percentage && totalTime ? ( {percentage.toFixed(1)}% {' '} / {formatDuration(totalTime)} ) : (

)}
) } if (col.id === 'calls') { return (
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (

{value.toLocaleString()}

) : (

)}
) } if (col.id === 'max_time' || col.id === 'mean_time' || col.id === 'min_time') { return (
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (

{Math.round(value).toLocaleString()}ms

) : (

)}
) } if (col.id === 'rows_read') { return (
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (

{value.toLocaleString()}

) : (

)}
) } if (col.id === 'cache_hit_rate') { const numericValue = typeof value === 'number' ? value : parseFloat(value) return (
{typeof numericValue === 'number' && !isNaN(numericValue) && isFinite(numericValue) ? (

{numericValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2, })} %

) : (

)}
) } if (col.id === 'rolname') { return (
{value ? (

{value}

{ QUERY_PERFORMANCE_ROLE_DESCRIPTION.find((role) => role.name === value) ?.description }
) : (

)}
) } return (

{formattedValue}

) }, } return result }) const reportData = useMemo(() => { let data = [...aggregatedData] if (search && typeof search === 'string' && search.length > 0) { data = data.filter((row) => row.query.toLowerCase().includes(search.toLowerCase())) } if (roles && Array.isArray(roles) && roles.length > 0) { data = data.filter((row) => row.rolname && roles.includes(row.rolname)) } const minCallsNum = Number(minCalls) if (!Number.isNaN(minCallsNum) && minCallsNum > 0) { data = data.filter((row) => (row.calls || 0) >= minCallsNum) } if (sort?.column === 'prop_total_time') { data.sort((a, b) => { const aValue = a.prop_total_time || 0 const bValue = b.prop_total_time || 0 return sort.order === 'asc' ? aValue - bValue : bValue - aValue }) } else if (sort?.column && sort.column !== 'query') { data.sort((a, b) => { const aValue = a[sort.column as keyof QueryPerformanceRow] || 0 const bValue = b[sort.column as keyof QueryPerformanceRow] || 0 if (typeof aValue === 'number' && typeof bValue === 'number') { return sort.order === 'asc' ? aValue - bValue : bValue - aValue } return 0 }) } return data }, [aggregatedData, sort, search, roles, minCalls]) const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined const query = (selectedQuery ?? '').trim().toLowerCase() const showIndexSuggestions = (query.startsWith('select') || query.startsWith('with pgrst_source') || query.startsWith('with pgrst_payload')) && hasIndexRecommendations(reportData[selectedRow!]?.index_advisor_result, true) useEffect(() => { setSelectedRow(undefined) }, [search, roles, urlSort, order, minCalls]) const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (!reportData.length || selectedRow === undefined) return if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return event.stopPropagation() let nextIndex = selectedRow if (event.key === 'ArrowUp' && selectedRow > 0) { nextIndex = selectedRow - 1 } else if (event.key === 'ArrowDown' && selectedRow < reportData.length - 1) { nextIndex = selectedRow + 1 } if (nextIndex !== selectedRow) { setSelectedRow(nextIndex) gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex }) const rowQuery = reportData[nextIndex]?.query ?? '' if (!rowQuery.trim().toLowerCase().startsWith('select')) { setView('details') } } }, [reportData, selectedRow] ) useEffect(() => { // run before RDG to prevent header focus (the third param: true) window.addEventListener('keydown', handleKeyDown, true) return () => { window.removeEventListener('keydown', handleKeyDown, true) } }, [handleKeyDown]) return (
{ const isSelected = idx === selectedRow const query = reportData[idx]?.query const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false return [ `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`, `${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:!border-l-foreground' : ''}`, `${isCharted ? 'bg-surface-200 dark:bg-surface-200' : ''}`, `${isCharted ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-brand' : ''}`, '[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', '[&>.rdg-cell.column-prop_total_time]:relative', ].join(' ') }} renderers={{ renderRow(idx, props) { return ( { event.stopPropagation() if (typeof idx === 'number' && idx >= 0) { // If onCurrentSelectQuery is provided, use the chart selection logic if (onCurrentSelectQuery) { const query = reportData[idx]?.query if (query) { onCurrentSelectQuery(query) } } else { // Otherwise, open the detail panel setSelectedRow(idx) setView('details') gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) } } }} /> ) }, noRowsFallback: isLoading ? (
) : (

No queries detected

There are no actively running queries that match the criteria

), }} />
{ if (!open) { setSelectedRow(undefined) } }} modal={false} > Query details { if (dataGridContainerRef.current?.contains(event.target as Node)) { event.preventDefault() } }} > setView(value)} >
Query details {showIndexSuggestions && ( Indexes )}
{selectedRow !== undefined && ( setView('suggestion')} /> )} {selectedRow !== undefined && }
) }