mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
moar
This commit is contained in:
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({
|
||||
|
||||
Reference in new issue
Block a user