This commit is contained in:
Jonathan Summers-Muir committed 2025-04-11 14:37:32 +08:00
1 parent 23cfebbb9f
commit 42bc97ee91
6 files changed
+642 -578

No files matched your search

@@ -53,6 +53,7 @@ export const QueryInsights = () => {
const [selectedMetric, setSelectedMetric] = useState<MetricType>('query_latency')
const [selectedTimeRange, setSelectedTimeRange] = useState('1d')
const [selectedQuery, setSelectedQuery] = useState<QueryInsightsQuery | null>(null)
const [hoveredQuery, setHoveredQuery] = useState<QueryInsightsQuery | null>(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}
/>
</div>
</Tabs_Shadcn_>
@@ -260,7 +262,9 @@ export const QueryInsights = () => {
queries={queriesData || []}
isLoading={isLoadingQueries}
onQuerySelect={setSelectedQuery}
onQueryHover={setHoveredQuery}
selectedQuery={selectedQuery}
hoveredQuery={hoveredQuery}
/>
</div>
)
@@ -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 (
@@ -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<VisibleMetricsState>({
// 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 (
<div className="flex-1 flex items-center justify-center">
@@ -158,7 +131,8 @@ export function MetricsChart({
)
}
if (!Array.isArray(data) || data.length === 0 || !chartConfig) {
// No data state
if (!chartConfig) {
return (
<div className="flex-1 flex items-center justify-center">
<motion.div
@@ -174,439 +148,29 @@ export function MetricsChart({
)
}
// Render charts based on metric type
if (metric === 'rows_read' || metric === 'query_latency' || metric === 'cache_hits') {
const config = (chartConfig as ChartConfigResult<any>).config
const chartData = (chartConfig as ChartConfigResult<any>).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 (
<div className="h-[320px] flex flex-col">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex flex-wrap items-center gap-2 px-5 mb-4">
<MetricToggleButtons
chartConfig={chartConfig as ChartConfigResult<any>}
visibleMetrics={visibleMetrics}
setVisibleMetrics={setVisibleMetrics}
metric={metric}
selectedQuery={selectedQuery}
/>
{selectedQuery && (
<SelectedQueryBadge
selectedQuery={selectedQuery}
onClear={handleClearSelectedQuery}
metric={metric}
metrics={getQueryMetrics()}
/>
)}
</div>
<div className="flex-1 min-h-0">
<ChartContainer className="h-full w-full" config={config}>
<AreaChart
data={chartData}
style={{ width: '100%', height: '100%' }}
margin={{ right: 16 }}
>
<defs>
{Object.entries(config).map(([key, value]) => (
<linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={(value as any).color}
stopOpacity={CHART_OPACITY.GRADIENT_START}
/>
<stop
offset="95%"
stopColor={(value as any).color}
stopOpacity={CHART_OPACITY.GRADIENT_END}
/>
</linearGradient>
))}
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
interval="preserveStartEnd"
minTickGap={50}
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => dayjs(value).format('HH:mm')}
/>
<YAxis
tickFormatter={
metric === 'cache_hits' ? (value) => `${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"
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" hideLabel />}
/>
<ChartLegend content={<ChartLegendContent />} />
{/* Render series based on metric type */}
{metric === 'rows_read' && (
<>
{visibleMetrics.rows && (
<Area
type="monotone"
dataKey="rows"
fill={`url(#gradient-rows)`}
stroke={config.rows.color}
strokeWidth={1}
dot={false}
animationDuration={100}
fillOpacity={
selectedQuery ? CHART_OPACITY.FILL_SELECTED : CHART_OPACITY.FILL_NORMAL
}
strokeOpacity={
selectedQuery
? CHART_OPACITY.STROKE_SELECTED
: CHART_OPACITY.STROKE_NORMAL
}
yAxisId="left"
/>
)}
{selectedQuery && visibleMetrics.query_rows && (
<Area
type="monotone"
dataKey="query_rows"
fill={`url(#gradient-query_rows)`}
stroke={config.query_rows.color}
strokeWidth={2}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.HIGHLIGHT_FILL}
strokeOpacity={CHART_OPACITY.HIGHLIGHT_STROKE}
yAxisId="left"
/>
)}
</>
)}
{metric === 'query_latency' && (
<>
{visibleMetrics.p99_9 && (
<Area
type="monotone"
dataKey="p99_9"
fill={`url(#gradient-p99_9)`}
stroke={config.p99_9.color}
strokeOpacity={
selectedQuery && visibleMetrics.query_latency
? CHART_OPACITY.STROKE_SELECTED
: CHART_OPACITY.STROKE_NORMAL
}
strokeWidth={1}
dot={false}
animationDuration={100}
yAxisId="left"
/>
)}
{visibleMetrics.p99 && (
<Area
type="monotone"
dataKey="p99"
fill={`url(#gradient-p99)`}
stroke={config.p99.color}
strokeOpacity={
selectedQuery && visibleMetrics.query_latency
? CHART_OPACITY.STROKE_SELECTED
: CHART_OPACITY.STROKE_NORMAL
}
strokeWidth={1}
dot={false}
animationDuration={100}
yAxisId="left"
/>
)}
{visibleMetrics.p95 && (
<Area
type="monotone"
dataKey="p95"
fill={`url(#gradient-p95)`}
stroke={config.p95.color}
strokeOpacity={
selectedQuery && visibleMetrics.query_latency
? CHART_OPACITY.STROKE_SELECTED
: CHART_OPACITY.STROKE_NORMAL
}
strokeWidth={1}
dot={false}
animationDuration={100}
yAxisId="left"
/>
)}
{visibleMetrics.p50 && (
<Area
type="monotone"
dataKey="p50"
fill={`url(#gradient-p50)`}
stroke={config.p50.color}
strokeOpacity={
selectedQuery && visibleMetrics.query_latency
? CHART_OPACITY.STROKE_SELECTED
: CHART_OPACITY.STROKE_NORMAL
}
strokeWidth={1}
dot={false}
animationDuration={100}
yAxisId="left"
/>
)}
{selectedQuery && visibleMetrics.query_latency && (
<Area
type="monotone"
dataKey="query_latency"
fill={`url(#gradient-query_latency)`}
stroke={config.query_latency.color}
strokeOpacity={CHART_OPACITY.HIGHLIGHT_STROKE}
strokeWidth={2.5}
dot={false}
animationDuration={100}
yAxisId="left"
/>
)}
</>
)}
{metric === 'cache_hits' && (
<>
{visibleMetrics.cache_hit_ratio && (
<Area
type="monotone"
dataKey="cache_hit_ratio"
fill={`url(#gradient-cache_hit_ratio)`}
stroke={config.cache_hit_ratio.color}
strokeWidth={2}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.FILL_NORMAL}
strokeOpacity={CHART_OPACITY.STROKE_NORMAL}
name="Cache Hit Ratio"
yAxisId="left"
/>
)}
{visibleMetrics.cache_miss_ratio && (
<Area
type="monotone"
dataKey="cache_miss_ratio"
fill={`url(#gradient-cache_miss_ratio)`}
stroke={config.cache_miss_ratio.color}
strokeWidth={2}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.FILL_NORMAL}
strokeOpacity={CHART_OPACITY.STROKE_NORMAL}
name="Cache Miss Ratio"
yAxisId="left"
/>
)}
</>
)}
</AreaChart>
</ChartContainer>
</div>
</div>
</div>
)
}
// For calls and other metrics, use the bar chart
if (metric === 'calls') {
const { chartData, config } = chartConfig as ChartConfigResult<any>
// 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 (
<div className="h-[320px] flex flex-col">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex flex-wrap items-center gap-2 px-5 mb-4">
<MetricToggleButtons
chartConfig={chartConfig as ChartConfigResult<any>}
visibleMetrics={visibleMetrics}
setVisibleMetrics={setVisibleMetrics}
metric={metric}
selectedQuery={selectedQuery}
/>
{selectedQuery && (
<SelectedQueryBadge
selectedQuery={selectedQuery}
onClear={handleClearSelectedQuery}
metric={metric}
metrics={getQueryMetrics()}
/>
)}
</div>
<div className="flex-1 min-h-0">
<ChartContainer className="h-full w-full" config={config}>
<AreaChart
data={chartData}
style={{ width: '100%', height: '100%' }}
margin={{ right: 16 }}
>
<defs>
<linearGradient id="gradient-calls" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={config.calls.color}
stopOpacity={CHART_OPACITY.GRADIENT_START}
/>
<stop
offset="95%"
stopColor={config.calls.color}
stopOpacity={CHART_OPACITY.GRADIENT_END}
/>
</linearGradient>
{selectedQuery && (
<linearGradient id="gradient-query_calls" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={config.query_calls.color}
stopOpacity={CHART_OPACITY.GRADIENT_START}
/>
<stop
offset="95%"
stopColor={config.query_calls.color}
stopOpacity={CHART_OPACITY.GRADIENT_END}
/>
</linearGradient>
)}
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
interval="preserveStartEnd"
minTickGap={50}
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => dayjs(value).format('HH:mm')}
/>
<YAxis
tickFormatter={formatMetricValue}
domain={[0, 'auto']}
allowDataOverflow={false}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" hideLabel />}
/>
<ChartLegend content={<ChartLegendContent />} />
{visibleMetrics.calls && (
<Area
type="monotone"
dataKey="calls"
fill="url(#gradient-calls)"
stroke={config.calls.color}
strokeWidth={1.5}
dot={false}
animationDuration={100}
fillOpacity={
selectedQuery ? CHART_OPACITY.FILL_SELECTED : CHART_OPACITY.FILL_NORMAL
}
strokeOpacity={
selectedQuery ? CHART_OPACITY.STROKE_SELECTED : CHART_OPACITY.STROKE_NORMAL
}
/>
)}
{selectedQuery && visibleMetrics.query_calls && (
<Area
type="monotone"
dataKey="query_calls"
fill="url(#gradient-query_calls)"
stroke={config.query_calls.color}
strokeWidth={2}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.HIGHLIGHT_FILL}
strokeOpacity={CHART_OPACITY.HIGHLIGHT_STROKE}
/>
)}
</AreaChart>
</ChartContainer>
</div>
</div>
</div>
)
}
// 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 (
<div className="h-[320px] flex flex-col">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex flex-wrap items-center gap-2 px-5 mb-4">
<MetricToggleButtons
chartConfig={chartConfig as ChartConfigResult<any>}
chartConfig={chartConfig}
visibleMetrics={visibleMetrics}
setVisibleMetrics={setVisibleMetrics}
metric={metric}
@@ -618,39 +182,33 @@ export function MetricsChart({
selectedQuery={selectedQuery}
onClear={handleClearSelectedQuery}
metric={metric}
metrics={[]}
metrics={metricBadges}
/>
)}
</div>
<div className="flex-1 min-h-0">
<ChartContainer
className="h-full w-full"
config={{
ok_count: {
label: 'Value',
color: 'hsl(var(--chart-1))',
},
}}
>
<ChartContainer className="h-full w-full" config={config}>
<AreaChart
data={chartData}
style={{ width: '100%', height: '100%' }}
margin={{ right: 16 }}
>
<defs>
<linearGradient id="gradient-generic" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopOpacity={CHART_OPACITY.GRADIENT_START}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopOpacity={CHART_OPACITY.GRADIENT_END}
/>
</linearGradient>
{Object.entries(config).map(([key, value]) => (
<linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor={(value as any).color}
stopOpacity={CHART_OPACITY.GRADIENT_START}
/>
<stop
offset="95%"
stopColor={(value as any).color}
stopOpacity={CHART_OPACITY.GRADIENT_END}
/>
</linearGradient>
))}
</defs>
<CartesianGrid vertical={false} />
<XAxis
@@ -663,26 +221,108 @@ export function MetricsChart({
tickFormatter={(value) => dayjs(value).format('HH:mm')}
/>
<YAxis
tickFormatter={formatMetricValue}
domain={[0, 'auto']}
tickFormatter={yAxisFormatter}
domain={yAxisDomain as any}
allowDataOverflow={false}
width={yAxisWidth}
tickCount={yAxisTickCount}
yAxisId="left"
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" hideLabel />}
/>
<ChartLegend content={<ChartLegendContent />} />
<Area
type="monotone"
dataKey="ok_count"
fill="url(#gradient-generic)"
stroke="hsl(var(--chart-1))"
strokeWidth={1.5}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.FILL_NORMAL}
strokeOpacity={CHART_OPACITY.STROKE_NORMAL}
/>
{/* 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<string, string>)[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 (
<Area
key={key}
type="monotone"
dataKey={key}
fill={`url(#gradient-${key})`}
stroke={(value as any).color}
strokeWidth={1.5}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.HIGHLIGHT_FILL * 0.7}
strokeOpacity={CHART_OPACITY.HIGHLIGHT_STROKE * 0.8}
yAxisId="left"
/>
)
}
// If selected, show selected style
else if (selectedQuery) {
return (
<Area
key={key}
type="monotone"
dataKey={key}
fill={`url(#gradient-${key})`}
stroke={(value as any).color}
strokeWidth={2}
dot={false}
animationDuration={100}
fillOpacity={CHART_OPACITY.HIGHLIGHT_FILL}
strokeOpacity={CHART_OPACITY.HIGHLIGHT_STROKE}
yAxisId="left"
/>
)
}
console.log(`[MetricsChart] Not rendering ${key} - conditions not met`)
return null
}
// Regular series
return (
<Area
key={key}
type="monotone"
dataKey={key}
fill={`url(#gradient-${key})`}
stroke={(value as any).color}
strokeWidth={key.includes('p99') || key.includes('p95') ? 1.5 : 1.5}
dot={false}
animationDuration={100}
fillOpacity={
selectedQuery || hoveredQuery
? CHART_OPACITY.FILL_SELECTED
: CHART_OPACITY.FILL_NORMAL
}
strokeOpacity={
selectedQuery || hoveredQuery
? CHART_OPACITY.STROKE_SELECTED
: CHART_OPACITY.STROKE_NORMAL
}
yAxisId="left"
/>
)
})}
</AreaChart>
</ChartContainer>
</div>
@@ -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<DataGridHandle>(null)
const [selectedRow, setSelectedRow] = useState<number | undefined>()
const [hoveredRow, setHoveredRow] = useState<number | undefined>()
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<QueryInsightsQuery>[] = [
// {
// 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 <Row {...props} key={`qi-row-${rowIdx}`} onClick={() => handleRowClick(idx)} />
return (
<Row
{...props}
key={`qi-row-${rowIdx}`}
onClick={() => handleRowClick(idx)}
// Commenting out hover functionality
// onMouseEnter={() => handleRowMouseEnter(idx)}
// onMouseLeave={handleRowMouseLeave}
/>
)
},
noRowsFallback: isLoading ? (
<div className="absolute top-14 px-6 w-full">
@@ -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<T = any> {
chartConfig: ChartConfigResult<T> | 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 }
}
@@ -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<QueryInsightsMetric[]>
) {
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<QueryInsightsMetric[]>
) {
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<QueryInsightsMetric[]>
) {
return useQuery({