Files
supabase/apps/studio/components/interfaces/QueryInsights/hooks/useQueryInsightsTableColumns.tsx
Ivan Vasilov 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

600 lines
22 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ArrowDown, ArrowRight, ArrowUp, ChevronDown, ExternalLink, ScanSearch } from 'lucide-react'
import { useMemo, type RefObject } from 'react'
// eslint-disable-next-line no-restricted-imports
import { type Column, type DataGridHandle } from 'react-data-grid'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
import { buildQueryInsightFixPrompt } from '../../QueryPerformance/QueryPerformance.ai'
import { QUERY_PERFORMANCE_ROLE_DESCRIPTION } from '../../QueryPerformance/QueryPerformance.constants'
import type { ClassifiedQuery } from '../QueryInsightsHealth/QueryInsightsHealth.types'
import {
ISSUE_DOT_COLORS,
ISSUE_ICONS,
NON_SORTABLE_COLUMNS,
QUERY_INSIGHTS_EXPLORER_COLUMNS,
} from '../QueryInsightsTable/QueryInsightsTable.constants'
import {
formatDuration,
getColumnName,
getTableName,
} from '../QueryInsightsTable/QueryInsightsTable.utils'
import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
interface UseQueryInsightsTableColumnsParams {
sort: { column: string; order: 'asc' | 'desc' }
setSort: (config: { column: string; order: 'asc' | 'desc' } | null) => void
timeConsumedWidth: number
triageQueryColWidth: number
gridRef: RefObject<DataGridHandle | null>
setSelectedRow: (idx: number) => void
setSelectedTriageRow: (idx: number | undefined) => void
setSheetView: (view: 'details' | 'indexes' | 'explain') => void
handleGoToLogs: () => void
handleAiSuggestedFix: (item: ClassifiedQuery) => void
}
export function useQueryInsightsTableColumns({
sort,
setSort,
timeConsumedWidth,
triageQueryColWidth,
gridRef,
setSelectedRow,
setSelectedTriageRow,
setSheetView,
handleGoToLogs,
handleAiSuggestedFix,
}: UseQueryInsightsTableColumnsParams): {
columns: Column<ClassifiedQuery>[]
triageColumns: Column<ClassifiedQuery>[]
} {
const columns = useMemo(() => {
return QUERY_INSIGHTS_EXPLORER_COLUMNS.map((col) => {
const isSortable = !NON_SORTABLE_COLUMNS.includes(col.id as never)
const result: Column<ClassifiedQuery> = {
key: col.id,
name: col.name,
cellClass: `column-${col.id}`,
resizable: true,
minWidth: col.id === 'prop_total_time' ? timeConsumedWidth : (col.minWidth ?? 120),
sortable: isSortable,
headerCellClass: 'first:pl-6 cursor-pointer',
renderHeaderCell: () => {
return (
<div className="flex items-center justify-between text-xs w-full">
<div className="flex items-center gap-x-2">
<p className="text-foreground! font-medium">{col.name}</p>
{col.description && (
<p className="text-foreground-lighter font-normal">{col.description}</p>
)}
</div>
{isSortable && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="text"
size="tiny"
className="p-1 h-5 w-5 shrink-0"
icon={<ChevronDown size={14} className="text-foreground-muted" />}
onClick={(e) => e.stopPropagation()}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={() => setSort({ column: col.id, order: 'asc' })}
className={cn(
'flex gap-2',
sort?.column === col.id && sort?.order === 'asc' && 'text-foreground'
)}
>
<ArrowUp size={14} />
Sort Ascending
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setSort({ column: col.id, order: 'desc' })}
className={cn(
'flex gap-2',
sort?.column === col.id && sort?.order === 'desc' && 'text-foreground'
)}
>
<ArrowDown size={14} />
Sort Descending
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
)
},
renderCell: (props) => {
const row = props.row
const value = row[col.id]
if (col.id === 'query') {
const IssueIcon = row.issueType ? ISSUE_ICONS[row.issueType] : null
return (
<div className="w-full flex items-center gap-x-3 group">
<div className="shrink-0 w-6">
{row.issueType && IssueIcon && (
<Tooltip>
<TooltipTrigger asChild>
<div
className={cn(
'h-6 w-6 rounded-full border flex items-center justify-center cursor-default',
ISSUE_DOT_COLORS[row.issueType]?.border,
ISSUE_DOT_COLORS[row.issueType]?.background
)}
>
<IssueIcon size={14} className={ISSUE_DOT_COLORS[row.issueType].color} />
</div>
</TooltipTrigger>
{row.hint && (
<TooltipContent side="top" className="max-w-[260px]">
{row.hint}
</TooltipContent>
)}
</Tooltip>
)}
</div>
<CodeBlock
language="pgsql"
className="bg-transparent! p-0! m-0! border-none! whitespace-nowrap! [&>code]:whitespace-nowrap! [&>code]:wrap-break-word overflow-visible! truncate! w-full! pr-20! pointer-events-none"
wrapperClassName="max-w-full! flex-1"
hideLineNumbers
hideCopy
value={typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : ''}
wrapLines={false}
/>
<ButtonTooltip
tooltip={{ content: { text: 'Query details' } }}
icon={<ArrowRight size={14} />}
size="tiny"
type="default"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
setSelectedRow(props.rowIdx)
setSheetView('details')
gridRef.current?.scrollToCell({ idx: 0, rowIdx: props.rowIdx })
}}
className="p-1 shrink-0 -translate-x-2 group-hover:flex hidden"
/>
</div>
)
}
if (col.id === 'prop_total_time') {
const percentage = row.prop_total_time || 0
const totalTime = row.total_time || 0
const fillWidth = Math.min(percentage, 100)
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
<div
className="absolute inset-0 bg-foreground transition-all duration-200 z-0"
style={{ width: `${fillWidth}%`, opacity: 0.04 }}
/>
{percentage && totalTime ? (
<span className="flex items-center justify-end gap-x-1.5">
<span
className={cn(percentage.toFixed(1) === '0.0' && 'text-foreground-lighter')}
>
{percentage.toFixed(1)}%
</span>
<span className="text-muted">/</span>
<span
className={cn(
formatDuration(totalTime) === '0ms' && 'text-foreground-lighter'
)}
>
{formatDuration(totalTime)}
</span>
</span>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
if (col.id === 'calls') {
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
<p className={cn(value === 0 && 'text-foreground-lighter')}>
{value.toLocaleString()}
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
if (col.id === 'max_time' || col.id === 'mean_time' || col.id === 'min_time') {
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
<p className={cn(value.toFixed(0) === '0' && 'text-foreground-lighter')}>
{Math.round(value).toLocaleString()}ms
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
if (col.id === 'rows_read') {
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
<p className={cn(value === 0 && 'text-foreground-lighter')}>
{value.toLocaleString()}
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
if (col.id === 'cache_hit_rate') {
const num = typeof value === 'number' ? value : parseFloat(value ?? '0')
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof num === 'number' && !isNaN(num) && isFinite(num) ? (
<p className={cn(num.toFixed(2) === '0.00' && 'text-foreground-lighter')}>
{num.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
%
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
if (col.id === 'rolname') {
return (
<div className="w-full flex flex-col justify-center">
{value ? (
<span className="flex items-center gap-x-1">
<p className="font-mono text-xs">{value}</p>
<InfoTooltip align="end" alignOffset={-12} className="w-56">
{
QUERY_PERFORMANCE_ROLE_DESCRIPTION.find((r) => r.name === value)
?.description
}
</InfoTooltip>
</span>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
if (col.id === 'application_name') {
return (
<div className="w-full flex flex-col justify-center">
{value ? (
<p className="font-mono text-xs">{value}</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
}
return null
},
}
return result
})
}, [sort, setSort, timeConsumedWidth, gridRef, setSelectedRow, setSheetView])
const triageColumns = useMemo(
(): Column<ClassifiedQuery>[] => [
{
key: 'query',
name: 'Query',
minWidth: triageQueryColWidth,
width: triageQueryColWidth,
resizable: true,
headerCellClass: 'first:pl-6 cursor-default',
renderHeaderCell: () => (
<div className="flex items-center text-xs w-full">
<p className="text-foreground! font-medium">Query</p>
</div>
),
renderCell: (props) => {
const row = props.row as ClassifiedQuery
const IssueIcon = row.issueType ? ISSUE_ICONS[row.issueType] : null
return (
<div className="w-full flex items-center gap-x-3 group">
<div className="shrink-0 w-6">
{row.issueType && IssueIcon && (
<div
className={cn(
'h-6 w-6 rounded-full border flex items-center justify-center',
ISSUE_DOT_COLORS[row.issueType]?.border,
ISSUE_DOT_COLORS[row.issueType]?.background
)}
>
<IssueIcon size={14} className={ISSUE_DOT_COLORS[row.issueType].color} />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-mono text-foreground truncate">
{row.queryType ?? ''}
{getTableName(row.query) && (
<>
{' '}
<span className="text-foreground-lighter">in</span> {getTableName(row.query)}
</>
)}
{getColumnName(row.query) && (
<>
<span className="text-foreground-lighter">,</span> {getColumnName(row.query)}
</>
)}
</p>
<p
className={cn(
'text-xs mt-0.5 font-mono truncate',
row.issueType === 'error' && 'text-destructive-600',
row.issueType === 'index' && 'text-warning-600',
row.issueType === 'slow' && 'text-foreground-lighter'
)}
>
{row.hint}
</p>
</div>
<ButtonTooltip
tooltip={{ content: { text: 'Query details' } }}
icon={<ArrowRight size={14} />}
size="tiny"
type="default"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
setSelectedTriageRow(props.rowIdx)
setSheetView('details')
}}
className="p-1 shrink-0 group-hover:flex hidden"
/>
</div>
)
},
},
{
key: 'prop_total_time',
name: 'Time consumed',
minWidth: timeConsumedWidth,
resizable: true,
cellClass: 'column-prop_total_time',
headerCellClass: 'cursor-default',
renderHeaderCell: () => (
<div className="flex items-center text-xs w-full">
<p className="text-foreground! font-medium">Time consumed</p>
</div>
),
renderCell: (props) => {
const row = props.row as ClassifiedQuery
const percentage = row.prop_total_time || 0
const totalTime = row.total_time || 0
const fillWidth = Math.min(percentage, 100)
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
<div
className="absolute inset-0 bg-foreground transition-all duration-200 z-0"
style={{ width: `${fillWidth}%`, opacity: 0.04 }}
/>
{percentage && totalTime ? (
<span className="flex items-center justify-end gap-x-1.5">
<span
className={cn(percentage.toFixed(1) === '0.0' && 'text-foreground-lighter')}
>
{percentage.toFixed(1)}%
</span>
<span className="text-muted">/</span>
<span
className={cn(formatDuration(totalTime) === '0ms' && 'text-foreground-lighter')}
>
{formatDuration(totalTime)}
</span>
</span>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
},
},
{
key: 'calls',
name: 'Calls',
minWidth: 90,
resizable: true,
headerCellClass: 'cursor-default',
renderHeaderCell: () => (
<div className="flex items-center text-xs w-full">
<p className="text-foreground! font-medium">Calls</p>
</div>
),
renderCell: (props) => {
const value = (props.row as ClassifiedQuery).calls
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
<p className={cn(value === 0 && 'text-foreground-lighter')}>
{value.toLocaleString()}
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
},
},
{
key: 'mean_time',
name: 'Mean time',
minWidth: 90,
resizable: true,
headerCellClass: 'cursor-default',
renderHeaderCell: () => (
<div className="flex items-center text-xs w-full">
<p className="text-foreground! font-medium">Mean time</p>
</div>
),
renderCell: (props) => {
const value = (props.row as ClassifiedQuery).mean_time
return (
<div className="w-full flex flex-col justify-center text-xs text-right tabular-nums font-mono">
{typeof value === 'number' && !isNaN(value) && isFinite(value) ? (
<p className={cn(value === 0 && 'text-foreground-lighter')}>
{formatDuration(value)}
</p>
) : (
<p className="text-muted">&ndash;</p>
)}
</div>
)
},
},
{
key: 'actions',
name: 'Actions',
minWidth: 200,
resizable: false,
headerCellClass: 'cursor-default',
renderHeaderCell: () => (
<div className="flex items-center text-xs w-full">
<p className="text-foreground! font-medium">Actions</p>
</div>
),
renderCell: (props) => {
const row = props.row as ClassifiedQuery
return (
<div className="flex items-center gap-2 justify-end w-full h-full">
{!row.issueType && (
<Button
type="default"
size="tiny"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
handleGoToLogs()
}}
>
Go to Logs
</Button>
)}
{row.issueType === 'index' && (
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>
<Button
type="primary"
size="tiny"
className="rounded-r-none border-r-0"
onClick={() => {
setSelectedTriageRow(props.rowIdx)
setSheetView('indexes')
}}
>
Create Index
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="primary"
size="tiny"
className="rounded-l-none px-1"
icon={<ChevronDown size={12} />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={() => handleGoToLogs()} className="gap-2">
<ExternalLink size={14} />
Go to Logs
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelectedTriageRow(props.rowIdx)
setSheetView('explain')
}}
className="gap-2"
>
<ScanSearch size={14} />
Explain
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{(row.issueType === 'error' || row.issueType === 'slow') && (
<div onClick={(e) => e.stopPropagation()}>
<AiAssistantDropdown
label="Fix with AI"
buildPrompt={() => buildQueryInsightFixPrompt(row).prompt}
onOpenAssistant={() => handleAiSuggestedFix(row)}
copyLabel="Copy Markdown"
extraDropdownItems={
<>
<DropdownMenuItem onClick={() => handleGoToLogs()} className="gap-2">
<ExternalLink size={14} />
Go to Logs
</DropdownMenuItem>
{row.issueType === 'slow' && (
<DropdownMenuItem
onClick={() => {
setSelectedTriageRow(props.rowIdx)
setSheetView('explain')
}}
className="gap-2"
>
<ScanSearch size={14} />
Explain
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
</>
}
/>
</div>
)}
</div>
)
},
},
],
[
triageQueryColWidth,
timeConsumedWidth,
handleGoToLogs,
handleAiSuggestedFix,
setSelectedTriageRow,
setSheetView,
]
)
return { columns, triageColumns }
}