| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- 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,
- 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">–</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">–</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">–</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">–</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">–</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">–</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">–</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">–</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">–</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">–</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"
- additionalDropdownItems={[
- {
- label: 'Go to Logs',
- icon: <ExternalLink size={14} />,
- onClick: () => handleGoToLogs(),
- },
- ...(row.issueType === 'slow'
- ? [
- {
- label: 'Explain',
- icon: <ScanSearch size={14} />,
- onClick: () => {
- setSelectedTriageRow(props.rowIdx)
- setSheetView('explain')
- },
- },
- ]
- : []),
- ]}
- />
- </div>
- )}
- </div>
- )
- },
- },
- ],
- [
- triageQueryColWidth,
- timeConsumedWidth,
- handleGoToLogs,
- handleAiSuggestedFix,
- setSelectedTriageRow,
- setSheetView,
- ]
- )
- return { columns, triageColumns }
- }
|