import { ArrowDown, 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 { DbQueryHook } from 'hooks/analytics/useDbQuery' import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Sheet, SheetContent, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_, cn, CodeBlock, SheetTitle, } from 'ui' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { hasIndexRecommendations } from './index-advisor.utils' import { IndexSuggestionIcon } from './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 { useQueryPerformanceSort } from './hooks/useQueryPerformanceSort' import { formatDuration } from './QueryPerformance.utils' import { GetIndexAdvisorResultResponse } from 'data/database/retrieve-index-advisor-result-query' interface QueryPerformanceGridProps { queryPerformanceQuery: DbQueryHook } interface QueryPerformanceRow { query: string prop_total_time: number total_time: number calls: number max_time: number mean_time: number min_time: number rows_read: number cache_hit_rate: string rolname: string index_advisor_result: GetIndexAdvisorResultResponse | null } 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 = ({ queryPerformanceQuery }: QueryPerformanceGridProps) => { const { sort, setSortConfig } = useQueryPerformanceSort() const gridRef = useRef(null) const { sort: urlSort, order, roles, search } = useParams() const { isLoading, data } = queryPerformanceQuery 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(data ?? []) : 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 }) }} /> )}
) } 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()}

) : (

)}
) } const cacheHitRateToNumber = (value: number | string) => { if (typeof value === 'number') return value return parseFloat(value.toString().replace('%', '')) || 0 } if (col.id === 'cache_hit_rate') { return (
{typeof value === 'string' ? (

{cacheHitRateToNumber(value).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(() => { const rawData = data ?? [] if (sort?.column === 'prop_total_time') { const sortedData = [...rawData].sort((a, b) => { const getNumericValue = (value: number | string) => { if (!value || value === 'n/a') return 0 if (typeof value === 'number') return value return parseFloat(value.toString().replace('%', '')) || 0 } const aValue = getNumericValue(a.prop_total_time) const bValue = getNumericValue(b.prop_total_time) return sort.order === 'asc' ? aValue - bValue : bValue - aValue }) return sortedData } return rawData }, [data, sort]) 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) // eslint-disable-next-line react-hooks/exhaustive-deps }, [search, roles, urlSort, order]) const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (!reportData.length || selectedRow === undefined) return if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return // stop default RDG behavior (which moves focus to header when selectedRow is 0) 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 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' : ''}`, '[&>.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) { setSelectedRow(idx) gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) const rowQuery = reportData[idx]?.query ?? '' if (!rowQuery.trim().toLowerCase().startsWith('select')) { setView('details') } } }} /> ) }, 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 && }
) }