mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
feat: working query select again
This commit is contained in:
1 parent
0dbb5c3ff2
commit
018ac807e4
4 files changed
+366
-469
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useState, useMemo, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Tabs_Shadcn_,
|
||||
TabsContent_Shadcn_,
|
||||
@@ -17,6 +17,13 @@ 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,
|
||||
useInsightsSingleQueryCalls,
|
||||
useInsightsSingleQueryRows,
|
||||
useInsightsSingleQueryRowsWritten,
|
||||
} from 'data/query-insights/insights-single-ql-query'
|
||||
|
||||
interface QueryMetricExplorerProps {
|
||||
startTime?: string
|
||||
@@ -29,6 +36,17 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
'rows_read' | 'query_latency' | 'queries_per_second' | 'calls' | 'cache_hits'
|
||||
>('query_latency')
|
||||
|
||||
// State to track selected query for chart highlighting
|
||||
const [selectedQuery, setSelectedQuery] = useState<InsightsQuery | undefined>()
|
||||
const [selectedQueryId, setSelectedQueryId] = useState<number | undefined>()
|
||||
|
||||
// Debug logging for query selection (commented out to reduce console noise)
|
||||
// console.log('QueryMetricExplorer Query Selection:', {
|
||||
// selectedQuery: selectedQuery?.query_id,
|
||||
// selectedQueryId,
|
||||
// selectedQueryQuery: selectedQuery?.query?.substring(0, 50) + '...',
|
||||
// })
|
||||
|
||||
// State to control which percentile lines are visible for query latency
|
||||
const [visiblePercentiles, setVisiblePercentiles] = useState({
|
||||
p50: true,
|
||||
@@ -62,30 +80,58 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
// Pre-fetch query data for the QueryRowExplorer
|
||||
useInsightsPrefetchQuery(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging
|
||||
console.log('QueryMetricExplorer Debug:', {
|
||||
props: { startTime, endTime },
|
||||
effective: { startTime: effectiveStartTime, endTime: effectiveEndTime },
|
||||
project: { ref: project?.ref, hasConnectionString: !!project?.connectionString },
|
||||
selectedMetric,
|
||||
})
|
||||
// Debug logging (commented out to reduce console noise)
|
||||
// console.log('QueryMetricExplorer Debug:', {
|
||||
// props: { startTime, endTime },
|
||||
// effective: { startTime: effectiveStartTime, endTime: effectiveEndTime },
|
||||
// project: { ref: project?.ref, hasConnectionString: !!project?.connectionString },
|
||||
// selectedMetric,
|
||||
// projectRefUndefined: !project?.ref,
|
||||
// })
|
||||
|
||||
const {
|
||||
data: metricsData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useInsightsMetricsQuery(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging for query results
|
||||
console.log('QueryMetricExplorer Query Results:', {
|
||||
metricsData,
|
||||
isLoading,
|
||||
error,
|
||||
dataLength: metricsData?.length,
|
||||
} = useInsightsMetricsQuery(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime, {
|
||||
enabled: !!project?.ref,
|
||||
})
|
||||
|
||||
// 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':
|
||||
// Calculate cache hit rate from shared_blks_hit and shared_blks_read
|
||||
const totalBlocks =
|
||||
(selectedQuery.shared_blks_hit || 0) + (selectedQuery.shared_blks_read || 0)
|
||||
return totalBlocks > 0 ? ((selectedQuery.shared_blks_hit || 0) / totalBlocks) * 100 : 0
|
||||
case 'queries_per_second':
|
||||
// Calculate QPS from calls and total_time
|
||||
const totalTimeSeconds = (selectedQuery.total_time || 0) / 1000
|
||||
return totalTimeSeconds > 0 ? (selectedQuery.calls || 0) / totalTimeSeconds : 0
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}, [selectedQuery, selectedMetric])
|
||||
|
||||
// Debug logging for query results (commented out to reduce console noise)
|
||||
// console.log('QueryMetricExplorer Query Results:', {
|
||||
// metricsData,
|
||||
// isLoading,
|
||||
// error,
|
||||
// dataLength: metricsData?.length,
|
||||
// })
|
||||
|
||||
// Define chart attributes based on selected metric
|
||||
const getChartAttributes = (): MultiAttribute[] => {
|
||||
const getChartAttributes = useMemo((): MultiAttribute[] => {
|
||||
const metricConfigs = {
|
||||
rows_read: { label: 'Rows Read', format: '' },
|
||||
query_latency: { label: 'Query Latency', format: 'ms' },
|
||||
@@ -154,10 +200,27 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
})
|
||||
}
|
||||
|
||||
// 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: 4, // Even thicker line for more prominence
|
||||
})
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
|
||||
return [
|
||||
const attributes: MultiAttribute[] = [
|
||||
{
|
||||
attribute: selectedMetric,
|
||||
label: config.label,
|
||||
@@ -165,7 +228,26 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
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: 4, // Even thicker line for more prominence
|
||||
})
|
||||
}
|
||||
|
||||
return attributes
|
||||
}, [selectedMetric, safeVisiblePercentiles, selectedQuery])
|
||||
|
||||
// Calculate average P95 for query latency
|
||||
const averageP95 = useMemo(() => {
|
||||
@@ -179,7 +261,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
// Transform data for the chart
|
||||
const chartData = useMemo(() => {
|
||||
if (!metricsData || metricsData.length === 0) {
|
||||
console.log('QueryMetricExplorer: No metrics data available')
|
||||
// console.log('QueryMetricExplorer: No metrics data available')
|
||||
return {
|
||||
data: [],
|
||||
format: '',
|
||||
@@ -188,12 +270,12 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
}
|
||||
}
|
||||
|
||||
console.log('QueryMetricExplorer: Processing metrics data:', {
|
||||
metricsData,
|
||||
selectedMetric,
|
||||
dataLength: metricsData.length,
|
||||
firstItem: metricsData[0],
|
||||
})
|
||||
// console.log('QueryMetricExplorer: Processing metrics data:', {
|
||||
// metricsData,
|
||||
// selectedMetric,
|
||||
// dataLength: metricsData.length,
|
||||
// firstItem: metricsData[0],
|
||||
// })
|
||||
|
||||
const transformed = metricsData.map((item) => {
|
||||
// Convert timestamp to ISO string format as expected by ChartData
|
||||
@@ -226,6 +308,31 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
}
|
||||
})
|
||||
|
||||
// Add selected query data if available
|
||||
if (selectedQuery && selectedQueryAverage !== undefined) {
|
||||
// Add the selected query average as a constant line across all data points
|
||||
const transformedWithSelected = transformed.map((item) => ({
|
||||
...item,
|
||||
selected: selectedQueryAverage,
|
||||
}))
|
||||
transformed.splice(0, transformed.length, ...transformedWithSelected)
|
||||
|
||||
// Debug: Check if selected query data was added
|
||||
console.log('Chart Data Merge Debug:', {
|
||||
selectedQueryId: selectedQuery?.query_id,
|
||||
selectedQueryAverage,
|
||||
transformedLength: transformed.length,
|
||||
hasSelectedData: transformed.some((item) => item.selected !== undefined),
|
||||
sampleTransformedItem: transformed[0],
|
||||
chartAttributes: getChartAttributes.map((attr) => ({
|
||||
attribute: attr.attribute,
|
||||
label: attr.label,
|
||||
type: attr.type,
|
||||
})),
|
||||
hasSelectedAttribute: getChartAttributes.some((attr) => attr.attribute === 'selected'),
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate totals for the ChartData format
|
||||
const total = transformed.reduce((sum, item) => {
|
||||
if (selectedMetric === 'query_latency') {
|
||||
@@ -244,23 +351,29 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
? { p95: total } // Use p95 as the main metric for query latency
|
||||
: { [selectedMetric]: total }
|
||||
|
||||
console.log('QueryMetricExplorer Chart Data:', {
|
||||
originalData: metricsData,
|
||||
transformedData: transformed,
|
||||
selectedMetric,
|
||||
dataLength: transformed.length,
|
||||
visiblePercentiles,
|
||||
total,
|
||||
totalGrouped,
|
||||
})
|
||||
// console.log('QueryMetricExplorer Chart Data:', {
|
||||
// originalData: metricsData,
|
||||
// transformedData: transformed,
|
||||
// selectedMetric,
|
||||
// dataLength: transformed.length,
|
||||
// visiblePercentiles,
|
||||
// total,
|
||||
// totalGrouped,
|
||||
// selectedQuery: selectedQuery?.query_id,
|
||||
// selectedQueryDataLength: selectedQueryData.length,
|
||||
// selectedQueryData: selectedQueryData,
|
||||
// selectedQueryLatency: selectedQueryLatency,
|
||||
// selectedQueryCalls: selectedQueryCalls,
|
||||
// selectedQueryRows: selectedQueryRows,
|
||||
// })
|
||||
|
||||
return {
|
||||
data: transformed,
|
||||
format: getChartAttributes()[0].format || '',
|
||||
format: getChartAttributes[0].format || '',
|
||||
total,
|
||||
totalGrouped,
|
||||
}
|
||||
}, [metricsData, selectedMetric, safeVisiblePercentiles])
|
||||
}, [metricsData, selectedMetric, safeVisiblePercentiles, selectedQuery, selectedQueryAverage])
|
||||
|
||||
// Mock updateDateRange function for LogChartHandler
|
||||
const updateDateRange = () => {
|
||||
@@ -268,6 +381,38 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
console.log('updateDateRange called - not implemented in QueryMetricExplorer')
|
||||
}
|
||||
|
||||
// Handle query selection from child component
|
||||
const handleQuerySelect = useCallback(
|
||||
(query: InsightsQuery | undefined) => {
|
||||
// console.log('QueryMetricExplorer: handleQuerySelect called', {
|
||||
// newQuery: query?.query_id,
|
||||
// currentSelectedQuery: selectedQuery?.query_id,
|
||||
// currentSelectedQueryId: selectedQueryId,
|
||||
// willUpdate:
|
||||
// query?.query_id !== selectedQuery?.query_id || query?.query_id !== selectedQueryId,
|
||||
// timestamp: new Date().toISOString(),
|
||||
// })
|
||||
setSelectedQuery(query)
|
||||
setSelectedQueryId(query?.query_id)
|
||||
},
|
||||
[] // No dependencies to prevent unnecessary recreations
|
||||
)
|
||||
|
||||
// Clear selected query
|
||||
const clearSelectedQuery = useCallback(() => {
|
||||
// console.log('QueryMetricExplorer: Clearing selected query', {
|
||||
// currentSelectedQuery: selectedQuery?.query_id,
|
||||
// currentSelectedQueryId: selectedQueryId,
|
||||
// })
|
||||
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>
|
||||
@@ -309,6 +454,18 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
QPS
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
{selectedQuery && (
|
||||
<div className="flex items-center gap-2 text-xs text-foreground-light">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<span>Query {selectedQuery.query_id} highlighted</span>
|
||||
<button
|
||||
onClick={clearSelectedQuery}
|
||||
className="text-foreground-light hover:text-foreground"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="!p-0 mt-0 flex-1">
|
||||
@@ -320,16 +477,32 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
</div>
|
||||
) : chartData.data.length > 0 ? (
|
||||
<>
|
||||
{(() => {
|
||||
console.log('Chart Data Final Debug:', {
|
||||
chartDataLength: chartData.data.length,
|
||||
firstDataPoint: chartData.data[0],
|
||||
attributes: getChartAttributes.map((attr) => attr.attribute),
|
||||
selectedQuery: selectedQuery?.query_id,
|
||||
// Check if selected query data is in the final chart data
|
||||
hasSelectedQueryData: chartData.data.some(
|
||||
(item) => item.selected !== undefined
|
||||
),
|
||||
selectedQueryDataPoints: chartData.data.filter(
|
||||
(item) => item.selected !== undefined
|
||||
).length,
|
||||
})
|
||||
return null
|
||||
})()}
|
||||
<ComposedChart
|
||||
data={chartData.data}
|
||||
attributes={getChartAttributes()}
|
||||
yAxisKey={getChartAttributes()[0].attribute}
|
||||
xAxisKey="timestamp"
|
||||
title={getChartAttributes()[0].label || ''}
|
||||
attributes={getChartAttributes}
|
||||
yAxisKey={getChartAttributes[0].attribute}
|
||||
xAxisKey="period_start"
|
||||
title={getChartAttributes[0].label || ''}
|
||||
customDateFormat="HH:mm"
|
||||
hideChartType={true}
|
||||
showTooltip={true}
|
||||
showLegend={selectedMetric === 'query_latency'}
|
||||
showLegend={selectedMetric === 'query_latency' || !!selectedQuery}
|
||||
showTotal={false}
|
||||
showMaxValue={false}
|
||||
updateDateRange={updateDateRange}
|
||||
@@ -344,7 +517,12 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
|
||||
</TabsContent_Shadcn_>
|
||||
</CardContent>
|
||||
</Tabs_Shadcn_>
|
||||
<QueryRowExplorer startTime={effectiveStartTime} endTime={effectiveEndTime} />
|
||||
<QueryRowExplorer
|
||||
startTime={effectiveStartTime}
|
||||
endTime={effectiveEndTime}
|
||||
onQuerySelect={handleQuerySelect}
|
||||
selectedQueryId={selectedQueryId}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,26 +25,29 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps)
|
||||
return { startTime: fallbackStartTime, endTime: fallbackEndTime }
|
||||
}, [startTime, endTime])
|
||||
|
||||
// Debug logging
|
||||
console.log('QueryQuickGlance Debug:', {
|
||||
props: { startTime, endTime },
|
||||
effective: { startTime: effectiveStartTime, endTime: effectiveEndTime },
|
||||
project: { ref: project?.ref, hasConnectionString: !!project?.connectionString },
|
||||
})
|
||||
// Debug logging (commented out to reduce console noise)
|
||||
// console.log('QueryQuickGlance Debug:', {
|
||||
// props: { startTime, endTime },
|
||||
// effective: { startTime: effectiveStartTime, endTime: effectiveEndTime },
|
||||
// project: { ref: project?.ref, hasConnectionString: !!project?.connectionString },
|
||||
// projectRefUndefined: !project?.ref,
|
||||
// })
|
||||
|
||||
const {
|
||||
data: glanceData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useInsightsGlanceQuery(project?.ref, effectiveStartTime, effectiveEndTime)
|
||||
|
||||
// Debug logging for glance results
|
||||
console.log('QueryQuickGlance Results:', {
|
||||
glanceData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useInsightsGlanceQuery(project?.ref, effectiveStartTime, effectiveEndTime, {
|
||||
enabled: !!project?.ref,
|
||||
})
|
||||
|
||||
// Debug logging for glance results (commented out to reduce console noise)
|
||||
// console.log('QueryQuickGlance Results:', {
|
||||
// glanceData,
|
||||
// isLoading,
|
||||
// error,
|
||||
// })
|
||||
|
||||
const cards = useMemo(() => {
|
||||
if (!glanceData) return []
|
||||
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import { Search, TextSearch, X } from 'lucide-react'
|
||||
import { Search, TextSearch } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState, useMemo } from 'react'
|
||||
import DataGrid, { DataGridHandle, Row } from 'react-data-grid'
|
||||
|
||||
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import {
|
||||
Button,
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
TabsContent_Shadcn_,
|
||||
TabsList_Shadcn_,
|
||||
TabsTrigger_Shadcn_,
|
||||
Tabs_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { cn } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FilterPopover } from 'components/ui/FilterPopover'
|
||||
@@ -24,45 +14,52 @@ import {
|
||||
useInsightsQueriesQuery,
|
||||
type InsightsQuery,
|
||||
} from 'data/query-insights/insights-queries-query'
|
||||
import {
|
||||
useInsightsSingleQueryLatency,
|
||||
useInsightsSingleQueryCalls,
|
||||
useInsightsSingleQueryRows,
|
||||
useInsightsSingleQueryRowsWritten,
|
||||
} from 'data/query-insights/insights-single-ql-query'
|
||||
import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants'
|
||||
import { formatQueryInsightsColumns } from './QueryInsights.utils'
|
||||
|
||||
interface QueryRowExplorerProps {
|
||||
startTime: string
|
||||
endTime: string
|
||||
onQuerySelect?: (query: InsightsQuery | undefined) => void
|
||||
selectedQueryId?: number
|
||||
}
|
||||
|
||||
export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps) => {
|
||||
export const QueryRowExplorer = ({
|
||||
startTime,
|
||||
endTime,
|
||||
onQuerySelect,
|
||||
selectedQueryId,
|
||||
}: QueryRowExplorerProps) => {
|
||||
const router = useRouter()
|
||||
const gridRef = useRef<DataGridHandle>(null)
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const { data: queries, isLoading, error } = useInsightsQueriesQuery(ref, startTime, endTime)
|
||||
|
||||
// Debug logging
|
||||
console.log('QueryRowExplorer Debug:', {
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
queries,
|
||||
const {
|
||||
data: queries,
|
||||
isLoading,
|
||||
error,
|
||||
queriesLength: queries?.length,
|
||||
} = useInsightsQueriesQuery(ref, startTime, endTime, {
|
||||
enabled: !!ref,
|
||||
})
|
||||
|
||||
// Debug logging (commented out to reduce console noise)
|
||||
// console.log('QueryRowExplorer Debug:', {
|
||||
// ref,
|
||||
// startTime,
|
||||
// endTime,
|
||||
// queries,
|
||||
// isLoading,
|
||||
// error,
|
||||
// queriesLength: queries?.length,
|
||||
// refUndefined: !ref,
|
||||
// })
|
||||
|
||||
const [sort, setSort] = useState<{ column: string; order: 'asc' | 'desc' } | undefined>({
|
||||
column: 'badness_score',
|
||||
order: 'desc',
|
||||
})
|
||||
const [selectedRow, setSelectedRow] = useState<number>()
|
||||
const [view, setView] = useState<'details' | 'indexes' | 'metrics'>('details')
|
||||
const [filterText, setFilterText] = useState<string>('')
|
||||
const [selectedColumns, setSelectedColumns] = useState<string[]>([])
|
||||
|
||||
@@ -156,50 +153,24 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
? sortedData.filter((query) => query.query.toLowerCase().includes(filterText.toLowerCase()))
|
||||
: sortedData
|
||||
|
||||
const selectedQuery = selectedRow !== undefined ? filteredData[selectedRow] : undefined
|
||||
const selectedQuery = useMemo(() => {
|
||||
return selectedRow !== undefined ? filteredData[selectedRow] : undefined
|
||||
}, [selectedRow, filteredData])
|
||||
|
||||
// Single query metrics hooks
|
||||
const { data: singleQueryLatency } = useInsightsSingleQueryLatency(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryCalls } = useInsightsSingleQueryCalls(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryRows } = useInsightsSingleQueryRows(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
const { data: singleQueryRowsWritten } = useInsightsSingleQueryRowsWritten(
|
||||
ref,
|
||||
startTime,
|
||||
endTime,
|
||||
selectedQuery?.query_id?.toString()
|
||||
)
|
||||
|
||||
// Debug logging for table data
|
||||
console.log('QueryRowExplorer Table Debug:', {
|
||||
queries,
|
||||
reportData,
|
||||
reportDataLength: reportData.length,
|
||||
sortedDataLength: sortedData.length,
|
||||
filteredDataLength: filteredData.length,
|
||||
filterText,
|
||||
isLoading,
|
||||
error,
|
||||
firstRow: filteredData[0],
|
||||
})
|
||||
// Update selectedRow when selectedQueryId changes from parent
|
||||
useEffect(() => {
|
||||
if (selectedQueryId !== undefined) {
|
||||
const index = filteredData.findIndex((query) => query.query_id === selectedQueryId)
|
||||
if (index !== -1) {
|
||||
setSelectedRow(index)
|
||||
gridRef.current?.scrollToCell({ idx: 0, rowIdx: index })
|
||||
}
|
||||
} else {
|
||||
setSelectedRow(undefined)
|
||||
}
|
||||
}, [selectedQueryId, filteredData])
|
||||
|
||||
// Reset selection when date range changes
|
||||
useEffect(() => {
|
||||
setSelectedRow(undefined)
|
||||
}, [startTime, endTime])
|
||||
@@ -264,346 +235,75 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
className="relative flex flex-grow bg-alternative min-h-0"
|
||||
autoSaveId="query-row-explorer-layout-v1"
|
||||
>
|
||||
<ResizablePanel defaultSize={1}>
|
||||
<DataGrid
|
||||
ref={gridRef}
|
||||
style={{ height: '100%' }}
|
||||
className={cn('flex-1 flex-grow h-full')}
|
||||
rowHeight={44}
|
||||
headerRowHeight={36}
|
||||
columns={columns}
|
||||
rows={filteredData}
|
||||
onRowsChange={(newRows) => {
|
||||
console.log('DataGrid rows changed:', newRows.length)
|
||||
}}
|
||||
rowClass={(_, idx) => {
|
||||
const isSelected = idx === selectedRow
|
||||
return [
|
||||
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`,
|
||||
`${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',
|
||||
].join(' ')
|
||||
}}
|
||||
renderers={{
|
||||
renderRow(idx, props) {
|
||||
return (
|
||||
<Row
|
||||
{...props}
|
||||
key={`qre-row-${props.rowIdx}`}
|
||||
onClick={() => {
|
||||
if (typeof idx === 'number' && idx >= 0) {
|
||||
<div className="flex flex-grow bg-alternative min-h-0">
|
||||
<DataGrid
|
||||
ref={gridRef}
|
||||
style={{ height: '100%' }}
|
||||
className={cn('flex-1 flex-grow h-full')}
|
||||
rowHeight={44}
|
||||
headerRowHeight={36}
|
||||
columns={columns}
|
||||
rows={filteredData}
|
||||
onRowsChange={(newRows) => {
|
||||
// console.log('DataGrid rows changed:', newRows.length)
|
||||
}}
|
||||
rowClass={(_, idx) => {
|
||||
const isSelected = idx === selectedRow
|
||||
return [
|
||||
`${isSelected ? 'bg-surface-300 dark:bg-surface-300' : 'bg-200'} cursor-pointer`,
|
||||
`${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',
|
||||
].join(' ')
|
||||
}}
|
||||
renderers={{
|
||||
renderRow(idx, props) {
|
||||
return (
|
||||
<Row
|
||||
{...props}
|
||||
key={`qre-row-${props.rowIdx}`}
|
||||
onClick={() => {
|
||||
if (typeof idx === 'number' && idx >= 0 && idx < filteredData.length) {
|
||||
const query = filteredData[idx]
|
||||
const isAlreadySelected = selectedQuery?.query_id === query.query_id
|
||||
|
||||
// If the query is already selected, deselect it
|
||||
if (isAlreadySelected) {
|
||||
setSelectedRow(undefined)
|
||||
onQuerySelect?.(undefined)
|
||||
} else {
|
||||
// Otherwise, select the new query
|
||||
setSelectedRow(idx)
|
||||
onQuerySelect?.(query)
|
||||
gridRef.current?.scrollToCell({ idx: 0, rowIdx: idx })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
noRowsFallback: isLoading ? (
|
||||
<div className="absolute top-14 px-6 w-full">
|
||||
<GenericSkeletonLoader />
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
noRowsFallback: isLoading ? (
|
||||
<div className="absolute top-14 px-6 w-full">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
|
||||
<TextSearch className="text-foreground-muted" strokeWidth={1} />
|
||||
<div className="text-center">
|
||||
<p className="text-foreground">
|
||||
{filterText ? 'No queries found' : 'No queries detected'}
|
||||
</p>
|
||||
<p className="text-foreground-light">
|
||||
{filterText
|
||||
? `No queries match "${filterText}" in the selected time range`
|
||||
: 'There are no queries that match the criteria in the selected time range'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute top-20 px-6 flex flex-col items-center justify-center w-full gap-y-2">
|
||||
<TextSearch className="text-foreground-muted" strokeWidth={1} />
|
||||
<div className="text-center">
|
||||
<p className="text-foreground">
|
||||
{filterText ? 'No queries found' : 'No queries detected'}
|
||||
</p>
|
||||
<p className="text-foreground-light">
|
||||
{filterText
|
||||
? `No queries match "${filterText}" in the selected time range`
|
||||
: 'There are no queries that match the criteria in the selected time range'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
{selectedRow !== undefined && selectedQuery && (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel
|
||||
defaultSize={30}
|
||||
maxSize={45}
|
||||
minSize={30}
|
||||
className="bg-studio border-t"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
className="absolute top-3 right-3 px-1"
|
||||
icon={<X />}
|
||||
onClick={() => setSelectedRow(undefined)}
|
||||
/>
|
||||
<Tabs_Shadcn_
|
||||
value={view}
|
||||
className="flex flex-col h-full"
|
||||
onValueChange={(value: any) => setView(value)}
|
||||
>
|
||||
<TabsList_Shadcn_ className="px-5 flex gap-x-4 min-h-[46px]">
|
||||
<TabsTrigger_Shadcn_
|
||||
value="details"
|
||||
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
|
||||
>
|
||||
Query details
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_
|
||||
value="metrics"
|
||||
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
|
||||
>
|
||||
Metrics
|
||||
</TabsTrigger_Shadcn_>
|
||||
{selectedQuery.index_statements && selectedQuery.index_statements.length > 0 && (
|
||||
<TabsTrigger_Shadcn_
|
||||
value="indexes"
|
||||
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
|
||||
>
|
||||
Indexes
|
||||
</TabsTrigger_Shadcn_>
|
||||
)}
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="details"
|
||||
className="mt-0 flex-grow min-h-0 overflow-y-auto"
|
||||
>
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Query</h4>
|
||||
<pre className="text-xs bg-surface-200 p-3 rounded overflow-x-auto">
|
||||
{selectedQuery.query}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Database</h5>
|
||||
<p className="text-sm">{selectedQuery.database}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Command Type
|
||||
</h5>
|
||||
<p className="text-sm">{selectedQuery.cmd_type_text}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Application
|
||||
</h5>
|
||||
<p className="text-sm">{selectedQuery.application_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Timestamp
|
||||
</h5>
|
||||
<p className="text-sm">
|
||||
{new Date(selectedQuery.timestamp).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Performance
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs">
|
||||
Total time: {selectedQuery.total_time?.toFixed(2)}ms
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Mean time: {selectedQuery.mean_exec_time?.toFixed(2)}ms
|
||||
</p>
|
||||
<p className="text-xs">Calls: {selectedQuery.calls?.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">Rows</h5>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs">
|
||||
Read: {selectedQuery.rows_read?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Insert: {selectedQuery.rows_insert?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Update: {selectedQuery.rows_update?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Delete: {selectedQuery.rows_delete?.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-1">
|
||||
Cache Performance
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs">
|
||||
Blocks read: {selectedQuery.shared_blks_read?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Blocks hit: {selectedQuery.shared_blks_hit?.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
Hit ratio:{' '}
|
||||
{selectedQuery.shared_blks_hit && selectedQuery.shared_blks_read
|
||||
? (
|
||||
(selectedQuery.shared_blks_hit /
|
||||
(selectedQuery.shared_blks_hit +
|
||||
selectedQuery.shared_blks_read)) *
|
||||
100
|
||||
).toFixed(1) + '%'
|
||||
: 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedQuery.error_count && selectedQuery.error_count > 0 && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-red-500 mb-1">Errors</h5>
|
||||
<p className="text-xs text-red-500">
|
||||
{selectedQuery.error_count} slow queries detected
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="metrics"
|
||||
className="mt-0 flex-grow min-h-0 overflow-y-auto"
|
||||
>
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Query Performance Over Time</h4>
|
||||
|
||||
{singleQueryLatency && singleQueryLatency.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">
|
||||
Latency
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryLatency.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toFixed(2)}ms</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{singleQueryCalls && singleQueryCalls.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">Calls</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryCalls.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{singleQueryRows && singleQueryRows.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">
|
||||
Rows Read
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryRows.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{singleQueryRowsWritten && singleQueryRowsWritten.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h5 className="text-xs font-medium text-foreground-light mb-2">
|
||||
Rows Written
|
||||
</h5>
|
||||
<div className="space-y-1">
|
||||
{singleQueryRowsWritten.slice(-5).map((point, index) => (
|
||||
<div key={index} className="flex justify-between text-xs">
|
||||
<span>{new Date(point.timestamp).toLocaleTimeString()}</span>
|
||||
<span>{point.value?.toLocaleString()}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!singleQueryLatency || singleQueryLatency.length === 0) &&
|
||||
(!singleQueryCalls || singleQueryCalls.length === 0) &&
|
||||
(!singleQueryRows || singleQueryRows.length === 0) &&
|
||||
(!singleQueryRowsWritten || singleQueryRowsWritten.length === 0) && (
|
||||
<p className="text-sm text-foreground-light">
|
||||
No metrics data available for this query
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_
|
||||
value="indexes"
|
||||
className="mt-0 flex-grow min-h-0 overflow-y-auto"
|
||||
>
|
||||
<div className="p-5 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-2">Index Recommendations</h4>
|
||||
{selectedQuery.index_statements &&
|
||||
selectedQuery.index_statements.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{selectedQuery.index_statements.map(
|
||||
(statement: string, index: number) => (
|
||||
<div key={index} className="bg-surface-200 p-3 rounded">
|
||||
<pre className="text-xs overflow-x-auto">{statement}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">
|
||||
No index recommendations available
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedQuery.errors && selectedQuery.errors.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-red-500 mb-2">
|
||||
Index Advisor Errors
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedQuery.errors.map((error: string, index: number) => (
|
||||
<div key={index} className="text-xs text-red-500 bg-red-50 p-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -102,10 +102,14 @@ export function useInsightsSingleQueryLatency(
|
||||
sql: getSingleQueryLatencySql(queryId, startTime, endTime),
|
||||
})
|
||||
|
||||
console.log('Chart data points:', result)
|
||||
// console.log('Chart data points:', result)
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch on window focus
|
||||
refetchOnMount: false, // Don't refetch on mount if data exists
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -124,17 +128,21 @@ export function useInsightsSingleQueryRows(
|
||||
if (!queryId) return []
|
||||
|
||||
const sql = getSingleQueryRowsSql(queryId, startTime, endTime)
|
||||
console.log('[useInsightsSingleQueryRows] Executing SQL:', sql)
|
||||
// console.log('[useInsightsSingleQueryRows] Executing SQL:', sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('[useInsightsSingleQueryRows] Result:', result)
|
||||
// console.log('[useInsightsSingleQueryRows] Result:', result)
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch on window focus
|
||||
refetchOnMount: false, // Don't refetch on mount if data exists
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -160,17 +168,21 @@ export function useInsightsSingleQueryRowsWritten(
|
||||
if (!queryId) return []
|
||||
|
||||
const sql = getSingleQueryRowsWrittenSql(queryId, startTime, endTime)
|
||||
console.log('[useInsightsSingleQueryRowsWritten] Executing SQL:', sql)
|
||||
// console.log('[useInsightsSingleQueryRowsWritten] Executing SQL:', sql)
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql,
|
||||
})
|
||||
|
||||
console.log('[useInsightsSingleQueryRowsWritten] Result:', result)
|
||||
// console.log('[useInsightsSingleQueryRowsWritten] Result:', result)
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch on window focus
|
||||
refetchOnMount: false, // Don't refetch on mount if data exists
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -196,6 +208,10 @@ export function useInsightsSingleQueryCalls(
|
||||
return result as InsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch on window focus
|
||||
refetchOnMount: false, // Don't refetch on mount if data exists
|
||||
...options,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user