import { safeSql, type SafeSqlFragment } from '@supabase/pg-meta' import { wrapWithRollback } from '@supabase/pg-meta/src/query' import { useParams } from 'common' import { Search, TextSearch, X } from 'lucide-react' import { useRouter } from 'next/router' import { parseAsArrayOf, parseAsString, useQueryStates } from 'nuqs' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { DataGridHandle, Row } from 'react-data-grid' import { Button, cn, Tabs_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { buildQueryInsightFixPrompt } from '../../QueryPerformance/QueryPerformance.ai' import type { QueryPerformanceRow } from '../../QueryPerformance/QueryPerformance.types' import { useQueryInsightsIssues } from '../hooks/useQueryInsightsIssues' import { useQueryInsightsTableColumns } from '../hooks/useQueryInsightsTableColumns' import type { ClassifiedQuery } from '../QueryInsightsHealth/QueryInsightsHealth.types' import { QueryInsightsDetailSheet } from './QueryInsightsDetailSheet' import type { IssueFilter, Mode } from './QueryInsightsTable.types' import { formatDuration, getColumnName, getQueryType, getTableName, } from './QueryInsightsTable.utils' import type { QueryPlanRow } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.types' import { FilterPill } from '@/components/interfaces/QueryPerformance/components/FilterPill' import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' import { FilterPopover } from '@/components/ui/FilterPopover' import { TwoOptionToggle } from '@/components/ui/TwoOptionToggle' import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state' import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state' interface QueryInsightsTableProps { data: QueryPerformanceRow[] isLoading: boolean currentSelectedQuery?: string | null onCurrentSelectQuery?: (query: string | null) => void } export const QueryInsightsTable = ({ data, isLoading, currentSelectedQuery, onCurrentSelectQuery, }: QueryInsightsTableProps) => { const [mode, setMode] = useState('triage') const [filter, setFilter] = useState('all') const [ { search: urlSearch, sort: urlSortCol, order: urlSortOrder, source: urlSource }, setQueryStates, ] = useQueryStates({ search: parseAsString.withDefault(''), sort: parseAsString, order: parseAsString, source: parseAsArrayOf(parseAsString).withDefault([]), }) const [searchQuery, setSearchQuery] = useState(urlSearch || '') const appNameFilter = urlSource const setAppNameFilter = (names: string[]) => setQueryStates({ source: names.length ? names : null }) const appNameOptions = useMemo(() => { const names = Array.from( new Set(data.map((r) => r.application_name).filter(Boolean)) ) as string[] return names.map((name) => ({ value: name, label: name })) }, [data]) const filteredData = useMemo(() => { if (appNameFilter.length === 0) return data return data.filter((r) => appNameFilter.includes(r.application_name ?? '')) }, [data, appNameFilter]) const { classified, errors, indexIssues, slowQueries } = useQueryInsightsIssues(filteredData) const [selectedRow, setSelectedRow] = useState() const [selectedTriageRow, setSelectedTriageRow] = useState() const [sheetView, setSheetView] = useState<'details' | 'indexes' | 'explain'>('details') const gridRef = useRef(null) const triageGridRef = useRef(null) const dataGridContainerRef = useRef(null) const triageContainerRef = useRef(null) const scrollContainerRef = useRef(null) const [triageContainerWidth, setTriageContainerWidth] = useState(0) const sort = useMemo<{ column: string; order: 'asc' | 'desc' }>( () => urlSortCol && urlSortOrder && ['asc', 'desc'].includes(urlSortOrder) ? { column: urlSortCol, order: urlSortOrder as 'asc' | 'desc' } : { column: 'prop_total_time', order: 'desc' }, [urlSortCol, urlSortOrder] ) const setSort = useCallback( (config: { column: string; order: 'asc' | 'desc' } | null) => setQueryStates( config ? { sort: config.column, order: config.order } : { sort: null, order: null } ), [setQueryStates] ) const [explainResults, setExplainResults] = useState>({}) const [explainLoadingQuery, setExplainLoadingQuery] = useState(null) const { ref } = useParams() const router = useRouter() const { openSidebar } = useSidebarManagerSnapshot() const aiSnap = useAiAssistantStateSnapshot() const { data: project } = useSelectedProjectQuery() const { mutate: executeExplain } = useExecuteSqlMutation() const triageItems = useMemo(() => classified.filter((q) => q.issueType !== null), [classified]) const filteredTriageItems = useMemo(() => { const filtered = filter === 'all' ? triageItems : triageItems.filter((q) => q.issueType === filter) return filtered.map((item) => ({ ...item, queryType: getQueryType(item.query), })) }, [triageItems, filter]) const explorerItems = useMemo(() => { let items = [...classified] if (searchQuery.trim()) { const searchLower = searchQuery.toLowerCase() items = items.filter((item) => { const queryType = getQueryType(item.query) ?? '' const tableName = getTableName(item.query) ?? '' const columnName = getColumnName(item.query) ?? '' const appName = item.application_name ?? '' const query = item.query ?? '' return ( queryType.toLowerCase().includes(searchLower) || tableName.toLowerCase().includes(searchLower) || columnName.toLowerCase().includes(searchLower) || appName.toLowerCase().includes(searchLower) || query.toLowerCase().includes(searchLower) ) }) } if (sort) { items.sort((a, b) => { if (sort.column === 'query') { const aDate = a.first_seen ? new Date(a.first_seen).getTime() : 0 const bDate = b.first_seen ? new Date(b.first_seen).getTime() : 0 return sort.order === 'asc' ? aDate - bDate : bDate - aDate } const aValue: unknown = a[sort.column as keyof typeof a] const bValue: unknown = b[sort.column as keyof typeof b] if (typeof aValue === 'number' && typeof bValue === 'number') { return sort.order === 'asc' ? aValue - bValue : bValue - aValue } return 0 }) } return items }, [classified, searchQuery, sort]) const activeSheetRow: ClassifiedQuery | undefined = useMemo(() => { if (mode === 'triage') { return selectedTriageRow !== undefined ? filteredTriageItems[selectedTriageRow] : undefined } return selectedRow !== undefined ? (explorerItems[selectedRow] as ClassifiedQuery) : undefined }, [mode, selectedTriageRow, selectedRow, filteredTriageItems, explorerItems]) const runExplain = useCallback( (query: SafeSqlFragment) => { if (explainResults[query]) return if (explainLoadingQuery) return const requestQuery = query setExplainLoadingQuery(requestQuery) executeExplain( { projectRef: project?.ref, connectionString: project?.connectionString, sql: wrapWithRollback(safeSql`EXPLAIN ANALYZE ${requestQuery}`), }, { onSuccess(data) { setExplainResults((prev) => ({ ...prev, [requestQuery]: data.result })) setExplainLoadingQuery(null) }, onError() { setExplainLoadingQuery(null) }, } ) }, [explainResults, explainLoadingQuery, executeExplain, project] ) const handleGoToLogs = useCallback(() => { router.push(`/project/${ref}/logs/postgres-logs`) }, [router, ref]) const handleAiSuggestedFix = useCallback( (item: ClassifiedQuery) => { const { query, prompt } = buildQueryInsightFixPrompt(item) openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) aiSnap.newChat({ sqlSnippets: [{ label: 'Query', content: query }], initialMessage: prompt, }) }, [openSidebar, aiSnap] ) useEffect(() => { if (sheetView === 'explain' && activeSheetRow?.query) { runExplain(activeSheetRow.query) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [sheetView, activeSheetRow?.query]) useEffect(() => { const currentPath = router.asPath.split('?')[0] const handleRouteChange = (url: string) => { if (url.split('?')[0] !== currentPath) { setQueryStates({ search: null }) onCurrentSelectQuery?.(null) } } router.events.on('routeChangeStart', handleRouteChange) return () => router.events.off('routeChangeStart', handleRouteChange) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const timeConsumedWidth = useMemo(() => { if (!explorerItems.length) return 150 let maxWidth = 150 explorerItems.forEach((row) => { const pct = row.prop_total_time || 0 const total = row.total_time || 0 if (pct && total) { const text = `${pct.toFixed(1)}% / ${formatDuration(total)}` maxWidth = Math.max(maxWidth, text.length * 8 + 40) } }) return Math.min(maxWidth, 300) }, [explorerItems]) const triageQueryColWidth = useMemo(() => { if (!triageContainerWidth) return 380 const fixed = timeConsumedWidth + 100 + 300 + 4 return Math.max(380, triageContainerWidth - fixed - 120) }, [triageContainerWidth, timeConsumedWidth]) const { columns, triageColumns } = useQueryInsightsTableColumns({ sort, setSort, timeConsumedWidth, triageQueryColWidth, gridRef, setSelectedRow, setSelectedTriageRow, setSheetView, handleGoToLogs, handleAiSuggestedFix, }) const handleKeyDown = useCallback( (event: KeyboardEvent) => { if (!explorerItems.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 < explorerItems.length - 1) { nextIndex = selectedRow + 1 } if (nextIndex !== selectedRow) { setSelectedRow(nextIndex) gridRef.current?.scrollToCell({ idx: 0, rowIdx: nextIndex }) } }, [explorerItems, selectedRow] ) useEffect(() => { window.addEventListener('keydown', handleKeyDown, true) return () => { window.removeEventListener('keydown', handleKeyDown, true) } }, [handleKeyDown]) useEffect(() => { setSelectedRow(undefined) }, [searchQuery, sort]) useEffect(() => { if (mode === 'triage') { triageGridRef.current?.scrollToCell({ idx: 0, rowIdx: 0 }) } else { gridRef.current?.scrollToCell({ idx: 0, rowIdx: 0 }) } }, [mode]) useEffect(() => { const el = triageContainerRef.current if (!el) return const observer = new ResizeObserver(([entry]) => { setTriageContainerWidth(entry.contentRect.width) }) observer.observe(el) return () => observer.disconnect() }, []) useEffect(() => { if (urlSearch !== searchQuery) { setQueryStates({ search: searchQuery || null }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery]) const errorCount = errors.length const indexCount = indexIssues.length const slowCount = slowQueries.length return (
setMode(mode as Mode)} /> {appNameFilter.length > 0 ? ( setAppNameFilter([])} /> ) : ( )}
{mode === 'triage' ? ( setFilter(v as IssueFilter)}> All{triageItems.length > 0 && ` (${triageItems.length})`} Errors{errorCount > 0 && ` (${errorCount})`} Index{indexCount > 0 && ` (${indexCount})`} Slow{slowCount > 0 && ` (${slowCount})`} ) : ( } value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} name="search" id="search" placeholder="Search queries..." className="w-64" actions={[ searchQuery && (
{isLoading ? (
) : mode === 'triage' ? (
{ const isSelected = idx === selectedTriageRow const query = filteredTriageItems[idx]?.query const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false return [ `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : isCharted ? 'bg-surface-200 dark:bg-surface-200' : 'bg-200 hover:bg-surface-200'} cursor-pointer`, '[&>div:first-child]:border-l-4 [&>div:first-child]:pl-5 [&>div:last-child]:pr-6', `${isSelected || isCharted ? '[&>div:first-child]:border-l-foreground' : '[&>div:first-child]:border-l-transparent'}`, '[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none [&>.rdg-cell]:py-3', '[&>.rdg-cell.column-prop_total_time]:relative', ].join(' ') }} renderers={{ renderRow(idx, props) { return ( { event.stopPropagation() if (typeof idx === 'number' && idx >= 0) { const query = filteredTriageItems[idx]?.query if (query && onCurrentSelectQuery) { onCurrentSelectQuery(currentSelectedQuery === query ? null : query) } } }} /> ) }, noRowsFallback: (

No issues found

{data.length === 0 ? 'No query data available yet' : 'No issues detected for the selected filter'}

), }} />
) : (
{ const isSelected = idx === selectedRow const query = explorerItems[idx]?.query const isCharted = currentSelectedQuery ? currentSelectedQuery === query : false return [ `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : isCharted ? 'bg-surface-200 dark:bg-surface-200' : 'bg-200 hover:bg-surface-200'} cursor-pointer`, '[&>div:first-child]:border-l-4 [&>div:first-child]:pl-5 [&>div:last-child]:pr-6', `${isSelected || isCharted ? '[&>div:first-child]:border-l-foreground' : '[&>div:first-child]:border-l-transparent'}`, '[&>.rdg-cell]:box-border [&>.rdg-cell]:outline-hidden [&>.rdg-cell]:shadow-none [&>.rdg-cell]:py-3', '[&>.rdg-cell.column-prop_total_time]:relative', ].join(' ') }} renderers={{ renderRow(idx, props) { return ( { event.stopPropagation() if (typeof idx === 'number' && idx >= 0) { const query = explorerItems[idx]?.query if (query && onCurrentSelectQuery) { onCurrentSelectQuery(currentSelectedQuery === query ? null : query) } } }} /> ) }, noRowsFallback: (

No queries found

{searchQuery.trim() ? 'No queries match your search criteria' : 'No query data available yet'}

), }} />
)}
{ if (!open) { setSelectedTriageRow(undefined) setSelectedRow(undefined) } }} activeSheetRow={activeSheetRow} sheetView={sheetView} onSheetViewChange={setSheetView} onClose={() => { setSelectedTriageRow(undefined) setSelectedRow(undefined) }} dataGridContainerRef={dataGridContainerRef} triageContainerRef={triageContainerRef} explainLoadingQuery={explainLoadingQuery} explainResults={explainResults} />
) }