mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 21:29:08 +03:00
716 lines
24 KiB
TypeScript
716 lines
24 KiB
TypeScript
import { useState, useMemo, useEffect, useCallback } from 'react'
|
|
|
|
import {
|
|
Tabs_Shadcn_,
|
|
TabsContent_Shadcn_,
|
|
TabsList_Shadcn_,
|
|
TabsTrigger_Shadcn_,
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
Button,
|
|
} from 'ui'
|
|
import { QueryRowExplorer } from './QueryRowExplorer'
|
|
import { formatNumberWithCommas } from './QueryInsights.utils'
|
|
import {
|
|
useInsightsMetricsQuery,
|
|
useInsightsPrefetchQuery,
|
|
useAllMetricsQuery,
|
|
} from 'data/query-insights/insights-metrics-query'
|
|
import { useInsightsQueriesQuery } from 'data/query-insights/insights-queries-query'
|
|
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
|
import { ComposedChart } from 'components/ui/Charts/ComposedChart'
|
|
import { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
|
|
import dayjs from 'dayjs'
|
|
import { type InsightsQuery } from 'data/query-insights/insights-queries-query'
|
|
import { useInsightsSingleQueryLatency } from 'data/query-insights/insights-single-ql-query'
|
|
import { useInsightsSingleQueryRows } from 'data/query-insights/insights-single-ql-query'
|
|
import { useInsightsSingleQueryCalls } from 'data/query-insights/insights-single-ql-query'
|
|
|
|
interface QueryMetricExplorerProps {
|
|
startTime?: string
|
|
endTime?: string
|
|
}
|
|
|
|
const QueryMetricBlock = ({
|
|
label,
|
|
value,
|
|
}: {
|
|
label: string
|
|
value: string | number | undefined
|
|
}) => {
|
|
return (
|
|
<div className="flex flex-col gap-0.5 text-xs">
|
|
<span className="font-mono text-xs text-foreground-light uppercase">{label}</span>
|
|
<span className="text-lg">{value}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerProps) => {
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const [selectedMetric, setSelectedMetric] = useState<
|
|
'rows_read' | 'query_latency' | 'calls' | 'cache_hits'
|
|
>('query_latency')
|
|
|
|
const [selectedQuery, setSelectedQuery] = useState<InsightsQuery | undefined>()
|
|
const [selectedQueryId, setSelectedQueryId] = useState<number | undefined>()
|
|
|
|
const [visiblePercentiles, setVisiblePercentiles] = useState({
|
|
p50: true,
|
|
p75: true,
|
|
p95: true,
|
|
p99: true,
|
|
p99_9: true,
|
|
})
|
|
|
|
const safeVisiblePercentiles = useMemo(() => {
|
|
const hasVisible = Object.values(visiblePercentiles).some((v) => v)
|
|
if (!hasVisible) {
|
|
return { ...visiblePercentiles, p95: true }
|
|
}
|
|
return visiblePercentiles
|
|
}, [visiblePercentiles])
|
|
|
|
const { startTime: effectiveStartTime, endTime: effectiveEndTime } = useMemo(() => {
|
|
if (startTime && endTime) {
|
|
return { startTime, endTime }
|
|
}
|
|
// Fallback to last 24 hours if no date range provided
|
|
const fallbackEndTime = new Date().toISOString()
|
|
const fallbackStartTime = dayjs().subtract(24, 'hours').toISOString()
|
|
return { startTime: fallbackStartTime, endTime: fallbackEndTime }
|
|
}, [startTime, endTime])
|
|
|
|
useInsightsPrefetchQuery(project?.ref, effectiveStartTime, effectiveEndTime)
|
|
|
|
const {
|
|
data: metricsData,
|
|
isLoading,
|
|
error,
|
|
} = useInsightsMetricsQuery(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime, {
|
|
enabled: !!project?.ref,
|
|
})
|
|
|
|
// Fetch all metrics for the metric blocks
|
|
const { data: allMetricsData, isLoading: isLoadingAllMetrics } = useAllMetricsQuery(
|
|
project?.ref,
|
|
effectiveStartTime,
|
|
effectiveEndTime,
|
|
{
|
|
enabled: !!project?.ref,
|
|
}
|
|
)
|
|
|
|
// Fetch all queries to calculate slowness ratings
|
|
const { data: allQueries, isLoading: isLoadingQueries } = useInsightsQueriesQuery(
|
|
project?.ref,
|
|
effectiveStartTime,
|
|
effectiveEndTime,
|
|
{
|
|
enabled: !!project?.ref,
|
|
}
|
|
)
|
|
|
|
// Fetch single query data for selected query
|
|
const { data: selectedQueryData, isLoading: isLoadingSelectedQuery } =
|
|
useInsightsSingleQueryLatency(
|
|
project?.ref,
|
|
effectiveStartTime,
|
|
effectiveEndTime,
|
|
selectedQuery?.query_id?.toString(),
|
|
{
|
|
enabled: !!project?.ref && !!selectedQuery?.query_id && selectedMetric === 'query_latency',
|
|
}
|
|
)
|
|
|
|
const { data: selectedQueryRowsData, isLoading: isLoadingSelectedQueryRows } =
|
|
useInsightsSingleQueryRows(
|
|
project?.ref,
|
|
effectiveStartTime,
|
|
effectiveEndTime,
|
|
selectedQuery?.query_id?.toString(),
|
|
{
|
|
enabled: !!project?.ref && !!selectedQuery?.query_id && selectedMetric === 'rows_read',
|
|
}
|
|
)
|
|
|
|
const { data: selectedQueryCallsData, isLoading: isLoadingSelectedQueryCalls } =
|
|
useInsightsSingleQueryCalls(
|
|
project?.ref,
|
|
effectiveStartTime,
|
|
effectiveEndTime,
|
|
selectedQuery?.query_id?.toString(),
|
|
{
|
|
enabled: !!project?.ref && !!selectedQuery?.query_id && selectedMetric === 'calls',
|
|
}
|
|
)
|
|
|
|
// Calculate queries with slowness_rating of "NOTICEABLE" and above
|
|
const slowQueriesCount = useMemo(() => {
|
|
if (!allQueries) return 0
|
|
|
|
return allQueries.filter((query) => {
|
|
const rating = query.slowness_rating
|
|
return rating === 'NOTICEABLE' || rating === 'SLOW' || rating === 'CRITICAL'
|
|
}).length
|
|
}, [allQueries])
|
|
|
|
// Calculate the average value for the selected query based on the current metric
|
|
const selectedQueryAverage = useMemo(() => {
|
|
if (!selectedQuery) return undefined
|
|
|
|
switch (selectedMetric) {
|
|
case 'query_latency':
|
|
return selectedQuery.mean_exec_time
|
|
case 'calls':
|
|
return selectedQuery.calls
|
|
case 'rows_read':
|
|
return selectedQuery.rows_read || 0
|
|
case 'cache_hits':
|
|
const totalBlocks =
|
|
(selectedQuery.shared_blks_hit || 0) + (selectedQuery.shared_blks_read || 0)
|
|
return totalBlocks > 0 ? ((selectedQuery.shared_blks_hit || 0) / totalBlocks) * 100 : 0
|
|
default:
|
|
return undefined
|
|
}
|
|
}, [selectedQuery, selectedMetric])
|
|
|
|
const getChartAttributes = useMemo((): MultiAttribute[] => {
|
|
const metricConfigs = {
|
|
rows_read: { label: 'Rows Read', format: '' },
|
|
query_latency: { label: 'Query Latency', format: 'ms' },
|
|
calls: { label: 'Calls', format: '' },
|
|
cache_hits: { label: 'Cache Hit Rate', format: '%' },
|
|
}
|
|
|
|
const config = metricConfigs[selectedMetric]
|
|
|
|
if (selectedMetric === 'query_latency') {
|
|
const attributes: MultiAttribute[] = []
|
|
|
|
if (safeVisiblePercentiles.p50) {
|
|
attributes.push({
|
|
attribute: 'p50',
|
|
label: 'P50',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
if (safeVisiblePercentiles.p75) {
|
|
attributes.push({
|
|
attribute: 'p75',
|
|
label: 'P75',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
if (safeVisiblePercentiles.p95) {
|
|
attributes.push({
|
|
attribute: 'p95',
|
|
label: 'P95',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
if (safeVisiblePercentiles.p99) {
|
|
attributes.push({
|
|
attribute: 'p99',
|
|
label: 'P99',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
if (safeVisiblePercentiles.p99_9) {
|
|
attributes.push({
|
|
attribute: 'p99_9',
|
|
label: 'P99.9',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
// Ensure we always have at least one attribute (p95 as fallback)
|
|
if (attributes.length === 0) {
|
|
attributes.push({
|
|
attribute: 'p95',
|
|
label: 'P95',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
// Add selected query attribute if available
|
|
if (selectedQuery && selectedQueryAverage !== undefined) {
|
|
attributes.push({
|
|
attribute: 'selected',
|
|
label: 'Selected',
|
|
format: 'ms',
|
|
provider: 'query-insights' as any,
|
|
color: {
|
|
light: '#dc2626', // Bright red color for selected query
|
|
dark: '#ef4444',
|
|
},
|
|
strokeDasharray: '5 5', // Dashed line
|
|
type: 'line',
|
|
strokeWidth: 2, // Match Database Reports stroke width
|
|
})
|
|
}
|
|
|
|
return attributes
|
|
}
|
|
|
|
const attributes: MultiAttribute[] = []
|
|
|
|
if (selectedMetric === 'cache_hits') {
|
|
// For cache hits, show both hits and misses as separate lines
|
|
attributes.push(
|
|
{
|
|
attribute: 'cache_hit_rate',
|
|
label: 'Cache Hit Rate',
|
|
format: '%',
|
|
provider: 'query-insights' as any,
|
|
color: {
|
|
light: '#10b981', // Green for hits
|
|
dark: '#34d399',
|
|
},
|
|
},
|
|
{
|
|
attribute: 'cache_miss_rate',
|
|
label: 'Cache Miss Rate',
|
|
format: '%',
|
|
provider: 'query-insights' as any,
|
|
color: {
|
|
light: '#ef4444', // Red for misses
|
|
dark: '#f87171',
|
|
},
|
|
}
|
|
)
|
|
} else {
|
|
// For other metrics, use the default single attribute
|
|
attributes.push({
|
|
attribute: selectedMetric,
|
|
label: config.label,
|
|
format: config.format,
|
|
provider: 'query-insights' as any,
|
|
})
|
|
}
|
|
|
|
// Add selected query attribute if available
|
|
if (selectedQuery && selectedQueryAverage !== undefined) {
|
|
attributes.push({
|
|
attribute: 'selected',
|
|
label: 'Selected',
|
|
format: config.format,
|
|
provider: 'query-insights' as any,
|
|
color: {
|
|
light: '#dc2626', // Bright red color for selected query
|
|
dark: '#ef4444',
|
|
},
|
|
strokeDasharray: '5 5', // Dashed line
|
|
type: 'line',
|
|
strokeWidth: 2, // Match Database Reports stroke width
|
|
})
|
|
}
|
|
|
|
return attributes
|
|
}, [selectedMetric, safeVisiblePercentiles, selectedQuery])
|
|
|
|
const transformedChartAttributes = useMemo(() => {
|
|
return getChartAttributes
|
|
.filter((attr) => attr.label && attr.format) // Filter out undefined values
|
|
.map((attr) => ({
|
|
attribute: attr.attribute,
|
|
label: attr.label!,
|
|
format: attr.format!,
|
|
color: attr.color,
|
|
strokeDasharray: attr.strokeDasharray,
|
|
type: attr.type === 'line' ? ('line' as const) : undefined,
|
|
strokeWidth: typeof attr.strokeWidth === 'number' ? attr.strokeWidth : undefined,
|
|
}))
|
|
}, [getChartAttributes])
|
|
|
|
// Calculate average P95 for query latency
|
|
const averageP95 = useMemo(() => {
|
|
if (selectedMetric === 'query_latency' && metricsData && metricsData.length > 0) {
|
|
const totalP95 = metricsData.reduce((sum, item) => sum + (item.p95 || 0), 0)
|
|
return totalP95 / metricsData.length
|
|
}
|
|
return undefined
|
|
}, [selectedMetric, metricsData])
|
|
|
|
// Get all metrics from the single query
|
|
const totalRowsRead = useMemo(() => {
|
|
if (allMetricsData?.total_rows_read !== undefined) {
|
|
return formatNumberWithCommas(allMetricsData.total_rows_read)
|
|
}
|
|
return '0'
|
|
}, [allMetricsData])
|
|
|
|
const totalCalls = useMemo(() => {
|
|
if (allMetricsData?.total_calls !== undefined) {
|
|
return formatNumberWithCommas(allMetricsData.total_calls)
|
|
}
|
|
return '0'
|
|
}, [allMetricsData])
|
|
|
|
const totalHits = useMemo(() => {
|
|
if (allMetricsData?.total_hits !== undefined) {
|
|
return formatNumberWithCommas(allMetricsData.total_hits)
|
|
}
|
|
return '0'
|
|
}, [allMetricsData])
|
|
|
|
const totalCacheHits = useMemo(() => {
|
|
if (
|
|
allMetricsData?.total_cache_hits !== undefined &&
|
|
allMetricsData?.total_hits !== undefined
|
|
) {
|
|
const hitRate =
|
|
allMetricsData.total_hits > 0
|
|
? (allMetricsData.total_cache_hits / allMetricsData.total_hits) * 100
|
|
: 0
|
|
return hitRate.toFixed(1) + '%'
|
|
}
|
|
return '0%'
|
|
}, [allMetricsData])
|
|
|
|
const totalCacheMisses = useMemo(() => {
|
|
if (
|
|
allMetricsData?.total_cache_misses !== undefined &&
|
|
allMetricsData?.total_hits !== undefined
|
|
) {
|
|
const missRate =
|
|
allMetricsData.total_hits > 0
|
|
? (allMetricsData.total_cache_misses / allMetricsData.total_hits) * 100
|
|
: 0
|
|
return missRate.toFixed(1) + '%'
|
|
}
|
|
return '0%'
|
|
}, [allMetricsData])
|
|
|
|
// Transform data for the chart
|
|
const chartData = useMemo(() => {
|
|
if (!metricsData || metricsData.length === 0) {
|
|
return {
|
|
data: [],
|
|
format: '',
|
|
total: 0,
|
|
totalGrouped: {},
|
|
}
|
|
}
|
|
|
|
const transformed = metricsData.map((item) => {
|
|
// Convert timestamp to ISO string format as expected by ChartData
|
|
const periodStart = new Date(item.timestamp).toISOString()
|
|
const timestamp = new Date(item.timestamp).getTime()
|
|
const value = item.value || 0
|
|
|
|
// Add percentile data for query latency
|
|
if (selectedMetric === 'query_latency') {
|
|
const percentileData: any = {
|
|
period_start: periodStart,
|
|
timestamp: timestamp,
|
|
}
|
|
|
|
// Only include percentiles that are visible
|
|
if (safeVisiblePercentiles.p50) percentileData.p50 = item.p50 || 0
|
|
if (safeVisiblePercentiles.p75) percentileData.p75 = item.p75 || 0
|
|
if (safeVisiblePercentiles.p95) percentileData.p95 = item.p95 || 0
|
|
if (safeVisiblePercentiles.p99) percentileData.p99 = item.p99 || 0
|
|
if (safeVisiblePercentiles.p99_9) percentileData.p99_9 = item.p99_9 || 0
|
|
|
|
return percentileData
|
|
}
|
|
|
|
// Handle cache_hits metric specially to show both hit and miss rates
|
|
if (selectedMetric === 'cache_hits') {
|
|
const sharedBlksHit = item.shared_blks_hit || 0
|
|
const sharedBlksRead = item.shared_blks_read || 0
|
|
const totalBlocks = sharedBlksHit + sharedBlksRead
|
|
|
|
const hitRate = totalBlocks > 0 ? (sharedBlksHit / totalBlocks) * 100 : 0
|
|
const missRate = totalBlocks > 0 ? (sharedBlksRead / totalBlocks) * 100 : 0
|
|
|
|
return {
|
|
period_start: periodStart,
|
|
timestamp: timestamp,
|
|
cache_hit_rate: hitRate,
|
|
cache_miss_rate: missRate,
|
|
}
|
|
}
|
|
|
|
// For other non-query_latency metrics, use the selectedMetric as the key
|
|
return {
|
|
period_start: periodStart,
|
|
timestamp: timestamp,
|
|
[selectedMetric]: value,
|
|
}
|
|
})
|
|
|
|
// Add selected query data if available
|
|
if (selectedQuery && selectedQueryAverage !== undefined) {
|
|
// Get the appropriate single query data based on the selected metric
|
|
let selectedQueryTimeSeriesData: any[] = []
|
|
|
|
if (selectedMetric === 'query_latency' && selectedQueryData) {
|
|
selectedQueryTimeSeriesData = selectedQueryData
|
|
} else if (selectedMetric === 'rows_read' && selectedQueryRowsData) {
|
|
selectedQueryTimeSeriesData = selectedQueryRowsData
|
|
} else if (selectedMetric === 'calls' && selectedQueryCallsData) {
|
|
selectedQueryTimeSeriesData = selectedQueryCallsData
|
|
}
|
|
|
|
if (selectedQueryTimeSeriesData.length > 0) {
|
|
// Transform the single query data to match the chart format
|
|
const selectedQueryTransformed = selectedQueryTimeSeriesData.map((item) => {
|
|
const periodStart = new Date(item.timestamp).toISOString()
|
|
const timestamp = new Date(item.timestamp).getTime()
|
|
|
|
return {
|
|
period_start: periodStart,
|
|
timestamp: timestamp,
|
|
selected: item.value,
|
|
}
|
|
})
|
|
|
|
// Fill gaps in the data to ensure continuous lines
|
|
const filledSelectedData = transformed.map((mainItem) => {
|
|
// Find the closest timestamp within a reasonable range (5 minutes)
|
|
const timeWindow = 5 * 60 * 1000 // 5 minutes in milliseconds
|
|
const matchingSelectedItem = selectedQueryTransformed.find(
|
|
(selectedItem) => Math.abs(selectedItem.timestamp - mainItem.timestamp) <= timeWindow
|
|
)
|
|
|
|
return {
|
|
...mainItem,
|
|
selected: matchingSelectedItem?.selected,
|
|
}
|
|
})
|
|
|
|
// Merge the selected query data with the main chart data
|
|
transformed.splice(0, transformed.length, ...filledSelectedData)
|
|
} else {
|
|
// Fallback to the old flat line approach if no time-series data
|
|
const transformedWithSelected = transformed.map((item) => ({
|
|
...item,
|
|
selected: selectedQueryAverage,
|
|
}))
|
|
transformed.splice(0, transformed.length, ...transformedWithSelected)
|
|
}
|
|
}
|
|
|
|
// Calculate totals for the ChartData format
|
|
const total = transformed.reduce((sum, item) => {
|
|
if (selectedMetric === 'query_latency') {
|
|
// For query latency, sum the p95 values as the main metric
|
|
const p95Value = (item as any).p95 || 0
|
|
return sum + p95Value
|
|
} else if (selectedMetric === 'cache_hits') {
|
|
// For cache hits, use the hit rate as the main metric
|
|
const hitRate = (item as any).cache_hit_rate || 0
|
|
return sum + hitRate
|
|
} else {
|
|
// For other metrics, use the selectedMetric key
|
|
const value = (item as any)[selectedMetric]
|
|
return sum + (typeof value === 'number' ? value : 0)
|
|
}
|
|
}, 0)
|
|
|
|
const totalGrouped = (() => {
|
|
if (selectedMetric === 'query_latency') {
|
|
return { p95: total }
|
|
} else if (selectedMetric === 'cache_hits') {
|
|
return { cache_hit_rate: total }
|
|
} else {
|
|
return { [selectedMetric]: total }
|
|
}
|
|
})()
|
|
|
|
return {
|
|
data: transformed,
|
|
format: getChartAttributes[0].format || '',
|
|
total,
|
|
totalGrouped,
|
|
}
|
|
}, [
|
|
metricsData,
|
|
selectedMetric,
|
|
safeVisiblePercentiles,
|
|
selectedQuery,
|
|
selectedQueryAverage,
|
|
selectedQueryData,
|
|
selectedQueryRowsData,
|
|
selectedQueryCallsData,
|
|
])
|
|
|
|
// Mock updateDateRange function for LogChartHandler
|
|
const updateDateRange = () => {
|
|
// This is a no-op since we're not implementing date range updates in this component
|
|
console.log('updateDateRange called - not implemented in QueryMetricExplorer')
|
|
}
|
|
|
|
// Handle query selection from child component
|
|
const handleQuerySelect = useCallback(
|
|
(query: InsightsQuery | undefined) => {
|
|
setSelectedQuery(query)
|
|
setSelectedQueryId(query?.query_id)
|
|
},
|
|
[] // No dependencies to prevent unnecessary recreations
|
|
)
|
|
|
|
// Clear selected query
|
|
const clearSelectedQuery = useCallback(() => {
|
|
setSelectedQuery(undefined)
|
|
setSelectedQueryId(undefined)
|
|
}, []) // No dependencies to prevent unnecessary recreations
|
|
|
|
// Reset selection when date range changes
|
|
useEffect(() => {
|
|
clearSelectedQuery()
|
|
}, [effectiveStartTime, effectiveEndTime])
|
|
|
|
return (
|
|
<div className="w-full mb-4">
|
|
<Card>
|
|
<Tabs_Shadcn_
|
|
value={selectedMetric}
|
|
onValueChange={(value) => setSelectedMetric(value as any)}
|
|
className="w-full"
|
|
>
|
|
<CardHeader className="h-10 py-0 pl-4 pr-2 flex flex-row items-center justify-between flex-0">
|
|
<TabsList_Shadcn_ className="flex justify-start rounded-none gap-x-4 border-b-0 !mt-0 pt-0">
|
|
<TabsTrigger_Shadcn_
|
|
value="query_latency"
|
|
className="flex items-center gap-2 text-xs py-3 border-b-[1px] font-mono uppercase"
|
|
>
|
|
Query latency
|
|
</TabsTrigger_Shadcn_>
|
|
<TabsTrigger_Shadcn_
|
|
value="rows_read"
|
|
className="flex items-center gap-2 text-xs py-3 border-b-[1px] font-mono uppercase"
|
|
>
|
|
Rows read
|
|
</TabsTrigger_Shadcn_>
|
|
<TabsTrigger_Shadcn_
|
|
value="calls"
|
|
className="flex items-center gap-2 text-xs py-3 border-b-[1px] font-mono uppercase"
|
|
>
|
|
Calls
|
|
</TabsTrigger_Shadcn_>
|
|
<TabsTrigger_Shadcn_
|
|
value="cache_hits"
|
|
className="flex items-center gap-2 text-xs py-3 border-b-[1px] font-mono uppercase"
|
|
>
|
|
Cache hits
|
|
</TabsTrigger_Shadcn_>
|
|
</TabsList_Shadcn_>
|
|
</CardHeader>
|
|
|
|
<CardContent className="!p-0 mt-0 flex-1">
|
|
<TabsContent_Shadcn_ value={selectedMetric} className="bg-surface-100 mt-0">
|
|
<div className="p-4 relative">
|
|
{selectedQuery && (
|
|
<div className="absolute top-4 right-4 z-10">
|
|
<Button type="default" size="tiny" onClick={clearSelectedQuery}>
|
|
Clear query
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isLoading ||
|
|
isLoadingQueries ||
|
|
(selectedQuery && selectedMetric === 'query_latency' && isLoadingSelectedQuery) ||
|
|
(selectedQuery && selectedMetric === 'rows_read' && isLoadingSelectedQueryRows) ||
|
|
(selectedQuery && selectedMetric === 'calls' && isLoadingSelectedQueryCalls) ? (
|
|
<div className="h-full min-h-[264px] flex items-center justify-center">
|
|
<div className="text-sm text-foreground-lighter">Loading metrics...</div>
|
|
</div>
|
|
) : chartData.data.length > 0 ? (
|
|
<>
|
|
{(() => {
|
|
const metricsConfig = {
|
|
query_latency: [
|
|
{
|
|
label: 'Average p95',
|
|
value: averageP95?.toFixed(2).toLocaleString() + 'ms',
|
|
},
|
|
{ label: 'Slow queries', value: slowQueriesCount },
|
|
],
|
|
rows_read: [{ label: 'Total rows read', value: totalRowsRead }],
|
|
calls: [{ label: 'Total calls', value: totalCalls }],
|
|
cache_hits: [
|
|
{ label: 'Total hits', value: totalHits },
|
|
{ label: 'Cache hits', value: totalCacheHits },
|
|
{ label: 'Cache misses', value: totalCacheMisses },
|
|
],
|
|
}
|
|
|
|
const currentMetrics =
|
|
metricsConfig[selectedMetric as keyof typeof metricsConfig] || []
|
|
|
|
return (
|
|
currentMetrics.length > 0 && (
|
|
<div className="flex flex-row gap-6 mt-1 mb-5 relative w-full">
|
|
{currentMetrics.map((metric, index) => (
|
|
<QueryMetricBlock
|
|
key={index}
|
|
label={metric.label}
|
|
value={metric.value}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
)
|
|
})()}
|
|
|
|
<ComposedChart
|
|
data={chartData.data}
|
|
attributes={getChartAttributes}
|
|
yAxisKey={getChartAttributes[0].attribute}
|
|
xAxisKey="period_start"
|
|
title={''}
|
|
customDateFormat="HH:mm"
|
|
hideChartType={true}
|
|
hideHighlightArea={true}
|
|
showTooltip={true}
|
|
showGrid={true}
|
|
showLegend={
|
|
selectedMetric === 'query_latency' ||
|
|
selectedMetric === 'cache_hits' ||
|
|
!!selectedQuery
|
|
}
|
|
showTotal={false}
|
|
showMaxValue={false}
|
|
updateDateRange={updateDateRange}
|
|
YAxisProps={{
|
|
tick: true,
|
|
width: 60,
|
|
tickFormatter: (value) => value.toLocaleString(),
|
|
}}
|
|
className="my-3"
|
|
/>
|
|
</>
|
|
) : (
|
|
<div className="h-full flex items-center justify-center">
|
|
<div className="text-sm text-foreground-lighter">No data available</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent_Shadcn_>
|
|
</CardContent>
|
|
</Tabs_Shadcn_>
|
|
<QueryRowExplorer
|
|
startTime={effectiveStartTime}
|
|
endTime={effectiveEndTime}
|
|
onQuerySelect={handleQuerySelect}
|
|
selectedQueryId={selectedQueryId}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|