QueryInsightsTableRow.tsx183 lines · main
1import { Loader2 } from 'lucide-react'
2import { AiIconAnimation, Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
3
4import type { ClassifiedQuery } from '../QueryInsightsHealth/QueryInsightsHealth.types'
5import { ISSUE_DOT_COLORS, ISSUE_ICONS } from './QueryInsightsTable.constants'
6import { formatDuration, getColumnName, getTableName } from './QueryInsightsTable.utils'
7
8interface QueryInsightsTableRowProps {
9 item: ClassifiedQuery
10 onRowClick?: () => void
11 onGoToLogs?: () => void
12 onCreateIndex?: () => void
13 onExplain?: () => void
14 onAiSuggestedFix?: () => void
15 isExplainLoading?: boolean
16}
17
18export const QueryInsightsTableRow = ({
19 item,
20 onRowClick,
21 onGoToLogs,
22 onCreateIndex,
23 onExplain,
24 onAiSuggestedFix,
25 isExplainLoading,
26}: QueryInsightsTableRowProps) => {
27 const IssueIcon = item.issueType ? ISSUE_ICONS[item.issueType] : null
28
29 return (
30 <div
31 className="flex items-center gap-4 px-6 py-4 border-b hover:bg-surface-100 cursor-pointer group"
32 onClick={onRowClick}
33 >
34 {item.issueType && IssueIcon && (
35 <div
36 className={cn(
37 'h-6 w-6 rounded-full shrink-0 border flex items-center justify-center',
38 ISSUE_DOT_COLORS[item.issueType]?.border,
39 ISSUE_DOT_COLORS[item.issueType]?.background
40 )}
41 >
42 <IssueIcon size={14} className={ISSUE_DOT_COLORS[item.issueType].color} />
43 </div>
44 )}
45
46 <div className="flex-1 min-w-0">
47 <p className="text-xs font-mono text-foreground line-clamp-1">
48 <span className="text-foreground">{item.queryType ?? '–'}</span>
49 {getTableName(item.query) && (
50 <>
51 {' '}
52 <span className="text-foreground-lighter">in</span> {getTableName(item.query)}
53 </>
54 )}
55 {getColumnName(item.query) && (
56 <>
57 <span className="text-foreground-lighter">,</span> {getColumnName(item.query)}
58 </>
59 )}
60 </p>
61 <p
62 className={cn(
63 'text-xs mt-0.5 font-mono line-clamp-1',
64 item.issueType === 'error' && 'text-destructive-600',
65 item.issueType === 'index' && 'text-warning-600',
66 item.issueType === 'slow' && 'text-foreground-lighter'
67 )}
68 >
69 {item.hint}
70 </p>
71 </div>
72
73 <div className="flex items-stretch divide-x divide-border shrink-0 tabular-nums">
74 <Tooltip>
75 <TooltipTrigger asChild>
76 <div className="flex flex-col items-end pr-4 cursor-default">
77 <span
78 className={cn(
79 'text-sm font-mono leading-snug',
80 item.mean_time >= 1000 && 'text-destructive-600'
81 )}
82 >
83 {formatDuration(item.mean_time)}
84 </span>
85 <span className="text-[10px] text-foreground-muted uppercase tracking-wide leading-snug">
86 avg
87 </span>
88 </div>
89 </TooltipTrigger>
90 <TooltipContent side="top" className="max-w-[220px] text-center">
91 Average execution time per call. High mean time means individual runs are slow —
92 directly felt by users.
93 </TooltipContent>
94 </Tooltip>
95
96 <Tooltip>
97 <TooltipTrigger asChild>
98 <div className="flex flex-col items-end px-4 cursor-default">
99 <span className="text-sm font-mono leading-snug">
100 {item.prop_total_time.toFixed(1)}%
101 </span>
102 <span className="text-[10px] text-foreground-muted uppercase tracking-wide leading-snug">
103 of db
104 </span>
105 </div>
106 </TooltipTrigger>
107 <TooltipContent side="top" className="max-w-[220px] text-center">
108 Percentage of total database execution time. Fixing high-impact queries has the biggest
109 overall effect on your database.
110 </TooltipContent>
111 </Tooltip>
112
113 <Tooltip>
114 <TooltipTrigger asChild>
115 <div className="flex flex-col items-end pl-4 cursor-default">
116 <span className="text-sm font-mono leading-snug">{item.calls.toLocaleString()}</span>
117 <span className="text-[10px] text-foreground-muted uppercase tracking-wide leading-snug">
118 calls
119 </span>
120 </div>
121 </TooltipTrigger>
122 <TooltipContent side="top" className="max-w-[220px] text-center">
123 Number of times this query ran in the selected time window.
124 </TooltipContent>
125 </Tooltip>
126 </div>
127
128 <div className="flex items-center gap-2 shrink-0 justify-end w-[260px]">
129 <Button
130 type="default"
131 size="tiny"
132 onClick={(e) => {
133 e.stopPropagation()
134 onGoToLogs?.()
135 }}
136 >
137 Go to Logs
138 </Button>
139
140 {(item.issueType === 'index' || item.issueType === 'slow') && (
141 <Button
142 type="default"
143 size="tiny"
144 icon={isExplainLoading ? <Loader2 size={12} className="animate-spin" /> : undefined}
145 onClick={(e) => {
146 e.stopPropagation()
147 onExplain?.()
148 }}
149 >
150 Explain
151 </Button>
152 )}
153
154 {item.issueType === 'index' && (
155 <Button
156 type="primary"
157 size="tiny"
158 onClick={(e) => {
159 e.stopPropagation()
160 onCreateIndex?.()
161 }}
162 >
163 Create Index
164 </Button>
165 )}
166
167 {(item.issueType === 'error' || item.issueType === 'slow') && (
168 <Button
169 type="default"
170 size="tiny"
171 icon={<AiIconAnimation size={14} />}
172 onClick={(e) => {
173 e.stopPropagation()
174 onAiSuggestedFix?.()
175 }}
176 >
177 Fix with AI
178 </Button>
179 )}
180 </div>
181 </div>
182 )
183}