QueryPerformanceMetrics.tsx115 lines · main
1import { Info } from 'lucide-react'
2import { parseAsJson, useQueryStates } from 'nuqs'
3import React, { useMemo } from 'react'
4import { cn, Skeleton, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
5
6import { useQueryPerformanceQuery } from './useQueryPerformanceQuery'
7import { NumericFilter } from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
8
9export const QueryPerformanceMetrics = () => {
10 const { data: queryMetrics, isLoading } = useQueryPerformanceQuery({ preset: 'queryMetrics' })
11
12 const [, setSearchParams] = useQueryStates({
13 totalTimeFilter: parseAsJson<NumericFilter | null>((value) =>
14 value === null || value === undefined ? null : (value as NumericFilter)
15 ),
16 })
17
18 const stats = useMemo(() => {
19 const slowQueriesTitle = queryMetrics?.[0]?.slow_queries === 1 ? 'Slow Query' : 'Slow Queries'
20 const slowQueriesValue = queryMetrics?.[0]?.slow_queries || '0'
21
22 return [
23 {
24 title: slowQueriesTitle,
25 value: slowQueriesValue,
26 onClick: () => {
27 setSearchParams({
28 totalTimeFilter: {
29 operator: '>',
30 value: 1000,
31 } as NumericFilter,
32 })
33 },
34 },
35 {
36 title: 'Cache Hit Rate',
37 value: queryMetrics?.[0]?.cache_hit_rate || '0%',
38 tooltip:
39 'Percentage of data read from cache vs disk. Higher is better - it means faster queries and less database load.',
40 },
41 {
42 title: 'Avg. Rows Per Call',
43 value: queryMetrics?.[0]?.avg_rows_per_call || '0',
44 tooltip:
45 'Average number of rows returned per query execution. Helps identify queries that return too much or too little data.',
46 },
47 ]
48 }, [queryMetrics, setSearchParams])
49
50 return (
51 <section className="px-6 pt-2 pb-4 flex flex-wrap gap-x-6 gap-y-2 w-full">
52 {stats.map((card, i) => (
53 <React.Fragment key={i}>
54 <div
55 className={cn('flex items-baseline gap-2 heading-subSection text-foreground-light', {
56 'cursor-pointer hover:text-foreground transition-colors': card.onClick,
57 })}
58 onClick={card.onClick}
59 >
60 {isLoading ? (
61 <Skeleton className="h-5 w-24" />
62 ) : (
63 <>
64 <span className="text-foreground">{card.value}</span>
65 <span className="flex items-center gap-1">
66 {card.title}
67 {(card.title === 'Slow Queries' || card.title === 'Slow Query') && (
68 <Tooltip>
69 <TooltipTrigger asChild>
70 <button
71 type="button"
72 aria-label="How are slow queries calculated?"
73 className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
74 onClick={(e) => {
75 e.stopPropagation()
76 }}
77 >
78 <Info size={12} />
79 </button>
80 </TooltipTrigger>
81 <TooltipContent side="top" align="start" className="max-w-xs text-xs">
82 Slow queries are those with total execution time (execution time + planning
83 time) greater than 1000ms.
84 </TooltipContent>
85 </Tooltip>
86 )}
87 {card.tooltip && (
88 <Tooltip>
89 <TooltipTrigger asChild>
90 <button
91 type="button"
92 aria-label={`What is ${card.title}?`}
93 className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
94 onClick={(e) => {
95 e.stopPropagation()
96 }}
97 >
98 <Info size={12} />
99 </button>
100 </TooltipTrigger>
101 <TooltipContent side="top" align="start" className="max-w-xs text-xs">
102 {card.tooltip}
103 </TooltipContent>
104 </Tooltip>
105 )}
106 </span>
107 </>
108 )}
109 </div>
110 {i < stats.length - 1 && <span className="text-foreground-muted">/</span>}
111 </React.Fragment>
112 ))}
113 </section>
114 )
115}