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, SheetDescription, 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 { Admonition } from 'ui-patterns' import { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' import { hasIndexRecommendations, queryInvolvesProtectedSchemas, } 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' import { parseAsString, parseAsArrayOf, parseAsJson, useQueryStates } from 'nuqs' import { NumericFilter } from 'components/interfaces/Reports/v2/ReportsNumericFilter' interface QueryPerformanceGridProps { aggregatedData: QueryPerformanceRow[] isLoading: boolean error?: string | null currentSelectedQuery?: string | null onCurrentSelectQuery?: (query: string) => void onRetry?: () => void } 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, error, currentSelectedQuery, onCurrentSelectQuery, onRetry, }: QueryPerformanceGridProps) => { const { sort, setSortConfig } = useQueryPerformanceSort() const gridRef = useRef(null) const { sort: urlSort, order } = useParams() const [{ search, roles, callsFilter }] = useQueryStates({ search: parseAsString.withDefault(''), roles: parseAsArrayOf(parseAsString).withDefault([]), callsFilter: parseAsJson( (value) => value as NumericFilter | null ).withDefault({ operator: '>=', value: 0, } as NumericFilter), }) 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)) } if (callsFilter) { const { operator, value } = callsFilter data = data.filter((row) => { const calls = row.calls || 0 switch (operator) { case '=': return calls === value case '>=': return calls >= value case '<=': return calls <= value case '>': return calls > value case '<': return calls < value case '!=': return calls !== value default: return true } }) } 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, callsFilter]) useEffect(() => { setSelectedRow(undefined) }, [search, roles, urlSort, order, callsFilter]) 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]) const isSelectQuery = (query: string | undefined): boolean => { if (!query) return false const formattedQuery = query.trim().toLowerCase() return ( formattedQuery.startsWith('select') || formattedQuery.startsWith('with pgrst_source') || formattedQuery.startsWith('with pgrst_payload') ) } useEffect(() => { if (selectedRow !== undefined && view === 'suggestion') { const query = reportData[selectedRow]?.query if (!isSelectQuery(query)) { setView('details') } } }, [selectedRow, view, reportData]) if (error) { return (
{onRetry && (
)}
) } const selectedQuery = selectedRow !== undefined ? reportData[selectedRow]?.query : undefined const isProtectedSchemaQuery = queryInvolvesProtectedSchemas(selectedQuery) const canShowIndexesTab = isSelectQuery(selectedQuery) && !isProtectedSchemaQuery return (
{ const isSelected = idx === selectedRow const query = reportData[idx]?.query const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false const hasRecommendations = hasIndexRecommendations( reportData[idx]?.index_advisor_result, true ) return [ `${isSelected ? (hasRecommendations ? 'bg-warning/10 hover:bg-warning/20' : 'bg-surface-300 dark:bg-surface-300') : hasRecommendations ? 'bg-warning/10 hover:bg-warning/20' : 'bg-200 hover:bg-surface-200'} cursor-pointer`, `${isSelected ? (hasRecommendations ? '[&>div:first-child]:border-l-4 border-l-warning [&>div]:border-l-warning' : '[&>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) { const query = reportData[idx]?.query if (query) { onCurrentSelectQuery(query) } } else { setSelectedRow(idx) const hasRecommendations = hasIndexRecommendations( reportData[idx]?.index_advisor_result, true ) setView(hasRecommendations ? 'suggestion' : '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 Query Performance Details & Indexes { if (dataGridContainerRef.current?.contains(event.target as Node)) { event.preventDefault() } }} > setView(value)} >
Query details {selectedRow !== undefined && canShowIndexesTab && ( Indexes )}
{selectedRow !== undefined && ( setView('suggestion')} onClose={() => setSelectedRow(undefined)} /> )} {selectedRow !== undefined && canShowIndexesTab && ( )}
) }