mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 13:49:09 +03:00
added rows read section
This commit is contained in:
1 parent
767e6de695
commit
89ee1f5537
6 files changed
+550
-57
No files matched your search
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Formats a latency value in milliseconds to a human-readable string
|
||||
* with appropriate units (μs, ms, or s) based on the magnitude.
|
||||
*/
|
||||
export function formatLatency(value: number): string
|
||||
|
||||
/**
|
||||
* Formats large numbers with thousands separators for better readability
|
||||
* Used for metrics like row counts, query calls, and other statistical values.
|
||||
*/
|
||||
export function formatMetricValue(value: number): string
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Formats a latency value in milliseconds to a human-readable string
|
||||
* with appropriate units (μs, ms, or s) based on the magnitude.
|
||||
*
|
||||
* @param value - The latency value in milliseconds
|
||||
* @returns Formatted string with appropriate unit
|
||||
*/
|
||||
export function formatLatency(value: number): string {
|
||||
if (value === 0) return '0 ms'
|
||||
if (value < 1) return `${(value * 1000).toFixed(2)} μs`
|
||||
if (value < 1000) return `${value.toFixed(2)} ms`
|
||||
return `${(value / 1000).toFixed(2)} s`
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats large numbers with thousands separators for better readability
|
||||
* Used for metrics like row counts, query calls, and other statistical values
|
||||
* that can grow to large numbers.
|
||||
*
|
||||
* Examples:
|
||||
* - 1000 -> "1,000"
|
||||
* - 1000000 -> "1,000,000"
|
||||
*
|
||||
* @param value - The number to format
|
||||
* @returns Formatted string with thousands separators
|
||||
*/
|
||||
export function formatMetricValue(value: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
@@ -16,7 +16,9 @@ import { useParams } from 'common'
|
||||
import { Button } from 'ui'
|
||||
import { MetricType } from '../QueryInsights'
|
||||
import { useSingleQueryLatency } from 'data/query-insights/single-query-latency-query'
|
||||
import { useSingleQueryRows } from 'data/query-insights/single-query-latency-query'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { formatLatency, formatMetricValue } from '../QueryInsights.utils'
|
||||
|
||||
interface MetricsChartProps {
|
||||
data: QueryInsightsMetric[]
|
||||
@@ -24,7 +26,15 @@ interface MetricsChartProps {
|
||||
isLoading: boolean
|
||||
startTime: string
|
||||
endTime: string
|
||||
selectedQuery?: QueryInsightsQuery | null
|
||||
selectedQuery: QueryInsightsQuery | null
|
||||
}
|
||||
|
||||
interface ChartConfigType {
|
||||
[key: string]: {
|
||||
label: string
|
||||
color: string
|
||||
formatter: (value: number) => string
|
||||
}
|
||||
}
|
||||
|
||||
export function MetricsChart({
|
||||
@@ -36,12 +46,22 @@ export function MetricsChart({
|
||||
selectedQuery,
|
||||
}: MetricsChartProps) {
|
||||
const { ref } = useParams()
|
||||
const [visibleMetrics, setVisibleMetrics] = useState({
|
||||
const [visibleMetrics, setVisibleMetrics] = useState<{
|
||||
p50: boolean
|
||||
p95: boolean
|
||||
p99: boolean
|
||||
p99_9: boolean
|
||||
query_latency: boolean
|
||||
rows: boolean
|
||||
query_rows: boolean
|
||||
}>({
|
||||
p50: true,
|
||||
p95: true,
|
||||
p99: true,
|
||||
p99_9: true,
|
||||
query_latency: true,
|
||||
rows: true,
|
||||
query_rows: true,
|
||||
})
|
||||
|
||||
// Fetch single query latency data if a query is selected
|
||||
@@ -49,8 +69,17 @@ export function MetricsChart({
|
||||
data: queryLatencyData,
|
||||
isLoading: isLoadingQueryLatency,
|
||||
error: queryLatencyError,
|
||||
} = useSingleQueryLatency(ref, selectedQuery?.query_id, startTime, endTime, {
|
||||
enabled: !!selectedQuery,
|
||||
} = 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',
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
@@ -85,6 +114,178 @@ export function MetricsChart({
|
||||
)
|
||||
}
|
||||
|
||||
if (metric === 'rows_read') {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => ({
|
||||
timestamp: point.timestamp,
|
||||
rows: Number(point.value) ?? 0,
|
||||
query_rows: 0,
|
||||
}))
|
||||
|
||||
// Sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
|
||||
// If we have query rows data, add it to the chart
|
||||
if (selectedQuery && queryRowsData && queryRowsData.length > 0) {
|
||||
// Create a map of timestamps to query rows values
|
||||
const queryRowsMap = new Map<string, number>()
|
||||
queryRowsData.forEach((point) => {
|
||||
queryRowsMap.set(point.timestamp, Number(point.value) || 0)
|
||||
})
|
||||
|
||||
// Add query rows data to each chart data point
|
||||
chartData.forEach((point) => {
|
||||
if (queryRowsMap.has(point.timestamp)) {
|
||||
point.query_rows = queryRowsMap.get(point.timestamp)!
|
||||
}
|
||||
})
|
||||
|
||||
// Add any missing query rows points
|
||||
queryRowsData.forEach((point) => {
|
||||
if (!chartData.find((d) => d.timestamp === point.timestamp)) {
|
||||
chartData.push({
|
||||
timestamp: point.timestamp,
|
||||
rows: 0,
|
||||
query_rows: Number(point.value) || 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Re-sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
}
|
||||
|
||||
const config: ChartConfigType = {
|
||||
rows: {
|
||||
label: 'Rows Read',
|
||||
color: '#3ECF8E',
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
query_rows: {
|
||||
label: 'Query Rows',
|
||||
color: '#9BA6B0',
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[320px] flex flex-col">
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex gap-2 px-5 mb-4">
|
||||
<AnimatePresence>
|
||||
{Object.entries(config).map(([key, value]) => {
|
||||
// Only show query rows button if we have a selected query
|
||||
if (key === 'query_rows' && !selectedQuery) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metricValue = Number(
|
||||
chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]]
|
||||
)
|
||||
const formattedValue = !isNaN(metricValue) ? value.formatter(metricValue) : '0'
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={key}
|
||||
initial={{ opacity: 0, scale: 0.95, x: -8 }}
|
||||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, x: -8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={cn(
|
||||
'text-xs px-2 py-1 rounded-full transition-colors inline-flex items-center gap-1.5 border',
|
||||
visibleMetrics[key as keyof typeof visibleMetrics]
|
||||
? 'bg-surface-300'
|
||||
: 'hover:bg-surface-100 border-transparent'
|
||||
)}
|
||||
onClick={() =>
|
||||
setVisibleMetrics((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key as keyof typeof visibleMetrics],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full',
|
||||
!visibleMetrics[key as keyof typeof visibleMetrics] && 'opacity-50'
|
||||
)}
|
||||
style={{ backgroundColor: value.color }}
|
||||
/>
|
||||
<span>
|
||||
{value.label}: {formattedValue}
|
||||
</span>
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</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.color} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={value.color} stopOpacity={0.05} />
|
||||
</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={['dataMin', 'dataMax']}
|
||||
allowDataOverflow={false}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" hideLabel />}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{visibleMetrics.rows && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="rows"
|
||||
fill={`url(#gradient-rows)`}
|
||||
stroke={config.rows.color}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
fillOpacity={selectedQuery ? 0.1 : 0.3}
|
||||
strokeOpacity={selectedQuery ? 0.3 : 1}
|
||||
/>
|
||||
)}
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (metric === 'query_latency') {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => {
|
||||
@@ -142,25 +343,30 @@ export function MetricsChart({
|
||||
const config = {
|
||||
p50: {
|
||||
label: 'p50',
|
||||
color: 'hsl(var(--brand-default))',
|
||||
color: '#4B5563',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p95: {
|
||||
label: 'p95',
|
||||
color: 'hsl(var(--foreground-default))',
|
||||
color: '#6B7280',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p99: {
|
||||
label: 'p99',
|
||||
color: 'hsl(var(--warning-default))',
|
||||
color: '#9CA3AF',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p99_9: {
|
||||
label: 'p99.9',
|
||||
color: 'hsl(var(--destructive-default))',
|
||||
color: '#D1D5DB',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
query_latency: {
|
||||
label: 'Query Latency',
|
||||
color: 'hsl(var(--foreground-default))',
|
||||
label: 'Query latency',
|
||||
color: '#3B82F6',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
} satisfies ChartConfigType
|
||||
|
||||
return (
|
||||
<div className="h-[320px] flex flex-col">
|
||||
@@ -270,7 +476,13 @@ export function MetricsChart({
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => dayjs(value).format('HH:mm')}
|
||||
/>
|
||||
<YAxis />
|
||||
<YAxis
|
||||
tickFormatter={formatMetricValue}
|
||||
width={80}
|
||||
tickCount={5}
|
||||
scale="linear"
|
||||
domain={['auto', 'auto']}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" hideLabel />}
|
||||
@@ -284,7 +496,7 @@ export function MetricsChart({
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p50.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
@@ -297,7 +509,7 @@ export function MetricsChart({
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p95.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
@@ -310,7 +522,7 @@ export function MetricsChart({
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p99.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
@@ -323,7 +535,7 @@ export function MetricsChart({
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p99_9.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={2}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
@@ -357,25 +569,160 @@ export function MetricsChart({
|
||||
error_count: 0,
|
||||
}))
|
||||
|
||||
const toggleButtons = () => {
|
||||
switch (metric as MetricType) {
|
||||
case 'query_latency':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p50 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p50: !prev.p50 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#4B5563]" />
|
||||
<span className="text-xs">p50: {formatLatency(data[data.length - 1]?.p50 ?? 0)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p95 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p95: !prev.p95 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#6B7280]" />
|
||||
<span className="text-xs">p95: {formatLatency(data[data.length - 1]?.p95 ?? 0)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p99 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p99: !prev.p99 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#9CA3AF]" />
|
||||
<span className="text-xs">p99: {formatLatency(data[data.length - 1]?.p99 ?? 0)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p99_9 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p99_9: !prev.p99_9 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#D1D5DB]" />
|
||||
<span className="text-xs">
|
||||
p99.9: {formatLatency(data[data.length - 1]?.p99_9 ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
{selectedQuery && (
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.query_latency ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() =>
|
||||
setVisibleMetrics((prev) => ({ ...prev, query_latency: !prev.query_latency }))
|
||||
}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#3B82F6]" />
|
||||
<span className="text-xs">
|
||||
Query latency:{' '}
|
||||
{formatLatency(queryLatencyData?.[queryLatencyData.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
case 'rows_read':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.rows ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, rows: !prev.rows }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#10B981]" />
|
||||
<span className="text-xs">
|
||||
Rows: {formatMetricValue(data[data.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
{selectedQuery && (
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.query_rows ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() =>
|
||||
setVisibleMetrics((prev) => ({ ...prev, query_rows: !prev.query_rows }))
|
||||
}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#3B82F6]" />
|
||||
<span className="text-xs">
|
||||
Query rows:{' '}
|
||||
{formatMetricValue(queryRowsData?.[queryRowsData.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
case 'rows_written':
|
||||
case 'queries_per_second':
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.rows ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, rows: !prev.rows }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#10B981]" />
|
||||
<span className="text-xs">
|
||||
Value: {formatMetricValue(data[data.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-[320px]">
|
||||
<motion.div
|
||||
className="h-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<LogsBarChart
|
||||
data={chartData}
|
||||
DateTimeFormat="HH:mm"
|
||||
EmptyState={
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-sm text-foreground-light">No data available</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 px-4">{toggleButtons()}</div>
|
||||
<div className="h-[320px]">
|
||||
<motion.div
|
||||
className="h-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<LogsBarChart
|
||||
data={chartData}
|
||||
DateTimeFormat="HH:mm"
|
||||
EmptyState={
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-sm text-foreground-light">No data available</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -123,13 +123,71 @@ export const QueryList = ({ queries, isLoading, onQuerySelect, selectedQuery }:
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'rows',
|
||||
name: 'Rows',
|
||||
key: 'rows_read',
|
||||
name: 'Rows Read',
|
||||
resizable: true,
|
||||
minWidth: 100,
|
||||
headerCellClass: '',
|
||||
renderCell: (props) => (
|
||||
<div className="font-mono text-xs text-right">{props.row.rows.toLocaleString()}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'font-mono text-xs text-right',
|
||||
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{((props.row.rows_read ?? 0) > 0 ? props.row.rows_read : 0).toLocaleString()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'rows_insert',
|
||||
name: 'Rows Insert',
|
||||
resizable: true,
|
||||
minWidth: 100,
|
||||
headerCellClass: '',
|
||||
renderCell: (props) => (
|
||||
<div
|
||||
className={cn(
|
||||
'font-mono text-xs text-right',
|
||||
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{((props.row.rows_insert ?? 0) > 0 ? props.row.rows_insert : 0).toLocaleString()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'rows_update',
|
||||
name: 'Rows Update',
|
||||
resizable: true,
|
||||
minWidth: 100,
|
||||
headerCellClass: '',
|
||||
renderCell: (props) => (
|
||||
<div
|
||||
className={cn(
|
||||
'font-mono text-xs text-right',
|
||||
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{((props.row.rows_update ?? 0) > 0 ? props.row.rows_update : 0).toLocaleString()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'rows_delete',
|
||||
name: 'Rows Delete',
|
||||
resizable: true,
|
||||
minWidth: 100,
|
||||
headerCellClass: '',
|
||||
renderCell: (props) => (
|
||||
<div
|
||||
className={cn(
|
||||
'font-mono text-xs text-right',
|
||||
selectedQuery && selectedQuery.query_id !== props.row.query_id && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
{((props.row.rows_delete ?? 0) > 0 ? props.row.rows_delete : 0).toLocaleString()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
// {
|
||||
|
||||
@@ -5,8 +5,9 @@ import { queryInsightsKeys } from './keys'
|
||||
export type QueryInsightsMetric = {
|
||||
timestamp: string
|
||||
value?: number
|
||||
database: string
|
||||
rows?: number
|
||||
cmd_type_text?: string
|
||||
database: string
|
||||
shared_blks_read?: number
|
||||
shared_blks_hit?: number
|
||||
mean_exec_time?: number
|
||||
@@ -22,7 +23,10 @@ export type QueryInsightsQuery = {
|
||||
query: string
|
||||
total_time: number
|
||||
calls: number
|
||||
rows: number
|
||||
rows_read: number
|
||||
rows_insert: number
|
||||
rows_update: number
|
||||
rows_delete: number
|
||||
shared_blks_read: number
|
||||
shared_blks_hit: number
|
||||
mean_exec_time: number
|
||||
@@ -54,28 +58,29 @@ const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) / 300 as value, -- Convert to per-second rate (5 mins = 300 seconds)
|
||||
SUM(rows) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
case 'rows_written':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) / 300 as value, -- Convert to per-second rate (5 mins = 300 seconds)
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
AND bucket_done = true
|
||||
AND cmd_type = 1
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
// case 'rows_written':
|
||||
// return /* SQL */ `
|
||||
// SELECT
|
||||
// bucket_start_time as timestamp,
|
||||
// SUM(rows) / 300 as value, -- Convert to per-second rate (5 mins = 300 seconds)
|
||||
// datname as database
|
||||
// FROM pg_stat_monitor
|
||||
// WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
// AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
// AND bucket_done = true -- Only include completed buckets
|
||||
// GROUP BY bucket_start_time, datname
|
||||
// ORDER BY timestamp ASC
|
||||
// `
|
||||
case 'query_latency':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
@@ -127,7 +132,10 @@ const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
|
||||
COALESCE(MAX(top_query), MAX(query)) as query,
|
||||
SUM(total_exec_time) as total_time,
|
||||
SUM(calls) as calls,
|
||||
SUM(rows) as rows,
|
||||
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) as rows_read,
|
||||
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) as rows_insert,
|
||||
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) as rows_update,
|
||||
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) as rows_delete,
|
||||
SUM(shared_blks_read) as shared_blks_read,
|
||||
SUM(shared_blks_hit) as shared_blks_hit,
|
||||
SUM(total_exec_time) / NULLIF(SUM(calls), 0) as mean_exec_time,
|
||||
@@ -137,9 +145,9 @@ const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'
|
||||
AND bucket_start_time <= '${endTime}'
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
AND bucket_done = true
|
||||
GROUP BY queryid
|
||||
ORDER BY SUM(calls) DESC
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 100
|
||||
`
|
||||
|
||||
|
||||
@@ -30,6 +30,21 @@ const getSingleQueryLatencySql = (
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
const getSingleQueryRowsSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(rows) as value,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true
|
||||
AND queryid = ${queryId}::bigint
|
||||
AND cmd_type = 1
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
export function useSingleQueryLatency(
|
||||
projectRef: string | undefined,
|
||||
queryId: string | undefined,
|
||||
@@ -55,3 +70,28 @@ export function useSingleQueryLatency(
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSingleQueryRows(
|
||||
projectRef: string | undefined,
|
||||
queryId: string | undefined,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
options?: UseQueryOptions<QueryInsightsMetric[]>
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: [...singleQueryLatencyKeys.base, 'rows', projectRef, queryId, startTime, endTime],
|
||||
queryFn: async () => {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
if (!queryId) return []
|
||||
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
sql: getSingleQueryRowsSql(queryId, startTime, endTime),
|
||||
})
|
||||
|
||||
return result as QueryInsightsMetric[]
|
||||
},
|
||||
enabled: !!queryId && !!projectRef,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user