diff --git a/apps/studio/components/interfaces/QueryInsights/QueryInsights.tsx b/apps/studio/components/interfaces/QueryInsights/QueryInsights.tsx index cf1c2746ea9..aba9f7400eb 100644 --- a/apps/studio/components/interfaces/QueryInsights/QueryInsights.tsx +++ b/apps/studio/components/interfaces/QueryInsights/QueryInsights.tsx @@ -53,6 +53,7 @@ export const QueryInsights = () => { const [selectedMetric, setSelectedMetric] = useState('query_latency') const [selectedTimeRange, setSelectedTimeRange] = useState('1d') const [selectedQuery, setSelectedQuery] = useState(null) + const [hoveredQuery, setHoveredQuery] = useState(null) const [timeRange, setTimeRange] = useState({ period_start: { date: dayjs().subtract(24, 'hour').format('YYYY-MM-DD HH:mm:ssZ'), @@ -247,6 +248,7 @@ export const QueryInsights = () => { startTime={timeRange.period_start.date} endTime={timeRange.period_end.date} selectedQuery={selectedQuery} + hoveredQuery={hoveredQuery} /> @@ -260,7 +262,9 @@ export const QueryInsights = () => { queries={queriesData || []} isLoading={isLoadingQueries} onQuerySelect={setSelectedQuery} + onQueryHover={setHoveredQuery} selectedQuery={selectedQuery} + hoveredQuery={hoveredQuery} /> ) diff --git a/apps/studio/components/interfaces/QueryInsights/components/MetricToggleButtons.tsx b/apps/studio/components/interfaces/QueryInsights/components/MetricToggleButtons.tsx index c518b55bc79..051562dbb62 100644 --- a/apps/studio/components/interfaces/QueryInsights/components/MetricToggleButtons.tsx +++ b/apps/studio/components/interfaces/QueryInsights/components/MetricToggleButtons.tsx @@ -7,10 +7,18 @@ import { MetricPill } from './MetricPill' // Define the exact shape of the visibleMetrics state export interface VisibleMetricsState { - p50: boolean - p95: boolean - p99: boolean - p99_9: boolean + // Old keys for backward compatibility + p50?: boolean + p95?: boolean + p99?: boolean + p99_9?: boolean + + // New latency keys + latency_p50?: boolean + latency_p90?: boolean + latency_p99?: boolean + + // Common keys query_latency: boolean rows: boolean query_rows: boolean @@ -22,7 +30,7 @@ export interface VisibleMetricsState { cache_miss_ratio: boolean calls: boolean query_calls: boolean - [key: string]: boolean // Allow string indexing + [key: string]: boolean | undefined // Allow string indexing with optional values } interface MetricToggleButtonsProps { @@ -72,9 +80,54 @@ export const MetricToggleButtons = ({ return null } - const metricValue = Number( - chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]] - ) + // Calculate metric value based on the type of metric + let metricValue: number + + // For rows and calls metrics, calculate the total across all data points + if (key === 'rows' || key === 'calls') { + metricValue = chartData.reduce((sum, point) => { + const value = Number(point[key as keyof typeof point]) + return sum + (isNaN(value) ? 0 : value) + }, 0) + } + // For latency metrics, calculate the average of all data points + else if ( + key.includes('latency') || + key.includes('p50') || + key.includes('p95') || + key.includes('p99') + ) { + const points = chartData.filter((point) => { + const value = Number(point[key as keyof typeof point]) + return !isNaN(value) && value > 0 + }) + + metricValue = + points.length > 0 + ? points.reduce((sum, point) => { + const value = Number(point[key as keyof typeof point]) + return sum + value + }, 0) / points.length + : 0 + } + // For ratio metrics, use the last data point + else if ( + key.includes('ratio') || + key.includes('percentage') || + key.includes('cache_hit_ratio') || + key.includes('cache_miss_ratio') + ) { + metricValue = Number( + chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]] + ) + } + // For all other metrics, use the last data point as default + else { + metricValue = Number( + chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]] + ) + } + const formattedValue = !isNaN(metricValue) ? (value as any).formatter(metricValue) : '0' return ( diff --git a/apps/studio/components/interfaces/QueryInsights/components/MetricsChart.tsx b/apps/studio/components/interfaces/QueryInsights/components/MetricsChart.tsx index 59966312d28..e3a082d1d33 100644 --- a/apps/studio/components/interfaces/QueryInsights/components/MetricsChart.tsx +++ b/apps/studio/components/interfaces/QueryInsights/components/MetricsChart.tsx @@ -1,53 +1,26 @@ -import { LogsBarChart } from 'ui-patterns/LogsBarChart' +import { useState } from 'react' +import { MetricType } from '../QueryInsights' import { QueryInsightsMetric, QueryInsightsQuery } from 'data/query-insights/query-insights-query' +import { motion } from 'framer-motion' +import { VisibleMetricsState } from './MetricToggleButtons' +import { MetricToggleButtons } from './MetricToggleButtons' +import { SelectedQueryBadge } from './SelectedQueryBadge' +import { + useQueryLatencyChart, + useRowsReadChart, + useCallsChart, + useCacheHitsChart, + CHART_OPACITY, +} from '../hooks/useChartConfig' +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import dayjs from 'dayjs' -import { Line, LineChart, XAxis, YAxis, CartesianGrid, Area, AreaChart } from 'recharts' import { - ChartConfig, ChartContainer, - ChartTooltip, - ChartTooltipContent, ChartLegend, ChartLegendContent, - cn, + ChartTooltip, + ChartTooltipContent, } from 'ui' -import { useState, useMemo } from 'react' -import { useParams } from 'common' -import { Button } from 'ui' -import { MetricType } from '../QueryInsights' -import { - useSingleQueryLatency, - useSingleQueryRows, - useSingleQueryCalls, -} from 'data/query-insights/single-query-latency-query' -import { motion, AnimatePresence } from 'framer-motion' -import { formatLatency, formatMetricValue } from '../QueryInsights.utils' -import { - getRowsReadConfig, - getQueryLatencyConfig, - getGenericMetricConfig, - getCallsConfig, - ChartConfigType, - RowsReadChartData, - QueryLatencyChartData, - GenericChartData, - ChartConfigResult, - getCacheHitsConfig, -} from '../chartConfigs' -import { MetricToggleButtons, VisibleMetricsState } from './MetricToggleButtons' -import { SelectedQueryBadge } from './SelectedQueryBadge' - -// Chart opacity constants -const CHART_OPACITY = { - GRADIENT_START: 0.3, - GRADIENT_END: 0.05, - FILL_NORMAL: 0.3, - FILL_SELECTED: 0.05, - STROKE_NORMAL: 0.8, - STROKE_SELECTED: 0.2, - HIGHLIGHT_FILL: 0.6, - HIGHLIGHT_STROKE: 1, -} interface MetricsChartProps { data: QueryInsightsMetric[] @@ -56,92 +29,92 @@ interface MetricsChartProps { startTime: string endTime: string selectedQuery: QueryInsightsQuery | null + hoveredQuery: QueryInsightsQuery | null } -export function MetricsChart({ - data, - metric, - isLoading, - startTime, - endTime, - selectedQuery, -}: MetricsChartProps) { - const { ref } = useParams() +export function MetricsChart(props: MetricsChartProps) { + const { data, metric, isLoading, startTime, endTime, selectedQuery, hoveredQuery } = props + const [visibleMetrics, setVisibleMetrics] = useState({ + // Old keys for backward compatibility p50: true, p95: true, p99: true, p99_9: true, + // Latency + latency_p50: true, + latency_p90: true, + latency_p99: true, query_latency: true, + // Rows rows: true, query_rows: true, + // Cache shared_blks_hit: true, shared_blks_read: true, shared_blks_dirtied: true, shared_blks_written: true, cache_hit_ratio: true, cache_miss_ratio: true, + // Calls calls: true, query_calls: true, }) - // Fetch single query latency data if a query is selected - const { - data: queryLatencyData, - isLoading: isLoadingQueryLatency, - error: queryLatencyError, - } = useSingleQueryLatency(ref, selectedQuery?.query_id?.toString(), startTime, endTime, { - enabled: !!selectedQuery && metric === 'query_latency', - }) - - // Fetch single query rows data if a query is selected - const { - data: queryRowsData, - isLoading: isLoadingQueryRows, - error: queryRowsError, - } = useSingleQueryRows(ref, selectedQuery?.query_id?.toString(), startTime, endTime, { - enabled: !!selectedQuery && metric === 'rows_read', - }) - - // Fetch single query calls data if a query is selected - const { - data: queryCallsData, - isLoading: isLoadingQueryCalls, - error: queryCallsError, - } = useSingleQueryCalls(ref, selectedQuery?.query_id?.toString(), startTime, endTime, { - enabled: !!selectedQuery && metric === 'calls', - }) - - // Derive chart configuration based on the selected metric - const chartConfig = useMemo(() => { - if (!Array.isArray(data) || data.length === 0) { - return null - } - - switch (metric) { - case 'rows_read': - return getRowsReadConfig(data, queryRowsData) - case 'query_latency': - return getQueryLatencyConfig(data, queryLatencyData) - case 'cache_hits': - return getCacheHitsConfig(data) - case 'calls': - return getCallsConfig(data, queryCallsData) - default: - return getGenericMetricConfig(data) - } - }, [data, metric, queryLatencyData, queryRowsData, queryCallsData]) - // Handler for clearing the selected query const handleClearSelectedQuery = () => { - // Find the appropriate onQuerySelect handler (it's actually in the parent component) - // This will be called via the parent's prop const paramsToSend = { clearQuery: true } window.dispatchEvent( new CustomEvent('clearSelectedQueryInsightsQuery', { detail: paramsToSend }) ) } + // Get the appropriate chart configuration based on metric type + const chartHookOptions = { data, startTime, endTime, selectedQuery, hoveredQuery, metric } + + // Call all hooks unconditionally to avoid React Hook rules violations + const queryLatencyResult = useQueryLatencyChart(chartHookOptions) + const rowsReadResult = useRowsReadChart(chartHookOptions) + const callsResult = useCallsChart(chartHookOptions) + const cacheHitsResult = useCacheHitsChart(chartHookOptions) + + // Get the appropriate result based on metric type + let chartConfig = null + let metricBadges: { label: string; value: string; color: string }[] = [] + + // Get the current hook result based on metric type + let currentHookResult + switch (metric) { + case 'query_latency': + currentHookResult = queryLatencyResult + break + case 'rows_read': + currentHookResult = rowsReadResult + break + case 'calls': + currentHookResult = callsResult + break + case 'cache_hits': + currentHookResult = cacheHitsResult + break + default: + currentHookResult = { + chartConfig: null, + metricBadges: [], + renderConfig: { yAxisDomain: [0, 'auto'], keyMappings: {} }, + } + } + + // Extract values + chartConfig = currentHookResult.chartConfig + metricBadges = currentHookResult.metricBadges + const yAxisDomain = currentHookResult.renderConfig.yAxisDomain + const yAxisFormatter = currentHookResult.renderConfig.yAxisFormatter + const yAxisWidth = currentHookResult.renderConfig.yAxisWidth + const yAxisTickCount = currentHookResult.renderConfig.yAxisTickCount + const keyMappings = currentHookResult.renderConfig.keyMappings + + // Loading state if (isLoading) { return (
@@ -158,7 +131,8 @@ export function MetricsChart({ ) } - if (!Array.isArray(data) || data.length === 0 || !chartConfig) { + // No data state + if (!chartConfig) { return (
).config - const chartData = (chartConfig as ChartConfigResult).chartData + const { config, chartData } = chartConfig - // Get metrics for the selected query badge - const getQueryMetrics = () => { - if (!selectedQuery) return [] - - const latestData = chartData[chartData.length - 1] - if (!latestData) return [] - - const metrics = [] - - if (metric === 'query_latency') { - // Add just the single query latency value - if (latestData.query_latency !== undefined) { - metrics.push({ - label: 'Query latency', - value: formatLatency(latestData.query_latency), - color: config.query_latency?.color || 'hsl(var(--chart-1))', - }) - } - } else if (metric === 'rows_read') { - // Add rows read for the query - if (latestData.query_rows !== undefined) { - metrics.push({ - label: 'Rows', - value: formatMetricValue(latestData.query_rows), - color: config.query_rows?.color || 'hsl(var(--chart-2))', - }) - } - } else if (metric === 'cache_hits') { - // Cache hits don't have query-specific metrics - } - - return metrics - } - - return ( -
-
-
- } - visibleMetrics={visibleMetrics} - setVisibleMetrics={setVisibleMetrics} - metric={metric} - selectedQuery={selectedQuery} - /> - - {selectedQuery && ( - - )} -
- -
- - - - {Object.entries(config).map(([key, value]) => ( - - - - - ))} - - - dayjs(value).format('HH:mm')} - /> - `${value}%` : formatMetricValue - } - domain={ - metric === 'cache_hits' - ? [0, 100] - : metric === 'query_latency' - ? ['auto', 'auto'] - : ['dataMin', 'dataMax'] - } - allowDataOverflow={false} - width={metric === 'query_latency' ? 80 : undefined} - tickCount={metric === 'query_latency' ? 5 : undefined} - yAxisId="left" - /> - } - /> - } /> - - {/* Render series based on metric type */} - {metric === 'rows_read' && ( - <> - {visibleMetrics.rows && ( - - )} - {selectedQuery && visibleMetrics.query_rows && ( - - )} - - )} - - {metric === 'query_latency' && ( - <> - {visibleMetrics.p99_9 && ( - - )} - {visibleMetrics.p99 && ( - - )} - {visibleMetrics.p95 && ( - - )} - {visibleMetrics.p50 && ( - - )} - {selectedQuery && visibleMetrics.query_latency && ( - - )} - - )} - - {metric === 'cache_hits' && ( - <> - {visibleMetrics.cache_hit_ratio && ( - - )} - {visibleMetrics.cache_miss_ratio && ( - - )} - - )} - - -
-
-
- ) - } - - // For calls and other metrics, use the bar chart - if (metric === 'calls') { - const { chartData, config } = chartConfig as ChartConfigResult - - // Calculate total calls for display - const totalCalls = chartData.reduce((sum, point) => sum + (point.calls ?? 0), 0) - const totalQueryCalls = selectedQuery - ? chartData.reduce((sum, point) => sum + (point.query_calls ?? 0), 0) - : 0 - - // Get metrics for the selected query badge - const getQueryMetrics = () => { - if (!selectedQuery) return [] - - return [ - { - label: 'Calls', - value: formatMetricValue(totalQueryCalls), - color: config.query_calls?.color || 'hsl(var(--chart-3))', - }, - ] - } - - return ( -
-
-
- } - visibleMetrics={visibleMetrics} - setVisibleMetrics={setVisibleMetrics} - metric={metric} - selectedQuery={selectedQuery} - /> - - {selectedQuery && ( - - )} -
- -
- - - - - - - - {selectedQuery && ( - - - - - )} - - - dayjs(value).format('HH:mm')} - /> - - } - /> - } /> - - {visibleMetrics.calls && ( - - )} - - {selectedQuery && visibleMetrics.query_calls && ( - - )} - - -
-
-
- ) - } - - // For other metrics, use the bar chart - const { chartData } = chartConfig as { chartData: GenericChartData[] } + // Debug logging to identify why query_rows isn't rendering + console.log('[MetricsChart] Rendering chart with:', { + metric, + selectedQuery: selectedQuery?.query_id, + chartDataLength: chartData.length, + configKeys: Object.keys(config), + querySpecificData: chartData.filter( + (point) => point.query_rows > 0 || point.query_latency > 0 || point.query_calls > 0 + ), + visibleMetrics: { + query_rows: visibleMetrics.query_rows, + rows: visibleMetrics.rows, + }, + }) return (
} + chartConfig={chartConfig} visibleMetrics={visibleMetrics} setVisibleMetrics={setVisibleMetrics} metric={metric} @@ -618,39 +182,33 @@ export function MetricsChart({ selectedQuery={selectedQuery} onClear={handleClearSelectedQuery} metric={metric} - metrics={[]} + metrics={metricBadges} /> )}
- + - - - - + {Object.entries(config).map(([key, value]) => ( + + + + + ))} dayjs(value).format('HH:mm')} /> } /> } /> - + + {/* Render data series based on configuration */} + {Object.entries(config).map(([key, value]) => { + // Map the key to the corresponding visibleMetrics key + const visibleMetricsKey = (keyMappings as Record)[key] || key + + // Skip series not enabled in visible metrics + if (!visibleMetrics[visibleMetricsKey] && !visibleMetrics[key]) return null + + // Skip query-specific metrics when not needed + if ( + (key === 'query_rows' || key === 'query_latency' || key === 'query_calls') && + !selectedQuery && + !hoveredQuery + ) { + console.log(`[MetricsChart] Skipping ${key} - no selected or hovered query`) + return null + } + + // Skip showing full series data for query-specific metrics + if (key === 'query_rows' || key === 'query_latency' || key === 'query_calls') { + console.log( + `[MetricsChart] Rendering ${key} with selectedQuery=${!!selectedQuery}` + ) + + // If hovering and not selected, show hover style + if (hoveredQuery && !selectedQuery) { + return ( + + ) + } + // If selected, show selected style + else if (selectedQuery) { + return ( + + ) + } + console.log(`[MetricsChart] Not rendering ${key} - conditions not met`) + return null + } + + // Regular series + return ( + + ) + })}
diff --git a/apps/studio/components/interfaces/QueryInsights/components/QueryList.tsx b/apps/studio/components/interfaces/QueryInsights/components/QueryList.tsx index 285d570202b..1abf3c4530a 100644 --- a/apps/studio/components/interfaces/QueryInsights/components/QueryList.tsx +++ b/apps/studio/components/interfaces/QueryInsights/components/QueryList.tsx @@ -11,13 +11,23 @@ interface QueryListProps { queries: QueryInsightsQuery[] isLoading: boolean onQuerySelect?: (query: QueryInsightsQuery | null) => void + onQueryHover?: (query: QueryInsightsQuery | null) => void selectedQuery?: QueryInsightsQuery | null + hoveredQuery?: QueryInsightsQuery | null } -export const QueryList = ({ queries, isLoading, onQuerySelect, selectedQuery }: QueryListProps) => { +export const QueryList = ({ + queries, + isLoading, + onQuerySelect, + onQueryHover, + selectedQuery, + hoveredQuery, +}: QueryListProps) => { const router = useRouter() const gridRef = useRef(null) const [selectedRow, setSelectedRow] = useState() + const [hoveredRow, setHoveredRow] = useState() const normalizeWhitespace = (sql: string) => { return sql.replace(/\s+/g, ' ').trim() @@ -56,6 +66,35 @@ export const QueryList = ({ queries, isLoading, onQuerySelect, selectedQuery }: gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx }) } + // Handler for row hover + const handleRowMouseEnter = (idx: number) => { + if (isNaN(idx) || idx < 0 || idx >= queries.length) return + if (selectedQuery) return // Don't trigger hover when a query is already selected + + const query = queries[idx] + setHoveredRow(idx) + onQueryHover?.(query) + console.log('Query hover:', query.query_id, query.query.substring(0, 50) + '...') + } + + const handleRowMouseLeave = () => { + if (selectedQuery) return // Don't clear hover when a query is already selected + + setHoveredRow(undefined) + onQueryHover?.(null) + console.log('Query hover cleared') + } + + const rowClassRender = (row: QueryInsightsQuery) => { + const isSelected = selectedQuery?.query_id === row.query_id + const isHovered = hoveredQuery?.query_id === row.query_id && !selectedQuery + + return cn( + 'cursor-pointer transition-colors', + isSelected ? 'bg-surface-300' : isHovered ? 'bg-surface-200' : '' + ) + } + const columns: Column[] = [ // { // key: 'timestamp', @@ -265,8 +304,9 @@ export const QueryList = ({ queries, isLoading, onQuerySelect, selectedQuery }: rowClass={(row, idx) => { // Use the query_id for comparison instead of row index const isSelected = selectedQuery?.query_id === row.query_id + // const isHovered = hoveredQuery?.query_id === row.query_id && !selectedQuery return [ - `${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'hover:bg-surface-200'} cursor-pointer`, + rowClassRender(row), `${isSelected ? '[&>div:first-child]:border-l-4 border-l-secondary [&>div]:border-l-foreground' : ''}`, '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none', '[&>.rdg-cell:first-child>div]:ml-4', @@ -275,7 +315,16 @@ export const QueryList = ({ queries, isLoading, onQuerySelect, selectedQuery }: renderers={{ renderRow: (rowIdx, props) => { const idx = typeof rowIdx === 'number' ? rowIdx : Number(rowIdx) - return handleRowClick(idx)} /> + return ( + handleRowClick(idx)} + // Commenting out hover functionality + // onMouseEnter={() => handleRowMouseEnter(idx)} + // onMouseLeave={handleRowMouseLeave} + /> + ) }, noRowsFallback: isLoading ? (
diff --git a/apps/studio/components/interfaces/QueryInsights/hooks/useChartConfig.ts b/apps/studio/components/interfaces/QueryInsights/hooks/useChartConfig.ts new file mode 100644 index 00000000000..09163544ab6 --- /dev/null +++ b/apps/studio/components/interfaces/QueryInsights/hooks/useChartConfig.ts @@ -0,0 +1,314 @@ +import { useMemo } from 'react' +import { useParams } from 'common' +import { + getQueryLatencyConfig, + getRowsReadConfig, + getCallsConfig, + getCacheHitsConfig, + ChartConfigResult, +} from '../chartConfigs' +import { QueryInsightsMetric, QueryInsightsQuery } from 'data/query-insights/query-insights-query' +import { + useSingleQueryLatency, + useSingleQueryRows, + useSingleQueryCalls, +} from 'data/query-insights/single-query-latency-query' +import { formatLatency, formatMetricValue } from '../QueryInsights.utils' +import { MetricType } from '../QueryInsights' + +// Shared opacity settings for all charts +export const CHART_OPACITY = { + GRADIENT_START: 0.3, + GRADIENT_END: 0.05, + FILL_NORMAL: 0.3, + FILL_SELECTED: 0.05, + STROKE_NORMAL: 0.8, + STROKE_SELECTED: 0.2, + HIGHLIGHT_FILL: 0.6, + HIGHLIGHT_STROKE: 1, +} + +interface ChartHookOptions { + data: QueryInsightsMetric[] + startTime: string + endTime: string + selectedQuery: QueryInsightsQuery | null + hoveredQuery: QueryInsightsQuery | null +} + +interface ChartHookResult { + chartConfig: ChartConfigResult | null + metricBadges: { label: string; value: string; color: string }[] + renderConfig: { + yAxisDomain: [number, string | number] + yAxisFormatter?: (value: number) => string + yAxisWidth?: number + yAxisTickCount?: number + keyMappings: { [key: string]: string } // Maps config keys to visibleMetrics keys + } +} + +/** + * Hook for Query Latency chart configuration + */ +export function useQueryLatencyChart({ + data, + startTime, + endTime, + selectedQuery, + hoveredQuery, + metric, +}: ChartHookOptions & { metric: MetricType }): ChartHookResult { + const { ref } = useParams() + const isActive = metric === 'query_latency' + + console.log( + '[useQueryLatencyChart] selectedQuery:', + selectedQuery?.query_id, + 'hoveredQuery:', + hoveredQuery?.query_id + ) + + // Fetch single query latency data + const { data: queryLatencyData } = useSingleQueryLatency( + ref, + startTime, + endTime, + (selectedQuery?.query_id || hoveredQuery?.query_id)?.toString() || '', + { + // Only fetch data if this is the current metric and we have a query selected/hovered + enabled: isActive && !!(selectedQuery || hoveredQuery), + } + ) + + // Generate chart configuration + const chartConfig = useMemo(() => { + if (!isActive || !Array.isArray(data) || data.length === 0) { + return null + } + return getQueryLatencyConfig(data, queryLatencyData) + }, [data, queryLatencyData, isActive]) + + // Get metrics for the selected query badge + const metricBadges = useMemo(() => { + if (!isActive || !selectedQuery || !chartConfig) return [] + + const { chartData, config } = chartConfig + + // Calculate average latency across all data points that have query_latency + const queryLatencyPoints = chartData.filter((point) => point.query_latency > 0) + const avgLatency = + queryLatencyPoints.length > 0 + ? queryLatencyPoints.reduce((sum, point) => sum + (point.query_latency ?? 0), 0) / + queryLatencyPoints.length + : 0 + + console.log('[useQueryLatencyChart] Calculating metric badge:', { + avgLatency, + pointsWithLatency: queryLatencyPoints.length, + totalPoints: chartData.length, + }) + + return [ + { + label: 'Query latency', + value: formatLatency(avgLatency), + color: config.query_latency?.color || 'hsl(var(--chart-1))', + }, + ] + }, [selectedQuery, chartConfig, isActive]) + + // Provide rendering configuration + const renderConfig = { + yAxisDomain: [0, 'auto'] as [number, string | number], + yAxisWidth: 80, + yAxisTickCount: 5, + keyMappings: { + p50: 'latency_p50', + p95: 'latency_p90', + p99: 'latency_p99', + }, + } + + return { chartConfig, metricBadges, renderConfig } +} + +/** + * Hook for Rows Read chart configuration + */ +export function useRowsReadChart({ + data, + startTime, + endTime, + selectedQuery, + hoveredQuery, + metric, +}: ChartHookOptions & { metric: MetricType }): ChartHookResult { + const { ref } = useParams() + const isActive = metric === 'rows_read' + + console.log( + '[useRowsReadChart] selectedQuery:', + selectedQuery?.query_id, + 'hoveredQuery:', + hoveredQuery?.query_id + ) + + // Fetch single query rows data + const { data: queryRowsData } = useSingleQueryRows( + ref, + startTime, + endTime, + (selectedQuery?.query_id || hoveredQuery?.query_id)?.toString() || '', + { + // Only fetch data if this is the current metric and we have a query selected/hovered + enabled: isActive && !!(selectedQuery || hoveredQuery), + } + ) + + console.log('[useRowsReadChart] queryRowsData:', queryRowsData) + + // Generate chart configuration + const chartConfig = useMemo(() => { + if (!isActive || !Array.isArray(data) || data.length === 0) { + return null + } + const config = getRowsReadConfig(data, queryRowsData) + console.log('[useRowsReadChart] chartConfig result:', { + chartDataLength: config.chartData.length, + hasQueryRows: config.chartData.some((point) => point.query_rows > 0), + firstFewPoints: config.chartData.slice(0, 3), + queryRowsPoints: config.chartData.filter((point) => point.query_rows > 0), + }) + return config + }, [data, queryRowsData, isActive]) + + // Get metrics for the selected query badge + const metricBadges = useMemo(() => { + if (!isActive || !selectedQuery || !chartConfig) return [] + + const { chartData, config } = chartConfig + + // Calculate total rows for display - sum across all data points + const totalQueryRows = chartData.reduce((sum, point) => sum + (point.query_rows ?? 0), 0) + + console.log('[useRowsReadChart] Calculating metric badge:', { + totalQueryRows, + chartDataPoints: chartData.length, + hasQueryRows: chartData.some((point) => point.query_rows > 0), + }) + + // Show the total rows rather than just the latest point + return [ + { + label: 'Rows', + value: formatMetricValue(totalQueryRows), + color: config.query_rows?.color || 'hsl(var(--chart-2))', + }, + ] + }, [selectedQuery, chartConfig, isActive]) + + // Provide rendering configuration + const renderConfig = { + yAxisDomain: [0, 'auto'] as [number, string | number], + keyMappings: {}, + } + + return { chartConfig, metricBadges, renderConfig } +} + +/** + * Hook for Calls chart configuration + */ +export function useCallsChart({ + data, + startTime, + endTime, + selectedQuery, + hoveredQuery, + metric, +}: ChartHookOptions & { metric: MetricType }): ChartHookResult { + const { ref } = useParams() + const isActive = metric === 'calls' + + console.log( + '[useCallsChart] selectedQuery:', + selectedQuery?.query_id, + 'hoveredQuery:', + hoveredQuery?.query_id + ) + + // Fetch single query calls data + const { data: queryCallsData } = useSingleQueryCalls( + ref, + startTime, + endTime, + (selectedQuery?.query_id || hoveredQuery?.query_id)?.toString() || '', + { + // Only fetch data if this is the current metric and we have a query selected/hovered + enabled: isActive && !!(selectedQuery || hoveredQuery), + } + ) + + // Generate chart configuration + const chartConfig = useMemo(() => { + if (!isActive || !Array.isArray(data) || data.length === 0) { + return null + } + return getCallsConfig(data, queryCallsData) + }, [data, queryCallsData, isActive]) + + // Get metrics for the selected query badge + const metricBadges = useMemo(() => { + if (!isActive || !selectedQuery || !chartConfig) return [] + + const { chartData, config } = chartConfig + + // Calculate total calls for display + const totalQueryCalls = chartData.reduce((sum, point) => sum + (point.query_calls ?? 0), 0) + + return [ + { + label: 'Calls', + value: formatMetricValue(totalQueryCalls), + color: config.query_calls?.color || 'hsl(var(--chart-3))', + }, + ] + }, [selectedQuery, chartConfig, isActive]) + + // Provide rendering configuration + const renderConfig = { + yAxisDomain: [0, 'auto'] as [number, string | number], + keyMappings: {}, + } + + return { chartConfig, metricBadges, renderConfig } +} + +/** + * Hook for Cache Hits chart configuration + */ +export function useCacheHitsChart({ + data, + metric, +}: ChartHookOptions & { metric: MetricType }): ChartHookResult { + const isActive = metric === 'cache_hits' + + // Generate chart configuration + const chartConfig = useMemo(() => { + if (!isActive || !Array.isArray(data) || data.length === 0) { + return null + } + return getCacheHitsConfig(data) + }, [data, isActive]) + + // Provide rendering configuration + const renderConfig = { + yAxisDomain: [0, 100] as [number, string | number], + yAxisFormatter: (value: number) => `${value}%`, + keyMappings: {}, + } + + // Cache hits don't have query-specific metrics + return { chartConfig, metricBadges: [], renderConfig } +} diff --git a/apps/studio/data/query-insights/single-query-latency-query.ts b/apps/studio/data/query-insights/single-query-latency-query.ts index 8a693cac36e..9c74434233c 100644 --- a/apps/studio/data/query-insights/single-query-latency-query.ts +++ b/apps/studio/data/query-insights/single-query-latency-query.ts @@ -71,9 +71,9 @@ const getSingleQueryCallsSql = (queryId: string, startTime: string, endTime: str export function useSingleQueryLatency( projectRef: string | undefined, - queryId: string | undefined, startTime: string, endTime: string, + queryId: string | undefined, options?: UseQueryOptions ) { return useQuery({ @@ -97,9 +97,9 @@ export function useSingleQueryLatency( export function useSingleQueryRows( projectRef: string | undefined, - queryId: string | undefined, startTime: string, endTime: string, + queryId: string | undefined, options?: UseQueryOptions ) { return useQuery({ @@ -108,11 +108,15 @@ export function useSingleQueryRows( if (!projectRef) throw new Error('Project ref is required') if (!queryId) return [] + const sql = getSingleQueryRowsSql(queryId, startTime, endTime) + console.log('[useSingleQueryRows] Executing SQL:', sql) + const { result } = await executeSql({ projectRef, - sql: getSingleQueryRowsSql(queryId, startTime, endTime), + sql, }) + console.log('[useSingleQueryRows] Result:', result) return result as QueryInsightsMetric[] }, enabled: !!queryId && !!projectRef, @@ -122,9 +126,9 @@ export function useSingleQueryRows( export function useSingleQueryCalls( projectRef: string | undefined, - queryId: string | undefined, startTime: string, endTime: string, + queryId: string | undefined, options?: UseQueryOptions ) { return useQuery({