QueryDetail.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { ChevronsUpDown, Lightbulb } from 'lucide-react'
  2. import dynamic from 'next/dynamic'
  3. import { useEffect, useState } from 'react'
  4. import { Alert, AlertDescription, AlertTitle, Button, cn } from 'ui'
  5. import { QueryPanelContainer, QueryPanelSection } from './QueryPanel'
  6. import { buildQueryExplanationPrompt } from './QueryPerformance.ai'
  7. import { QUERY_PERFORMANCE_COLUMNS } from './QueryPerformance.constants'
  8. import { QueryPerformanceRow } from './QueryPerformance.types'
  9. import { formatDuration } from './QueryPerformance.utils'
  10. import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
  11. import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
  12. import { formatSql } from '@/lib/formatSql'
  13. import { useTrack } from '@/lib/telemetry/track'
  14. import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
  15. import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
  16. interface QueryDetailProps {
  17. selectedRow?: QueryPerformanceRow
  18. onClickViewSuggestion: () => void
  19. onClose?: () => void
  20. }
  21. // Load SqlMonacoBlock (monaco editor) client-side only (does not behave well server-side)
  22. const SqlMonacoBlock = dynamic(
  23. () => import('./SqlMonacoBlock').then(({ SqlMonacoBlock }) => SqlMonacoBlock),
  24. {
  25. ssr: false,
  26. }
  27. )
  28. export const QueryDetail = ({ selectedRow, onClickViewSuggestion, onClose }: QueryDetailProps) => {
  29. // [Joshen] TODO implement this logic once the linter rules are in
  30. const isLinterWarning = false
  31. const report = QUERY_PERFORMANCE_COLUMNS
  32. const [query, setQuery] = useState(selectedRow?.['query'])
  33. const { openSidebar } = useSidebarManagerSnapshot()
  34. const aiSnap = useAiAssistantStateSnapshot()
  35. const track = useTrack()
  36. useEffect(() => {
  37. if (selectedRow !== undefined) {
  38. const formattedQuery = formatSql(selectedRow['query'])
  39. setQuery(formattedQuery)
  40. }
  41. }, [selectedRow])
  42. const [isExpanded, setIsExpanded] = useState(false)
  43. const handleExplainQuery = () => {
  44. if (!selectedRow?.query) return
  45. const { query, prompt } = buildQueryExplanationPrompt(selectedRow)
  46. openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
  47. aiSnap.newChat({
  48. sqlSnippets: [
  49. {
  50. label: 'Query',
  51. content: query,
  52. },
  53. ],
  54. initialMessage: prompt,
  55. })
  56. track('query_performance_explain_with_ai_button_clicked')
  57. // Close the query detail panel since we need to see the AI assistant panel
  58. onClose?.()
  59. }
  60. const buildPromptForCopy = () => {
  61. if (!selectedRow?.query) return ''
  62. const { query, prompt } = buildQueryExplanationPrompt(selectedRow)
  63. return `${prompt}\n\nSQL Query:\n\`\`\`sql\n${query}\n\`\`\``
  64. }
  65. return (
  66. <QueryPanelContainer>
  67. <QueryPanelSection className="pt-2 border-b relative">
  68. <div className="flex items-center justify-between mb-4">
  69. <h4>Query pattern</h4>
  70. <AiAssistantDropdown
  71. label="Explain with AI"
  72. buildPrompt={buildPromptForCopy}
  73. onOpenAssistant={handleExplainQuery}
  74. telemetrySource="query_performance"
  75. size="tiny"
  76. type="default"
  77. />
  78. </div>
  79. <div
  80. className={cn(
  81. 'overflow-hidden pb-0 z-0 relative transition-all duration-300',
  82. isExpanded ? 'h-[348px]' : 'h-[120px]'
  83. )}
  84. >
  85. <SqlMonacoBlock
  86. value={query}
  87. height={322}
  88. lineNumbers="off"
  89. wrapperClassName={cn('pl-3 bg-surface-100', !isExpanded && 'pointer-events-none')}
  90. />
  91. {isLinterWarning && (
  92. <Alert
  93. variant="default"
  94. className="mt-2 border-brand-400 bg-alternative [&>svg]:p-0.5 [&>svg]:bg-transparent [&>svg]:text-brand"
  95. >
  96. <Lightbulb />
  97. <AlertTitle>Suggested optimization: Add an index</AlertTitle>
  98. <AlertDescription>
  99. Adding an index will help this query execute faster
  100. </AlertDescription>
  101. <AlertDescription>
  102. <Button className="mt-3" onClick={() => onClickViewSuggestion()}>
  103. View suggestion
  104. </Button>
  105. </AlertDescription>
  106. </Alert>
  107. )}
  108. </div>
  109. <div
  110. className={cn(
  111. 'absolute left-0 bottom-0 w-full bg-linear-to-t from-black/30 to-transparent h-24 transition-opacity duration-300',
  112. isExpanded && 'opacity-0 pointer-events-none'
  113. )}
  114. />
  115. <div className="absolute bottom-[-13px] left-0 right-0 w-full flex items-center justify-center z-10">
  116. <Button
  117. type="default"
  118. className="rounded-full"
  119. icon={<ChevronsUpDown />}
  120. onClick={() => setIsExpanded(!isExpanded)}
  121. >
  122. {isExpanded ? 'Collapse' : 'Expand'}
  123. </Button>
  124. </div>
  125. </QueryPanelSection>
  126. <QueryPanelSection className="pb-3 pt-6">
  127. <h4 className="mb-4">Metadata</h4>
  128. <ul className="flex flex-col gap-y-3 divide-y divide-dashed">
  129. {report
  130. .filter((x) => x.id !== 'query')
  131. .map((x) => {
  132. const rawValue = selectedRow?.[x.id]
  133. const isTime = x.name.includes('time')
  134. const formattedValue = isTime
  135. ? typeof rawValue === 'number' && !isNaN(rawValue) && isFinite(rawValue)
  136. ? `${Math.round(rawValue).toLocaleString()}ms`
  137. : 'n/a'
  138. : rawValue != null
  139. ? String(rawValue)
  140. : 'n/a'
  141. if (x.id === 'prop_total_time') {
  142. const percentage = selectedRow?.prop_total_time || 0
  143. const totalTime = selectedRow?.total_time || 0
  144. return (
  145. <li key={x.id} className="flex justify-between pb-3 text-sm">
  146. <p className="text-foreground-light">{x.name}</p>
  147. {percentage && totalTime ? (
  148. <p className="flex items-center gap-x-1.5">
  149. <span
  150. className={cn(
  151. 'tabular-nums',
  152. percentage.toFixed(1) === '0.0' && 'text-foreground-lighter'
  153. )}
  154. >
  155. {percentage.toFixed(1)}%
  156. </span>{' '}
  157. <span className="text-muted">/</span>{' '}
  158. <span
  159. className={cn(
  160. 'tabular-nums',
  161. formatDuration(totalTime) === '0.00s' && 'text-foreground-lighter'
  162. )}
  163. >
  164. {formatDuration(totalTime)}
  165. </span>
  166. </p>
  167. ) : (
  168. <p className="text-muted">&ndash;</p>
  169. )}
  170. </li>
  171. )
  172. }
  173. if (x.id == 'rows_read') {
  174. return (
  175. <li key={x.id} className="flex justify-between pb-3 text-sm">
  176. <p className="text-foreground-light">{x.name}</p>
  177. {typeof rawValue === 'number' && !isNaN(rawValue) && isFinite(rawValue) ? (
  178. <p
  179. className={cn('tabular-nums', rawValue === 0 && 'text-foreground-lighter')}
  180. >
  181. {rawValue.toLocaleString()}
  182. </p>
  183. ) : (
  184. <p className="text-muted">&ndash;</p>
  185. )}
  186. </li>
  187. )
  188. }
  189. const cacheHitRateToNumber = (value: number | string) => {
  190. if (typeof value === 'number') return value
  191. return parseFloat(value.toString().replace('%', '')) || 0
  192. }
  193. if (x.id === 'cache_hit_rate') {
  194. return (
  195. <li key={x.id} className="flex justify-between pb-3 text-sm">
  196. <p className="text-foreground-light">{x.name}</p>
  197. {typeof rawValue === 'string' || typeof rawValue === 'number' ? (
  198. <p
  199. className={cn(
  200. cacheHitRateToNumber(rawValue).toFixed(2) === '0.00' &&
  201. 'text-foreground-lighter'
  202. )}
  203. >
  204. {cacheHitRateToNumber(rawValue).toLocaleString(undefined, {
  205. minimumFractionDigits: 2,
  206. maximumFractionDigits: 2,
  207. })}
  208. %
  209. </p>
  210. ) : (
  211. <p className="text-muted">&ndash;</p>
  212. )}
  213. </li>
  214. )
  215. }
  216. return (
  217. <li key={x.id} className="flex justify-between pb-3 text-sm">
  218. <p className="text-foreground-light">{x.name}</p>
  219. <p className={cn('tabular-nums', x.id === 'rolname' && 'font-mono')}>
  220. {formattedValue}
  221. </p>
  222. </li>
  223. )
  224. })}
  225. </ul>
  226. </QueryPanelSection>
  227. </QueryPanelContainer>
  228. )
  229. }