Files
supabase/apps/studio/components/interfaces/QueryInsights/QueryList.tsx
T
kemal 6d171ccaa7 chore: removing unused code
AI generates stuff we dont need so removing it all.
2025-08-11 18:19:52 +01:00

418 lines
14 KiB
TypeScript

import { useRouter } from 'next/router'
import { useRef, useState, useEffect } from 'react'
import DataGrid, { Column, DataGridHandle, Row } from 'react-data-grid'
import { QueryInsightsQuery } from 'data/query-insights/query-insights-query'
import dayjs from 'dayjs'
import { cn, CodeBlock, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { TextSearch } from 'lucide-react'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
interface QueryListProps {
queries: QueryInsightsQuery[]
isLoading: boolean
onQuerySelect?: (query: QueryInsightsQuery | null) => void
onQueryHover?: (query: QueryInsightsQuery | null) => void
selectedQuery?: QueryInsightsQuery | null
hoveredQuery?: QueryInsightsQuery | null
}
export const QueryList = ({
queries,
isLoading,
onQuerySelect,
onQueryHover,
selectedQuery,
hoveredQuery,
}: QueryListProps) => {
const router = useRouter()
const gridRef = useRef<DataGridHandle>(null)
const [selectedRow, setSelectedRow] = useState<number | undefined>()
const [hoveredRow, setHoveredRow] = useState<number | undefined>()
const normalizeWhitespace = (sql: string) => {
return sql.replace(/\s+/g, ' ').trim()
}
// Update selected row when selectedQuery changes from parent
useEffect(() => {
if (selectedQuery) {
const idx = queries.findIndex((q) => q.query_id === selectedQuery.query_id)
if (idx !== -1) {
setSelectedRow(idx)
}
} else {
setSelectedRow(undefined)
}
}, [selectedQuery, queries])
// Handler for row click
const handleRowClick = (idx: number) => {
if (isNaN(idx) || idx < 0 || idx >= queries.length) return
const query = queries[idx]
const isAlreadySelected = selectedQuery?.query_id === query.query_id
// If the query is already selected, deselect it
if (isAlreadySelected) {
setSelectedRow(undefined)
onQuerySelect?.(null)
} else {
// Otherwise, select the new query
setSelectedRow(idx)
onQuerySelect?.(query)
}
// Scroll to keep the row in view
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
}
// Handler for row hover
const handleRowMouseEnter = (idx: number) => {
if (isNaN(idx) || idx < 0 || idx >= queries.length) return
if (selectedQuery) return // Don't trigger hover when a query is already selected
const query = queries[idx]
setHoveredRow(idx)
onQueryHover?.(query)
}
const handleRowMouseLeave = () => {
if (selectedQuery) return // Don't clear hover when a query is already selected
setHoveredRow(undefined)
onQueryHover?.(null)
}
const rowClassRender = (row: QueryInsightsQuery) => {
const isSelected = selectedQuery?.query_id === row.query_id
const isHovered = hoveredQuery?.query_id === row.query_id && !selectedQuery
return cn(
'cursor-pointer transition-colors',
isSelected ? 'bg-surface-300' : isHovered ? 'bg-surface-200' : ''
)
}
const columns: Column<QueryInsightsQuery>[] = [
{
key: 'query',
name: 'Query',
resizable: true,
minWidth: 400,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Query</p>
</div>
),
renderCell: (props) => (
<div className="font-mono text-xs truncate w-full overflow-hidden flex items-center">
<CodeBlock
language="sql"
className="!bg-transparent !p-0 !m-0 !border-none !whitespace-nowrap !overflow-visible"
hideLineNumbers
hideCopy
>
{normalizeWhitespace(props.row.query)}
</CodeBlock>
</div>
),
},
{
key: 'calls',
name: 'Calls',
resizable: true,
minWidth: 100,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Calls</p>
</div>
),
renderCell: (props) => (
<div className="font-mono text-xs text-right flex items-center justify-end h-full w-full">
{props.row.calls.toLocaleString()}
</div>
),
},
{
key: 'total_time',
name: 'Total Time',
resizable: true,
minWidth: 120,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Total Time</p>
</div>
),
renderCell: (props) => (
<div className="font-mono text-xs text-right flex flex-col justify-center h-full w-full">
<p>{props.row.total_time.toFixed(0)}ms</p>
<p className="text-foreground-lighter">{(props.row.total_time / 1000).toFixed(2)}s</p>
</div>
),
},
{
key: 'mean_exec_time',
name: 'Mean Time',
resizable: true,
minWidth: 120,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Mean Time</p>
</div>
),
renderCell: (props) => (
<div className="font-mono text-xs text-right flex flex-col justify-center h-full w-full">
<p>{props.row.mean_exec_time.toFixed(0)}ms</p>
<p className="text-foreground-lighter">{(props.row.mean_exec_time / 1000).toFixed(2)}s</p>
</div>
),
},
{
key: 'rows_read',
name: 'Rows Read',
resizable: true,
minWidth: 100,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Rows Read</p>
</div>
),
renderCell: (props) => (
<div
className={cn(
'font-mono text-xs text-right flex items-center justify-end h-full w-full',
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
)}
>
{((props.row.rows_read ?? 0) > 0 ? props.row.rows_read : 0).toLocaleString()}
</div>
),
},
{
key: 'rows_insert',
name: 'Rows Insert',
resizable: true,
minWidth: 100,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Rows Insert</p>
</div>
),
renderCell: (props) => (
<div
className={cn(
'font-mono text-xs text-right flex items-center justify-end h-full w-full',
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
)}
>
{((props.row.rows_insert ?? 0) > 0 ? props.row.rows_insert : 0).toLocaleString()}
</div>
),
},
{
key: 'rows_update',
name: 'Rows Update',
resizable: true,
minWidth: 100,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Rows Update</p>
</div>
),
renderCell: (props) => (
<div
className={cn(
'font-mono text-xs text-right flex items-center justify-end h-full w-full',
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
)}
>
{((props.row.rows_update ?? 0) > 0 ? props.row.rows_update : 0).toLocaleString()}
</div>
),
},
{
key: 'rows_delete',
name: 'Rows Delete',
resizable: true,
minWidth: 100,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Rows Delete</p>
</div>
),
renderCell: (props) => (
<div
className={cn(
'font-mono text-xs text-right flex items-center justify-end h-full w-full',
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
)}
>
{((props.row.rows_delete ?? 0) > 0 ? props.row.rows_delete : 0).toLocaleString()}
</div>
),
},
{
key: 'application_name',
name: 'Application',
resizable: true,
minWidth: 120,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Application</p>
</div>
),
renderCell: (props) => (
<div className="font-mono text-xs flex items-center h-full">
{props.row.application_name || 'Unknown'}
</div>
),
},
{
key: 'badness_score',
name: 'Badness Score',
resizable: true,
minWidth: 120,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Badness Score</p>
</div>
),
renderCell: (props) => (
<div className="font-mono text-xs text-right flex items-center justify-end h-full w-full">
{(props.row.badness_score ?? 0).toFixed(2)}
</div>
),
},
{
key: 'index_recommendations',
name: 'Index Recommendations',
resizable: true,
minWidth: 160,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Index Recommendations</p>
</div>
),
renderCell: (props) => {
const indexStatements = props.row.index_statements
const hasRecommendations =
indexStatements !== undefined &&
Array.isArray(indexStatements) &&
indexStatements.length > 0
return (
<div className="font-mono text-xs flex items-center justify-end h-full w-full">
{hasRecommendations ? (
<span className="px-2 py-1 bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-100 rounded">
{indexStatements.length} suggestions
</span>
) : props.row.query.toLowerCase().startsWith('select') ? (
<span className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100 rounded">
Optimized
</span>
) : (
<span className="text-foreground-lighter">N/A</span>
)}
</div>
)
},
},
{
key: 'errors',
name: 'Slow Queries',
resizable: true,
minWidth: 120,
headerCellClass: '',
renderHeaderCell: () => (
<div className="flex items-center font-mono font-normal text-xs w-full">
<p className="!text-foreground">Slow Queries</p>
</div>
),
renderCell: (props) => {
const errorCount = props.row.error_count || 0
if (errorCount === 0) {
return (
<div className="font-mono text-xs flex items-center justify-end h-full w-full">
<span className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100 rounded">
No issues
</span>
</div>
)
}
return (
<div className="font-mono text-xs flex items-center justify-end h-full w-full">
<span className="px-2 py-1 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100 rounded">
{errorCount} slow query{errorCount !== 1 ? 's' : ''}
</span>
</div>
)
},
},
]
return (
<div className="border rounded-md bg-surface-100 flex-grow flex flex-col">
<DataGrid
ref={gridRef}
style={{ height: '100%' }}
className={cn('flex-1')}
rowHeight={44}
headerRowHeight={36}
columns={columns}
rows={queries}
rowClass={(row, idx) => {
// Use the query_id for comparison instead of row index
const isSelected = selectedQuery?.query_id === row.query_id
return [
rowClassRender(row),
`${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`,
'[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
'[&>.rdg-cell:first-child>div]:ml-4',
].join(' ')
}}
renderers={{
renderRow: (rowIdx, props) => {
const idx = typeof rowIdx === 'number' ? rowIdx : Number(rowIdx)
return (
<Row
{...props}
key={`qi-row-${rowIdx}`}
onClick={() => handleRowClick(idx)}
onMouseEnter={() => handleRowMouseEnter(idx)}
onMouseLeave={handleRowMouseLeave}
/>
)
},
noRowsFallback: isLoading ? (
<div className="absolute top-14 px-6 w-full">
<GenericSkeletonLoader />
</div>
) : (
<div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
<TextSearch className="text-foreground-muted" strokeWidth={1} />
<div className="text-center">
<p className="text-foreground">No queries detected</p>
<p className="text-foreground-light">
There are no actively running queries that match the criteria
</p>
</div>
</div>
),
}}
/>
</div>
)
}