feat: working query insights at a glance

This adds cards for some quick averages and totals in our query insights section.
This commit is contained in:
kemal committed 2025-08-27 14:51:56 +01:00
1 parent 2ad68cbcce
commit 1d3ff5f0eb
3 files changed
+200 -3

No files matched your search

@@ -1,7 +1,118 @@
import { useMemo } from 'react'
import { useQueryInsightsGlanceQuery } from 'data/query-insights/query-glance-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Card, CardContent } from 'ui'
import { Database, BarChart3, Clock, AlertCircle } from 'lucide-react'
export const QueryQuickGlance = () => {
const { data: project } = useSelectedProjectQuery()
// Memoize time range to prevent constant re-renders
const { startTime, endTime } = useMemo(() => {
const endTime = new Date().toISOString()
const startTime = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()
return { startTime, endTime }
}, []) // Empty dependency array - only calculate once
const {
data: glanceData,
isLoading,
error,
} = useQueryInsightsGlanceQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
startTime,
endTime,
})
const cards = useMemo(() => {
if (!glanceData) return []
return [
{
icon: <Database size={14} strokeWidth={1} />,
title: 'Queries',
values: [
{
label: 'Unique',
value: glanceData.unique_queries || 0,
},
{
label: 'Total',
value: glanceData.total_queries || 0,
},
],
},
{
icon: <Clock size={14} strokeWidth={1} />,
title: 'Query Time',
values: [
{
label: 'Average',
value: glanceData.avg_query_time?.toFixed(2) || 0,
},
{
label: 'Max',
value: glanceData.max_query_time?.toFixed(2) || 0,
},
],
},
{
icon: <AlertCircle size={14} strokeWidth={1} />,
title: 'Errors',
values: [
{
label: 'Count',
value: glanceData.error_count || 0,
},
{
label: 'Rate',
value: glanceData.avg_error_rate?.toFixed(2) || 0,
},
],
},
]
}, [glanceData])
if (isLoading) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{/* {[...Array(6)].map((_, i) => (
<div key={i} className="h-24 bg-gray-100 animate-pulse rounded-lg" />
))} */}
Loading...
</div>
)
}
if (error) {
return <div className="text-red-500">Error loading query insights: {error.message}</div>
}
return (
<>
<div>At a glance blocks here...</div>
</>
<section className="grid grid-cols-3 gap-4 w-full">
{cards.map((card, index) => (
<Card key={index}>
<CardContent className="flex flex-col items-start gap-2">
<div className="flex gap-2 items-center text-foreground-light ">
<span>{card.icon}</span>
<span className="text-xs font-mono uppercase truncate max-w-[24ch]">
{card.title}
</span>
</div>
<div className="flex flex-col w-full divide-y divide-dashed">
{card.values.map((value, index) => (
<div key={index} className="text-sm py-2 flex justify-between items-center w-full">
<span className="font-mono text-foreground-lighter truncate max-w-[16ch]">
{value.label}{' '}
</span>
<span>{value.value}</span>
</div>
))}
</div>
</CardContent>
</Card>
))}
</section>
)
}
+13
View File
@@ -0,0 +1,13 @@
import type { QueryKey } from '@tanstack/react-query'
export const queryInsightsKeys = {
all: (projectRef: string | undefined) => ['projects', projectRef, 'query-insights'] as const,
metrics: (projectRef: string | undefined, metric: string, startTime: string, endTime: string) =>
['projects', projectRef, 'query-insights', 'metrics', metric, startTime, endTime] as const,
queries: (projectRef: string | undefined, startTime: string, endTime: string) =>
['projects', projectRef, 'query-insights', 'queries', startTime, endTime] as const,
queriesWithErrors: (projectRef: string | undefined, startTime: string, endTime: string) =>
['projects', projectRef, 'query-insights', 'queries-with-errors', startTime, endTime] as const,
glance: (projectRef: string | undefined, startTime: string, endTime: string) =>
['projects', projectRef, 'query-insights', 'glance', startTime, endTime] as const,
}
@@ -0,0 +1,73 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { queryInsightsKeys } from './keys'
export type QueryInsightsGlance = {
unique_queries: number
total_queries: number
avg_query_time: number
max_query_time: number
error_count: number
avg_error_rate: number
}
export const getQueriesGlanceMetrics = (startTime: string, endTime: string) => /* SQL */ `
SELECT
COUNT(DISTINCT query) as unique_queries,
SUM(calls) as total_queries,
AVG(mean_exec_time) as avg_query_time,
MAX(mean_exec_time) as max_query_time,
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as error_count,
CASE
WHEN SUM(calls) > 0 THEN
(COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END)::float / SUM(calls)::float) * 100
ELSE 0
END as avg_error_rate
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
`.trim()
export type QueryInsightsGlanceVariables = {
projectRef?: string
connectionString?: string | null
startTime: string
endTime: string
}
export async function getQueryInsightsGlance(
{ projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables,
signal?: AbortSignal
) {
const sql = getQueriesGlanceMetrics(startTime, endTime)
const { result } = await executeSql(
{ projectRef, connectionString, sql, queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime) },
signal
)
return result[0] as QueryInsightsGlance
}
export type QueryInsightsGlanceData = Awaited<ReturnType<typeof getQueryInsightsGlance>>
export type QueryInsightsGlanceError = ExecuteSqlError
export const useQueryInsightsGlanceQuery = <TData = QueryInsightsGlanceData>(
{ projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables,
{ enabled = true, ...options }: UseQueryOptions<QueryInsightsGlanceData, QueryInsightsGlanceError, TData> = {}
) => {
return useQuery<QueryInsightsGlanceData, QueryInsightsGlanceError, TData>(
queryInsightsKeys.glance(projectRef, startTime, endTime),
({ signal }) => getQueryInsightsGlance({ projectRef, connectionString, startTime, endTime }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
...options
}
)
}