mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 13:49:09 +03:00
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:
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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user