diff --git a/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx b/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx index e599503674a..5681cbc700a 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryQuickGlance.tsx @@ -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: , + title: 'Queries', + values: [ + { + label: 'Unique', + value: glanceData.unique_queries || 0, + }, + { + label: 'Total', + value: glanceData.total_queries || 0, + }, + ], + }, + { + icon: , + 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: , + title: 'Errors', + values: [ + { + label: 'Count', + value: glanceData.error_count || 0, + }, + { + label: 'Rate', + value: glanceData.avg_error_rate?.toFixed(2) || 0, + }, + ], + }, + ] + }, [glanceData]) + + if (isLoading) { + return ( +
+ {/* {[...Array(6)].map((_, i) => ( +
+ ))} */} + Loading... +
+ ) + } + + if (error) { + return
Error loading query insights: {error.message}
+ } + return ( - <> -
At a glance blocks here...
- +
+ {cards.map((card, index) => ( + + +
+ {card.icon} + + {card.title} + +
+
+ {card.values.map((value, index) => ( +
+ + {value.label}{' '} + + {value.value} +
+ ))} +
+
+
+ ))} +
) } diff --git a/apps/studio/data/query-insights/keys.ts b/apps/studio/data/query-insights/keys.ts new file mode 100644 index 00000000000..6db7854a9c4 --- /dev/null +++ b/apps/studio/data/query-insights/keys.ts @@ -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, +} \ No newline at end of file diff --git a/apps/studio/data/query-insights/query-glance-query.ts b/apps/studio/data/query-insights/query-glance-query.ts new file mode 100644 index 00000000000..bdb301b0bd6 --- /dev/null +++ b/apps/studio/data/query-insights/query-glance-query.ts @@ -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> +export type QueryInsightsGlanceError = ExecuteSqlError + +export const useQueryInsightsGlanceQuery = ( + { projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => { + return useQuery( + 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 + } + ) +}