mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Improvements to Reports > Database (#35477)
* improve legend items hover * show yAxis label * open range in logs explorer * add provisioned disk size to Memory Usage chart * temp new charts * Network Traffic chart * sync charts marker * update api types * fix layout * add max memory * add db size * chore: remove unused properties * disk_io_usage * add max db and pooler connections * chart header tooltip * clean charts * add cpu max * refactor charts and change max color * reference line on Database Size chart * reference line text * reference lines on Database Size chart * fix aggregation * legend spacing * reference line * add docsUrl
This commit is contained in:
1 parent
159455c34e
commit
fda426b9ad
12 files changed
+731
-279
No files matched your search
@@ -59,6 +59,7 @@ const ChartHandler = ({
|
||||
isLoading,
|
||||
format,
|
||||
highlightedValue,
|
||||
...otherProps
|
||||
}: PropsWithChildren<ChartHandlerProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
@@ -168,7 +169,6 @@ const ChartHandler = ({
|
||||
</div>
|
||||
{chartStyle === 'bar' ? (
|
||||
<BarChart
|
||||
YAxisProps={{ width: 1 }}
|
||||
data={(chartData?.data ?? []) as any}
|
||||
format={format || chartData?.format}
|
||||
xAxisKey={'period_start'}
|
||||
@@ -176,6 +176,7 @@ const ChartHandler = ({
|
||||
highlightedValue={_highlightedValue}
|
||||
title={label}
|
||||
customDateFormat={customDateFormat}
|
||||
{...otherProps}
|
||||
/>
|
||||
) : (
|
||||
<AreaChart
|
||||
@@ -186,6 +187,7 @@ const ChartHandler = ({
|
||||
highlightedValue={_highlightedValue}
|
||||
title={label}
|
||||
customDateFormat={customDateFormat}
|
||||
{...otherProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { Activity, BarChartIcon, GitCommitHorizontalIcon } from 'lucide-react'
|
||||
import { Activity, BarChartIcon, GitCommitHorizontalIcon, InfoIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
|
||||
export interface ChartHeaderProps {
|
||||
title?: string
|
||||
@@ -9,17 +10,20 @@ export interface ChartHeaderProps {
|
||||
displayDateInUtc?: boolean
|
||||
highlightedLabel?: number | string | any | null
|
||||
highlightedValue?: number | string | any | null
|
||||
hideHighlightedValue?: boolean
|
||||
hideChartType?: boolean
|
||||
chartStyle?: string
|
||||
onChartStyleChange?: (style: string) => void
|
||||
showMaxValue?: boolean
|
||||
setShowMaxValue?: (value: boolean) => void
|
||||
docsUrl?: string
|
||||
}
|
||||
|
||||
const ChartHeader = ({
|
||||
format,
|
||||
highlightedValue,
|
||||
highlightedLabel,
|
||||
hideHighlightedValue = false,
|
||||
title,
|
||||
minimalHeader = false,
|
||||
hideChartType = false,
|
||||
@@ -27,11 +31,23 @@ const ChartHeader = ({
|
||||
onChartStyleChange,
|
||||
showMaxValue = false,
|
||||
setShowMaxValue,
|
||||
docsUrl,
|
||||
}: ChartHeaderProps) => {
|
||||
const chartTitle = (
|
||||
<h3 className={'text-foreground-lighter ' + (minimalHeader ? 'text-xs' : 'text-sm')}>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<h3 className={'text-foreground-lighter ' + (minimalHeader ? 'text-xs' : 'text-sm')}>
|
||||
{title}
|
||||
</h3>
|
||||
{docsUrl && (
|
||||
<Link
|
||||
href={docsUrl}
|
||||
target="_blank"
|
||||
className="text-foreground-lighter hover:text-foreground-light"
|
||||
>
|
||||
<InfoIcon className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const highlighted = (
|
||||
@@ -52,7 +68,7 @@ const ChartHeader = ({
|
||||
<div className="flex flex-row items-center gap-x-4" style={{ minHeight: '1.8rem' }}>
|
||||
{title && chartTitle}
|
||||
<div className="flex flex-row items-baseline gap-x-2">
|
||||
{highlightedValue !== undefined && highlighted}
|
||||
{highlightedValue !== undefined && !hideHighlightedValue && highlighted}
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +79,7 @@ const ChartHeader = ({
|
||||
<div className="flex-grow flex justify-between items-start min-h-16">
|
||||
<div className="flex flex-col">
|
||||
{title && chartTitle}
|
||||
{highlightedValue !== undefined && highlighted}
|
||||
{highlightedValue !== undefined && !hideHighlightedValue && highlighted}
|
||||
{label}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -77,7 +93,7 @@ const ChartHeader = ({
|
||||
onClick={() => onChartStyleChange(chartStyle === 'bar' ? 'line' : 'bar')}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" align="center">
|
||||
<TooltipContent side="top" align="center">
|
||||
View as {chartStyle === 'bar' ? 'line chart' : 'bar chart'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -90,13 +106,13 @@ const ChartHeader = ({
|
||||
className="px-1.5"
|
||||
icon={
|
||||
<GitCommitHorizontalIcon
|
||||
className={showMaxValue ? 'text-foreground' : 'text-foreground-lighter'}
|
||||
className={showMaxValue ? 'text-foreground-light' : 'text-foreground-lighter'}
|
||||
/>
|
||||
}
|
||||
onClick={() => setShowMaxValue(!showMaxValue)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" align="center">
|
||||
<TooltipContent side="top" align="center">
|
||||
{showMaxValue ? 'Hide' : 'Show'} limit
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowRight, SearchIcon } from 'lucide-react'
|
||||
import { ArrowRight, LogsIcon, SearchIcon } from 'lucide-react'
|
||||
import {
|
||||
cn,
|
||||
DropdownMenu,
|
||||
@@ -20,6 +21,8 @@ const ChartHighlightActions = ({
|
||||
chartHighlight?: ChartHighlight
|
||||
updateDateRange: UpdateDateRange
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
const { left: selectedRangeStart, right: selectedRangeEnd, clearHighlight } = chartHighlight ?? {}
|
||||
const [isOpen, setIsOpen] = useState(!!chartHighlight?.popoverPosition)
|
||||
|
||||
@@ -34,6 +37,12 @@ const ChartHighlightActions = ({
|
||||
clearHighlight && clearHighlight()
|
||||
}
|
||||
|
||||
const handleOpenLogsExplorer = () => {
|
||||
const rangeQueryParams = `?its=${selectedRangeStart}&ite=${selectedRangeEnd}`
|
||||
router.push(`/project/${ref}/logs/postgres-logs${rangeQueryParams}`)
|
||||
clearHighlight && clearHighlight()
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
@@ -62,9 +71,15 @@ const ChartHighlightActions = ({
|
||||
>
|
||||
<SearchIcon className="text-foreground-lighter" size={12} />
|
||||
<span className="flex-grow text-left text-foreground-light">Zoom in</span>
|
||||
{disableZoomIn && (
|
||||
<span className="text-foreground-muted text-xs">10 minutes min.</span>
|
||||
)}
|
||||
{disableZoomIn && <span className="text-xs">Min. 10 minutes</span>}
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className={cn('group', disableZoomIn && '!bg-transparent')}>
|
||||
<button onClick={handleOpenLogsExplorer} className="w-full flex items-center gap-1.5">
|
||||
<LogsIcon className="text-foreground-lighter" size={12} />
|
||||
<span className="flex-grow text-left text-foreground-light">
|
||||
Open range in Logs Explorer
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -9,6 +9,8 @@ export const CHART_COLORS = {
|
||||
GREEN_2: 'hsl(var(--brand-500))',
|
||||
RED_1: 'hsl(var(--destructive-default))',
|
||||
RED_2: 'hsl(var(--destructive-500))',
|
||||
REFERENCE_LINE: 'hsl(var(--foreground-muted))',
|
||||
REFERENCE_LINE_TEXT: 'hsl(var(--foreground-muted))',
|
||||
}
|
||||
|
||||
const LIGHT_STACKED_CHART_COLORS = [
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
Line,
|
||||
ComposedChart as RechartComposedChart,
|
||||
ReferenceArea,
|
||||
ReferenceLine,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Label,
|
||||
} from 'recharts'
|
||||
import { CategoricalChartState } from 'recharts/types/chart/types'
|
||||
import { cn } from 'ui'
|
||||
@@ -26,17 +28,13 @@ import {
|
||||
} from './Charts.constants'
|
||||
import { CommonChartProps, Datum } from './Charts.types'
|
||||
import { numberFormatter, useChartSize } from './Charts.utils'
|
||||
import {
|
||||
calculateTotalChartAggregate,
|
||||
CustomLabel,
|
||||
CustomTooltip,
|
||||
formatBytes,
|
||||
} from './ComposedChart.utils'
|
||||
import { calculateTotalChartAggregate, CustomLabel, CustomTooltip } from './ComposedChart.utils'
|
||||
import { MultiAttribute } from './ComposedChartHandler'
|
||||
import NoDataPlaceholder from './NoDataPlaceholder'
|
||||
import { ChartHighlight } from './useChartHighlight'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
export interface BarChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
export interface ComposedChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
attributes: MultiAttribute[]
|
||||
yAxisKey: string
|
||||
xAxisKey: string
|
||||
@@ -56,6 +54,10 @@ export interface BarChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
chartStyle?: string
|
||||
onChartStyleChange?: (style: string) => void
|
||||
updateDateRange: any
|
||||
hideYAxis?: boolean
|
||||
hideHighlightedValue?: boolean
|
||||
syncId?: string
|
||||
docsUrl?: string
|
||||
}
|
||||
|
||||
export default function ComposedChart({
|
||||
@@ -88,16 +90,22 @@ export default function ComposedChart({
|
||||
chartStyle,
|
||||
onChartStyleChange,
|
||||
updateDateRange,
|
||||
}: BarChartProps) {
|
||||
hideYAxis,
|
||||
hideHighlightedValue,
|
||||
syncId,
|
||||
docsUrl,
|
||||
}: ComposedChartProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [_activePayload, setActivePayload] = useState<any>(null)
|
||||
const [_showMaxValue, setShowMaxValue] = useState(showMaxValue)
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
const [hoveredLabel, setHoveredLabel] = useState<string | null>(null)
|
||||
const [isActiveHoveredChart, setIsActiveHoveredChart] = useState(false)
|
||||
const isDarkMode = resolvedTheme?.includes('dark')
|
||||
|
||||
// Update chart colors when theme changes
|
||||
useEffect(() => {
|
||||
updateStackedChartColors(resolvedTheme?.includes('dark') ?? false)
|
||||
updateStackedChartColors(isDarkMode ?? false)
|
||||
}, [resolvedTheme])
|
||||
|
||||
const { Container } = useChartSize(size)
|
||||
@@ -134,7 +142,7 @@ export default function ComposedChart({
|
||||
const maxAttribute = attributes.find((a) => a.isMaxValue)
|
||||
const maxAttributeData = {
|
||||
name: maxAttribute?.attribute,
|
||||
color: '#3ECF8E',
|
||||
color: CHART_COLORS.REFERENCE_LINE,
|
||||
}
|
||||
|
||||
const lastDataPoint = !!data[data.length - 1]
|
||||
@@ -145,6 +153,7 @@ export default function ComposedChart({
|
||||
}))
|
||||
.filter((entry) => entry.dataKey !== 'timestamp')
|
||||
: undefined
|
||||
const referenceLines = attributes.filter((attribute) => attribute?.provider === 'reference-line')
|
||||
|
||||
const resolvedHighlightedLabel = getHeaderLabel()
|
||||
const resolvedHighlightedValue =
|
||||
@@ -152,13 +161,13 @@ export default function ComposedChart({
|
||||
? showTotal
|
||||
? calculateTotalChartAggregate(
|
||||
_activePayload,
|
||||
maxAttribute?.attribute ? [maxAttribute?.attribute] : []
|
||||
maxAttribute?.attribute ? referenceLines.map((a) => a.attribute) : []
|
||||
)
|
||||
: data[focusDataIndex]?.[yAxisKey]
|
||||
: showTotal && lastDataPoint
|
||||
? calculateTotalChartAggregate(
|
||||
lastDataPoint,
|
||||
maxAttribute?.attribute ? [maxAttribute?.attribute] : []
|
||||
maxAttribute?.attribute ? referenceLines.map((a) => a.attribute) : []
|
||||
)
|
||||
: highlightedValue
|
||||
|
||||
@@ -175,13 +184,25 @@ export default function ComposedChart({
|
||||
value: value,
|
||||
color: STACKED_CHART_COLORS[index - (1 % STACKED_CHART_COLORS.length)],
|
||||
}))
|
||||
.filter((att) => att.name !== 'timestamp' && att.name !== maxAttribute?.attribute)
|
||||
.filter(
|
||||
(att) =>
|
||||
att.name !== 'timestamp' &&
|
||||
att.name !== maxAttribute?.attribute &&
|
||||
!referenceLines.map((a) => a.attribute).includes(att.name)
|
||||
)
|
||||
: []
|
||||
|
||||
const stackedAttributes = chartData.filter((att) => !att.name.includes('max'))
|
||||
const isPercentage = format === '%'
|
||||
const isRamChart = chartData?.some((att: any) => att.name.toLowerCase().includes('ram_'))
|
||||
|
||||
const isDiskSpaceChart = chartData?.some((att: any) =>
|
||||
att.name.toLowerCase().includes('disk_space_')
|
||||
)
|
||||
const isDBSizeChart = chartData?.some((att: any) =>
|
||||
att.name.toLowerCase().includes('pg_database_size')
|
||||
)
|
||||
const isNetworkChart = chartData?.some((att: any) => att.name.toLowerCase().includes('network_'))
|
||||
const shouldFormatBytes = isRamChart || isDiskSpaceChart || isDBSizeChart || isNetworkChart
|
||||
//*
|
||||
// Set the y-axis domain
|
||||
// to the highest value in the chart data for percentage charts
|
||||
@@ -213,8 +234,11 @@ export default function ComposedChart({
|
||||
customDateFormat={customDateFormat}
|
||||
highlightedValue={
|
||||
typeof resolvedHighlightedValue === 'number'
|
||||
? isRamChart
|
||||
? formatBytes(resolvedHighlightedValue, valuePrecision)
|
||||
? shouldFormatBytes
|
||||
? formatBytes(
|
||||
isNetworkChart ? Math.abs(resolvedHighlightedValue) : resolvedHighlightedValue,
|
||||
valuePrecision
|
||||
)
|
||||
: numberFormatter(resolvedHighlightedValue, valuePrecision)
|
||||
: resolvedHighlightedValue
|
||||
}
|
||||
@@ -225,11 +249,15 @@ export default function ComposedChart({
|
||||
onChartStyleChange={onChartStyleChange}
|
||||
showMaxValue={_showMaxValue}
|
||||
setShowMaxValue={maxAttribute ? setShowMaxValue : undefined}
|
||||
hideHighlightedValue={hideHighlightedValue}
|
||||
docsUrl={docsUrl}
|
||||
/>
|
||||
<Container className="relative z-10">
|
||||
<RechartComposedChart
|
||||
data={data}
|
||||
syncId={syncId}
|
||||
onMouseMove={(e: any) => {
|
||||
setIsActiveHoveredChart(true)
|
||||
if (e.activeTooltipIndex !== focusDataIndex) {
|
||||
setFocusDataIndex(e.activeTooltipIndex)
|
||||
setActivePayload(e.activePayload)
|
||||
@@ -249,6 +277,7 @@ export default function ComposedChart({
|
||||
}}
|
||||
onMouseUp={chartHighlight?.handleMouseUp}
|
||||
onMouseLeave={(e) => {
|
||||
setIsActiveHoveredChart(false)
|
||||
setFocusDataIndex(null)
|
||||
setActivePayload(null)
|
||||
}}
|
||||
@@ -258,7 +287,14 @@ export default function ComposedChart({
|
||||
}}
|
||||
>
|
||||
{showGrid && <CartesianGrid stroke={CHART_COLORS.AXIS} />}
|
||||
<YAxis {..._YAxisProps} hide domain={isPercentage ? yDomain : undefined} key={yAxisKey} />
|
||||
<YAxis
|
||||
{..._YAxisProps}
|
||||
hide={hideYAxis}
|
||||
axisLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
tickLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
domain={isPercentage && !showMaxValue ? yDomain : ['auto', 'auto']}
|
||||
key={yAxisKey}
|
||||
/>
|
||||
<XAxis
|
||||
{..._XAxisProps}
|
||||
axisLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
@@ -277,6 +313,7 @@ export default function ComposedChart({
|
||||
attributes={attributes}
|
||||
valuePrecision={valuePrecision}
|
||||
showTotal={showTotal}
|
||||
isActiveHoveredChart={isActiveHoveredChart}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
@@ -286,22 +323,23 @@ export default function ComposedChart({
|
||||
<Bar
|
||||
key={attribute.name}
|
||||
dataKey={attribute.name}
|
||||
stackId="1"
|
||||
stackId={attributes?.find((a) => a.attribute === attribute?.name)?.stackId ?? '1'}
|
||||
fill={attribute.color}
|
||||
fillOpacity={hoveredLabel && hoveredLabel !== attribute.name ? 0.25 : 1}
|
||||
fillOpacity={hoveredLabel && hoveredLabel !== attribute?.name ? 0.25 : 1}
|
||||
radius={0.75}
|
||||
opacity={hoveredLabel && hoveredLabel !== attribute.name ? 0.5 : 1}
|
||||
opacity={hoveredLabel && hoveredLabel !== attribute?.name ? 0.5 : 1}
|
||||
name={
|
||||
attributes?.find((a) => a.attribute === attribute.name)?.label || attribute.name
|
||||
attributes?.find((a) => a.attribute === attribute?.name)?.label ||
|
||||
attribute?.name
|
||||
}
|
||||
/>
|
||||
))
|
||||
: stackedAttributes.map((attribute) => (
|
||||
: stackedAttributes.map((attribute, i) => (
|
||||
<Area
|
||||
key={attribute.name}
|
||||
type="step"
|
||||
dataKey={attribute.name}
|
||||
stackId="1"
|
||||
stackId={attributes?.find((a) => a.attribute === attribute.name)?.stackId ?? '1'}
|
||||
fill={attribute.color}
|
||||
strokeOpacity={hoveredLabel && hoveredLabel !== attribute.name ? 0.4 : 1}
|
||||
stroke={attribute.color}
|
||||
@@ -317,6 +355,8 @@ export default function ComposedChart({
|
||||
name={
|
||||
attributes?.find((a) => a.attribute === attribute.name)?.label || attribute.name
|
||||
}
|
||||
// Show dot for the first attribute when a point is focused
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
{/* Max value, if available */}
|
||||
@@ -325,13 +365,33 @@ export default function ComposedChart({
|
||||
key={maxAttribute.attribute}
|
||||
type="stepAfter"
|
||||
dataKey={maxAttribute.attribute}
|
||||
stroke="#3ECF8E"
|
||||
stroke={CHART_COLORS.REFERENCE_LINE}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="3 3"
|
||||
strokeDasharray={maxAttribute.strokeDasharray ?? '3 3'}
|
||||
dot={false}
|
||||
name={maxAttribute.label}
|
||||
/>
|
||||
)}
|
||||
{referenceLines
|
||||
.filter((line) => line.isReferenceLine)
|
||||
.map((line) => (
|
||||
<ReferenceLine
|
||||
key={line.attribute}
|
||||
y={line.value}
|
||||
strokeWidth={1}
|
||||
{...line}
|
||||
strokeDasharray={line.strokeDasharray ?? '3 3'}
|
||||
label={undefined}
|
||||
>
|
||||
<Label
|
||||
value={line.label}
|
||||
position="insideTopRight"
|
||||
fill={CHART_COLORS.REFERENCE_LINE_TEXT}
|
||||
className="text-xs"
|
||||
style={{ fill: CHART_COLORS.REFERENCE_LINE_TEXT }}
|
||||
/>
|
||||
</ReferenceLine>
|
||||
))}
|
||||
{/* Selection highlight */}
|
||||
{showHighlightActions && (
|
||||
<ReferenceArea
|
||||
@@ -347,7 +407,10 @@ export default function ComposedChart({
|
||||
<ChartHighlightActions chartHighlight={chartHighlight} updateDateRange={updateDateRange} />
|
||||
</Container>
|
||||
{data && (
|
||||
<div className="text-foreground-lighter -mt-9 flex items-center justify-between text-xs">
|
||||
<div
|
||||
className="text-foreground-lighter -mt-9 flex items-center justify-between text-xs"
|
||||
style={{ marginLeft: YAxisProps?.width }}
|
||||
>
|
||||
<span>
|
||||
{xAxisIsDate ? day(data[0]?.timestamp).format(customDateFormat) : data[0]?.timestamp}
|
||||
</span>
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useState } from 'react'
|
||||
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { DateTimeFormats } from './Charts.constants'
|
||||
import { CHART_COLORS, DateTimeFormats } from './Charts.constants'
|
||||
import { numberFormatter } from './Charts.utils'
|
||||
import { MultiAttribute } from './ComposedChartHandler'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
interface CustomIconProps {
|
||||
color: string
|
||||
@@ -17,9 +18,17 @@ const CustomIcon = ({ color }: CustomIconProps) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const MaxConnectionsIcon = () => (
|
||||
const MaxConnectionsIcon = ({ color }: { color?: string }) => (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="2" y1="6" x2="12" y2="6" stroke="#3ECF8E" strokeWidth="2" strokeDasharray="2 2" />
|
||||
<line
|
||||
x1="2"
|
||||
y1="6"
|
||||
x2="12"
|
||||
y2="6"
|
||||
stroke={color ?? CHART_COLORS.REFERENCE_LINE}
|
||||
strokeWidth="2"
|
||||
strokeDasharray="2 2"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -32,14 +41,7 @@ interface TooltipProps {
|
||||
valuePrecision?: number
|
||||
showMaxValue?: boolean
|
||||
showTotal?: boolean
|
||||
}
|
||||
|
||||
export const formatBytes = (bytes: number, precision: number = 1) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(precision)} ${sizes[i]}`
|
||||
isActiveHoveredChart?: boolean
|
||||
}
|
||||
|
||||
const isMaxAttribute = (attributes?: MultiAttribute[]) => attributes?.find((a) => a.isMaxValue)
|
||||
@@ -64,6 +66,7 @@ const CustomTooltip = ({
|
||||
isPercentage,
|
||||
valuePrecision,
|
||||
showTotal,
|
||||
isActiveHoveredChart,
|
||||
}: TooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
const timestamp = payload[0].payload.timestamp
|
||||
@@ -72,6 +75,15 @@ const CustomTooltip = ({
|
||||
maxValueAttribute && payload?.find((p: any) => p.dataKey === maxValueAttribute.attribute)
|
||||
const maxValue = maxValueData?.value
|
||||
const isRamChart = payload?.some((p: any) => p.dataKey.toLowerCase().includes('ram_'))
|
||||
const isDiskSpaceChart = payload?.some((p: any) =>
|
||||
p.dataKey.toLowerCase().includes('disk_space_')
|
||||
)
|
||||
const isDBSizeChart = payload?.some((p: any) =>
|
||||
p.dataKey.toLowerCase().includes('pg_database_size')
|
||||
)
|
||||
const isNetworkChart = payload?.some((p: any) => p.dataKey.toLowerCase().includes('network_'))
|
||||
const shouldFormatBytes = isRamChart || isDiskSpaceChart || isDBSizeChart || isNetworkChart
|
||||
|
||||
const total =
|
||||
showTotal &&
|
||||
calculateTotalChartAggregate(
|
||||
@@ -79,29 +91,23 @@ const CustomTooltip = ({
|
||||
maxValueAttribute?.attribute ? [maxValueAttribute.attribute] : []
|
||||
)
|
||||
|
||||
const getIcon = (name: string, color: string) => {
|
||||
switch (name.toLowerCase().includes('max')) {
|
||||
case false:
|
||||
return <CustomIcon color={color} />
|
||||
default:
|
||||
return <MaxConnectionsIcon />
|
||||
}
|
||||
}
|
||||
const getIcon = (color: string, isMax: boolean) =>
|
||||
isMax ? <MaxConnectionsIcon /> : <CustomIcon color={color} />
|
||||
|
||||
const LabelItem = ({ entry }: { entry: any }) => {
|
||||
const attribute = attributes?.find((a: MultiAttribute) => a.attribute === entry.name)
|
||||
const attribute = attributes?.find((a: MultiAttribute) => a?.attribute === entry.name)
|
||||
const percentage = ((entry.value / maxValue) * 100).toFixed(1)
|
||||
const isMax = entry.dataKey === maxValueAttribute?.attribute
|
||||
|
||||
return (
|
||||
<div key={entry.name} className="flex items-center w-full">
|
||||
{getIcon(entry.name, entry.color)}
|
||||
{getIcon(entry.color, isMax)}
|
||||
<span className="text-foreground-lighter ml-1 flex-grow">
|
||||
{attribute?.label || entry.name}
|
||||
</span>
|
||||
<span className="ml-3.5 flex items-end gap-1">
|
||||
{isRamChart
|
||||
? formatBytes(entry.value, valuePrecision)
|
||||
{shouldFormatBytes
|
||||
? formatBytes(isNetworkChart ? Math.abs(entry.value) : entry.value, valuePrecision)
|
||||
: numberFormatter(entry.value, valuePrecision)}
|
||||
{isPercentage ? '%' : ''}
|
||||
|
||||
@@ -115,18 +121,28 @@ const CustomTooltip = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg px-2.5 py-1.5 text-xs shadow-xl">
|
||||
<div
|
||||
className={cn(
|
||||
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg px-2.5 py-1.5 text-xs shadow-xl transition-opacity opacity-100',
|
||||
!isActiveHoveredChart && 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<p className="font-medium">{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}</p>
|
||||
<div className="grid gap-0">
|
||||
{payload.reverse().map((entry: any, index: number) => (
|
||||
<LabelItem key={`${entry.name}-${index}`} entry={entry} />
|
||||
))}
|
||||
{active && showTotal && (
|
||||
<div className="flex md:flex-col gap-1 md:gap-0 text-foreground font-semibold">
|
||||
<div className="flex md:flex-col gap-1 md:gap-0 text-foreground mt-1">
|
||||
<span className="flex-grow text-foreground-lighter">Total</span>
|
||||
<div className="flex items-end gap-1">
|
||||
<span className="text-base">
|
||||
{isRamChart ? formatBytes(total as number, 1) : numberFormatter(total as number)}
|
||||
{shouldFormatBytes
|
||||
? formatBytes(
|
||||
isDBSizeChart ? (total as number) * 1024 * 1024 : (total as number),
|
||||
valuePrecision
|
||||
)
|
||||
: numberFormatter(total as number, valuePrecision)}
|
||||
{isPercentage ? '%' : ''}
|
||||
</span>
|
||||
{maxValueAttribute &&
|
||||
@@ -184,11 +200,11 @@ const CustomLabel = ({ payload, attributes, showMaxValue, onLabelHover }: Custom
|
||||
const isHovered = hoveredLabel === entry.name
|
||||
|
||||
const Label = () => (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 p-1">
|
||||
{getIcon(entry.name, entry.color)}
|
||||
<span
|
||||
className={cn(
|
||||
'text-nowrap text-foreground-lighter pr-2',
|
||||
'text-nowrap text-foreground-lighter',
|
||||
hoveredLabel && !isHovered && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
@@ -203,7 +219,7 @@ const CustomLabel = ({ payload, attributes, showMaxValue, onLabelHover }: Custom
|
||||
<div
|
||||
key={entry.name}
|
||||
className="inline-flex md:flex-col gap-1 md:gap-0 w-fit text-foreground"
|
||||
onMouseEnter={() => handleMouseEnter(entry.name)}
|
||||
onMouseOver={() => handleMouseEnter(entry.name)}
|
||||
onMouseOutCapture={handleMouseLeave}
|
||||
>
|
||||
{!!attribute?.tooltip ? (
|
||||
|
||||
@@ -17,13 +17,14 @@ import { useChartHighlight } from './useChartHighlight'
|
||||
import type { ChartData } from './Charts.types'
|
||||
import type { UpdateDateRange } from 'pages/project/[ref]/reports/database'
|
||||
|
||||
type Provider = 'infra-monitoring' | 'daily-stats'
|
||||
type Provider = 'infra-monitoring' | 'daily-stats' | 'reference-line'
|
||||
|
||||
export type MultiAttribute = {
|
||||
attribute: string
|
||||
provider: Provider
|
||||
label?: string
|
||||
color?: string
|
||||
stackId?: string
|
||||
format?: 'percent' | 'number'
|
||||
description?: string
|
||||
docsLink?: string
|
||||
@@ -31,6 +32,12 @@ export type MultiAttribute = {
|
||||
type?: 'line' | 'area-bar'
|
||||
omitFromTotal?: boolean
|
||||
tooltip?: string
|
||||
customValue?: number
|
||||
id?: string
|
||||
value?: number
|
||||
isReferenceLine?: boolean
|
||||
strokeDasharray?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface ComposedChartHandlerProps {
|
||||
@@ -55,6 +62,7 @@ interface ComposedChartHandlerProps {
|
||||
updateDateRange: UpdateDateRange
|
||||
valuePrecision?: number
|
||||
isVisible?: boolean
|
||||
docsUrl?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +132,8 @@ const ComposedChartHandler = ({
|
||||
updateDateRange,
|
||||
valuePrecision,
|
||||
isVisible = true,
|
||||
id,
|
||||
...otherProps
|
||||
}: PropsWithChildren<ComposedChartHandlerProps>) => {
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
@@ -160,20 +170,46 @@ const ComposedChartHandler = ({
|
||||
const timestamps = new Set<string>()
|
||||
attributeQueries.forEach((query: any) => {
|
||||
query.data?.data?.forEach((point: any) => {
|
||||
timestamps.add(point.period_start)
|
||||
if (point?.period_start) {
|
||||
timestamps.add(point.period_start)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const referenceLineQueries = attributeQueries.filter(
|
||||
(_, index) => attributes[index].provider === 'reference-line'
|
||||
)
|
||||
|
||||
// Combine data points for each timestamp
|
||||
const combined = Array.from(timestamps)
|
||||
.sort()
|
||||
.map((timestamp) => {
|
||||
const point: any = { timestamp }
|
||||
|
||||
// Add regular attributes
|
||||
attributes.forEach((attr, index) => {
|
||||
const queryData = attributeQueries[index].data?.data
|
||||
if (!attr) return
|
||||
// Handle custom value attributes (like disk size)
|
||||
if (attr.customValue !== undefined) {
|
||||
point[attr.attribute] = attr.customValue
|
||||
return
|
||||
}
|
||||
|
||||
// Skip reference line attributes here, we'll add them below
|
||||
if (attr.provider === 'reference-line') return
|
||||
|
||||
const queryData = attributeQueries[index]?.data?.data
|
||||
const matchingPoint = queryData?.find((p: any) => p.period_start === timestamp)
|
||||
point[attr.attribute] = matchingPoint?.[attr.attribute] ?? 0
|
||||
})
|
||||
|
||||
// Add reference line values for each timestamp
|
||||
referenceLineQueries.forEach((query: any) => {
|
||||
const attr = query.data.attribute
|
||||
const value = query.data.total
|
||||
point[attr] = value
|
||||
})
|
||||
|
||||
return point as DataPoint
|
||||
})
|
||||
|
||||
@@ -240,19 +276,14 @@ const ComposedChartHandler = ({
|
||||
<Panel
|
||||
noMargin
|
||||
noHideOverflow
|
||||
className={cn('relative py-2 w-full', className)}
|
||||
className={cn('relative py-2 w-full scroll-mt-16', className)}
|
||||
wrapWithLoading={false}
|
||||
id={id ?? label.toLowerCase().replaceAll(' ', '-')}
|
||||
>
|
||||
<Panel.Content className="flex flex-col gap-4">
|
||||
<div
|
||||
className="absolute right-6 z-50 flex justify-between scroll-mt-10"
|
||||
id={label.toLowerCase().replaceAll(' ', '-')}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="absolute right-6 z-50 flex justify-between scroll-mt-16">{children}</div>
|
||||
<ComposedChart
|
||||
attributes={attributes}
|
||||
YAxisProps={{ width: 1 }}
|
||||
data={combinedData as DataPoint[]}
|
||||
format={format}
|
||||
xAxisKey="period_start"
|
||||
@@ -270,6 +301,7 @@ const ComposedChartHandler = ({
|
||||
updateDateRange={updateDateRange}
|
||||
valuePrecision={valuePrecision}
|
||||
hideChartType={hideChartType}
|
||||
{...otherProps}
|
||||
/>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
@@ -287,11 +319,12 @@ const useAttributeQueries = (
|
||||
isVisible: boolean
|
||||
) => {
|
||||
const infraAttributes = attributes
|
||||
.filter((attr) => attr.provider === 'infra-monitoring')
|
||||
.filter((attr) => attr?.provider === 'infra-monitoring')
|
||||
.map((attr) => attr.attribute as InfraMonitoringAttribute)
|
||||
const dailyStatsAttributes = attributes
|
||||
.filter((attr) => attr.provider === 'daily-stats')
|
||||
.filter((attr) => attr?.provider === 'daily-stats')
|
||||
.map((attr) => attr.attribute as ProjectDailyStatsAttribute)
|
||||
const referenceLines = attributes.filter((attr) => attr?.provider === 'reference-line')
|
||||
|
||||
const infraQueries = useInfraMonitoringQueries(
|
||||
infraAttributes,
|
||||
@@ -314,7 +347,23 @@ const useAttributeQueries = (
|
||||
isVisible
|
||||
)
|
||||
|
||||
return [...infraQueries, ...dailyStatsQueries]
|
||||
const referenceLineQueries = referenceLines.map((line) => {
|
||||
let value = line.value || 0
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: [], // Will be populated in combinedData
|
||||
attribute: line.attribute,
|
||||
total: value,
|
||||
maximum: value,
|
||||
totalGrouped: { [line.attribute]: value },
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
}
|
||||
})
|
||||
|
||||
return [...infraQueries, ...dailyStatsQueries, ...referenceLineQueries]
|
||||
}
|
||||
|
||||
export default function LazyComposedChartHandler(props: ComposedChartHandlerProps) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Badge, Button, cn, Loading } from 'ui'
|
||||
|
||||
interface PanelProps {
|
||||
className?: string
|
||||
id?: string
|
||||
footer?: JSX.Element | false
|
||||
loading?: boolean
|
||||
noMargin?: boolean
|
||||
@@ -24,6 +25,7 @@ function Panel(props: PropsWithChildren<PanelProps>) {
|
||||
props.noMargin ? '' : 'mb-4 md:mb-8',
|
||||
props.className
|
||||
)}
|
||||
id={props.id}
|
||||
>
|
||||
{props.title && (
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { numberFormatter } from 'components/ui/Charts/Charts.utils'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
import { Organization } from '../../types'
|
||||
import { Project } from '../projects/project-detail-query'
|
||||
|
||||
export const getReportAttributes = (isFreePlan: boolean) => [
|
||||
{ id: 'ram_usage', label: 'Memory usage', hide: false },
|
||||
{ id: 'avg_cpu_usage', label: 'Average CPU usage', hide: false },
|
||||
@@ -21,179 +26,426 @@ export const getReportAttributes = (isFreePlan: boolean) => [
|
||||
},
|
||||
]
|
||||
|
||||
export const getReportAttributesV2 = (isFreePlan: boolean) => [
|
||||
{
|
||||
id: 'ram-usage',
|
||||
label: 'Memory usage',
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
showMaxValue: false,
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'ram_usage_used',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Used',
|
||||
tooltip:
|
||||
'Total RAM currently in use by the system and applications. High usage may indicate memory pressure.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_cache_and_buffers',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Cache + buffers',
|
||||
tooltip:
|
||||
'RAM used for caching and buffering disk operations. Higher values improve read performance.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_free',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Free',
|
||||
tooltip:
|
||||
'Available unused RAM. A small amount is always reserved. High unused RAM present with erratic Used RAM may indicate unoptimized queries disrupting cache.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_swap',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Swap',
|
||||
tooltip:
|
||||
'Memory swapped to disk when RAM is full. An instance has 1GB of SWAP. High swap with high disk I/O signals memory stress.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cpu-usage',
|
||||
label: 'CPU usage',
|
||||
format: '%',
|
||||
valuePrecision: 2,
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
showMaxValue: false,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'cpu_usage_busy_system',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'System',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead.',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_user',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'User',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries.',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_iowait',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOwait',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks.',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_irqs',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IRQs',
|
||||
format: '%',
|
||||
tooltip: 'CPU time handling hardware interrupt requests (IRQ)',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_other',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Other',
|
||||
format: '%',
|
||||
tooltip: 'CPU time spent on other tasks (e.g., background processes, software interrupts).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'disk-iops',
|
||||
label: 'Disk IOps',
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'disk_iops_write',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOps write/s',
|
||||
tooltip:
|
||||
'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity.',
|
||||
},
|
||||
{
|
||||
attribute: 'disk_iops_read',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOps read/s',
|
||||
tooltip:
|
||||
'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'client-connections',
|
||||
label: 'Database client connections',
|
||||
valuePrecision: 0,
|
||||
hide: false,
|
||||
showTooltip: false,
|
||||
showLegend: false,
|
||||
showMaxValue: false,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'pg_stat_database_num_backends',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'active connections',
|
||||
tooltip: 'Active connections',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pgbouncer-connections',
|
||||
label: 'Pooler client connections',
|
||||
valuePrecision: 0,
|
||||
hide: false,
|
||||
showTooltip: false,
|
||||
showLegend: false,
|
||||
showMaxValue: false,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'client_connections_pgbouncer',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'pgbouncer',
|
||||
tooltip: 'PgBouncer connections',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'supavisor-connections-active',
|
||||
label: 'Supavisor client connections',
|
||||
valuePrecision: 0,
|
||||
hide: isFreePlan,
|
||||
showTooltip: false,
|
||||
showLegend: false,
|
||||
showMaxValue: false,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'supavisor_connections_active',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'supavisor',
|
||||
tooltip: 'Supavisor connections',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
export const getReportAttributesV2 = (org: Organization, project: Project) => {
|
||||
const isFreePlan = org?.plan?.id === 'free'
|
||||
const computeSize = project?.infra_compute_size || 'medium'
|
||||
const isSpendCapEnabled =
|
||||
org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'ram-usage',
|
||||
label: 'Memory usage',
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
showMaxValue: true,
|
||||
showGrid: true,
|
||||
syncId: 'database-reports',
|
||||
YAxisProps: { width: 60, tickFormatter: (value: any) => formatBytes(value, 0) },
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'ram_usage_used',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Used',
|
||||
tooltip:
|
||||
'Total RAM currently in use by the system and applications. High usage may indicate memory pressure.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_cache_and_buffers',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Cache + buffers',
|
||||
tooltip:
|
||||
'RAM used for caching and buffering disk operations. Higher values improve read performance.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_free',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Free',
|
||||
tooltip:
|
||||
'Available unused RAM. A small amount is always reserved. High unused RAM present with erratic Used RAM may indicate unoptimized queries disrupting cache.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_swap',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Swap',
|
||||
tooltip:
|
||||
'Memory swapped to disk when RAM is full. An instance has 1GB of SWAP. High swap with high disk I/O signals memory stress.',
|
||||
},
|
||||
{
|
||||
attribute: 'ram_usage_total',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Max',
|
||||
isMaxValue: true,
|
||||
tooltip: 'Total RAM',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'cpu-usage',
|
||||
label: 'CPU usage',
|
||||
syncId: 'database-reports',
|
||||
format: '%',
|
||||
valuePrecision: 2,
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
showMaxValue: false,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 45, tickFormatter: (value: any) => `${numberFormatter(value, 2)}%` },
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'cpu_usage_busy_system',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'System',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead.',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_user',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'User',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries.',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_iowait',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOwait',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks.',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_irqs',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IRQs',
|
||||
format: '%',
|
||||
tooltip: 'CPU time handling hardware interrupt requests (IRQ)',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_busy_other',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Other',
|
||||
format: '%',
|
||||
tooltip:
|
||||
'CPU time spent on other tasks (e.g., background processes, software interrupts).',
|
||||
},
|
||||
{
|
||||
attribute: 'cpu_usage_max',
|
||||
provider: 'reference-line',
|
||||
label: 'Max',
|
||||
value: 100,
|
||||
tooltip: 'Max CPU usage',
|
||||
isMaxValue: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'disk-iops',
|
||||
label: 'Disk IOps',
|
||||
syncId: 'database-reports',
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
valuePrecision: 2,
|
||||
showLegend: true,
|
||||
hideChartType: false,
|
||||
showGrid: true,
|
||||
showMaxValue: true,
|
||||
YAxisProps: { width: 35, tickFormatter: (value: any) => numberFormatter(value, 2) },
|
||||
defaultChartStyle: 'line',
|
||||
docsUrl: 'https://supabase.com/docs/guides/platform/compute-and-disk#compute-size',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'disk_iops_write',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOps write/s',
|
||||
tooltip:
|
||||
'Number of write operations per second. High values indicate frequent data writes, logging, or transaction activity.',
|
||||
},
|
||||
{
|
||||
attribute: 'disk_iops_read',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IOps read/s',
|
||||
tooltip:
|
||||
'Number of read operations per second. High values suggest frequent disk reads due to queries or poor caching.',
|
||||
},
|
||||
{
|
||||
attribute: 'disk_iops_max',
|
||||
provider: 'reference-line',
|
||||
label: 'Max IOPS',
|
||||
value: getIOPSLimits(computeSize),
|
||||
tooltip:
|
||||
'Maximum IOPS (Input/Output Operations Per Second) for your current compute size',
|
||||
isMaxValue: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'disk-io-usage',
|
||||
label: 'Disk IO Usage',
|
||||
syncId: 'database-reports',
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
format: '%',
|
||||
valuePrecision: 6,
|
||||
showLegend: false,
|
||||
showMaxValue: false,
|
||||
hideChartType: false,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 70, tickFormatter: (value: any) => `${numberFormatter(value, 6)}%` },
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'disk_io_usage',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'IO Usage',
|
||||
tooltip:
|
||||
'The actual number of IO operations per second that the database is currently using.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'db-size',
|
||||
label: 'Database Size',
|
||||
syncId: 'database-reports',
|
||||
valuePrecision: 2,
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
showMaxValue: true,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 65, tickFormatter: (value: any) => formatBytes(value, 1) },
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
docsUrl: 'https://supabase.com/docs/guides/platform/database-size',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'pg_database_size',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Database',
|
||||
tooltip: 'Total space on disk used by your database (tables, indexes, data, ...).',
|
||||
},
|
||||
{
|
||||
attribute: 'max_pg_database_size',
|
||||
provider: 'reference-line',
|
||||
label: 'Disk size',
|
||||
value: (project?.volumeSizeGb || getRecommendedDbSize(computeSize)) * 1024 * 1024 * 1024,
|
||||
tooltip: 'Disk Size refers to the total space your project occupies on disk',
|
||||
isMaxValue: true,
|
||||
},
|
||||
!isFreePlan &&
|
||||
(isSpendCapEnabled
|
||||
? {
|
||||
attribute: 'pg_database_size_percent_paid_spendCap',
|
||||
provider: 'reference-line',
|
||||
isReferenceLine: true,
|
||||
strokeDasharray: '4 2',
|
||||
label: 'Spend cap enabled',
|
||||
value:
|
||||
(project?.volumeSizeGb || getRecommendedDbSize(computeSize)) * 1024 * 1024 * 1024,
|
||||
className: '[&_line]:!stroke-yellow-800 [&_line]:!opacity-100',
|
||||
opacity: 1,
|
||||
}
|
||||
: {
|
||||
attribute: 'pg_database_size_percent_paid',
|
||||
provider: 'reference-line',
|
||||
isReferenceLine: true,
|
||||
label: '90% - Disk resize threshold',
|
||||
className: '[&_line]:!stroke-yellow-800',
|
||||
value:
|
||||
(project?.volumeSizeGb || getRecommendedDbSize(computeSize)) *
|
||||
1024 *
|
||||
1024 *
|
||||
1024 *
|
||||
0.9, // reaching 90% of the disk size will trigger a disk resize https://supabase.com/docs/guides/platform/database-size
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'network-traffic',
|
||||
label: 'Network Traffic',
|
||||
syncId: 'database-reports',
|
||||
valuePrecision: 1,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
showMaxValue: false,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 65, tickFormatter: (value: any) => formatBytes(value, 1) },
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
hideHighlightedValue: true,
|
||||
showTotal: false,
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'network_transmit_bytes',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Transmit',
|
||||
tooltip:
|
||||
'Data sent from your database to clients. High values may indicate large query results or numerous outgoing connections.',
|
||||
stackId: '2',
|
||||
},
|
||||
{
|
||||
attribute: 'network_receive_bytes',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Receive',
|
||||
tooltip:
|
||||
'Data received by your database from clients. High values may indicate frequent queries, large data inserts, or many incoming connections.',
|
||||
stackId: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'client-connections',
|
||||
label: 'Database client connections',
|
||||
syncId: 'database-reports',
|
||||
valuePrecision: 0,
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
showMaxValue: true,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 30 },
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
docsUrl: 'https://supabase.com/docs/guides/platform/compute-and-disk#limits-and-constraints',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'pg_stat_database_num_backends',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'Active connections',
|
||||
},
|
||||
{
|
||||
attribute: 'pg_database_max_connections',
|
||||
provider: 'reference-line',
|
||||
label: 'Max connections',
|
||||
value: getConnectionLimits(computeSize).direct,
|
||||
tooltip: 'Max available connections for your current compute size',
|
||||
isMaxValue: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pgbouncer-connections',
|
||||
label: 'Pooler client connections',
|
||||
syncId: 'database-reports',
|
||||
valuePrecision: 0,
|
||||
hide: false,
|
||||
showTooltip: true,
|
||||
showLegend: true,
|
||||
showMaxValue: true,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 30 },
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
docsUrl: 'https://supabase.com/docs/guides/platform/compute-and-disk#limits-and-constraints',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'client_connections_pgbouncer',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'pgbouncer',
|
||||
tooltip: 'PgBouncer connections',
|
||||
},
|
||||
{
|
||||
attribute: 'pg_pooler_max_connections',
|
||||
provider: 'reference-line',
|
||||
label: 'Max pooler connections',
|
||||
value: getConnectionLimits(computeSize).pooler,
|
||||
tooltip: 'Maximum allowed pooler connections for your current compute size',
|
||||
isMaxValue: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'supavisor-connections-active',
|
||||
label: 'Supavisor client connections',
|
||||
syncId: 'database-reports',
|
||||
valuePrecision: 0,
|
||||
hide: isFreePlan,
|
||||
showTooltip: false,
|
||||
showLegend: false,
|
||||
showMaxValue: false,
|
||||
showGrid: true,
|
||||
YAxisProps: { width: 30 },
|
||||
hideChartType: false,
|
||||
defaultChartStyle: 'line',
|
||||
attributes: [
|
||||
{
|
||||
attribute: 'supavisor_connections_active',
|
||||
provider: 'infra-monitoring',
|
||||
label: 'supavisor',
|
||||
tooltip: 'Supavisor connections',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Helper function to get connection limits based on compute size
|
||||
export const getConnectionLimits = (computeSize: string = 'medium') => {
|
||||
const connectionLimits = {
|
||||
nano: { direct: 60, pooler: 200 },
|
||||
micro: { direct: 60, pooler: 200 },
|
||||
small: { direct: 90, pooler: 400 },
|
||||
medium: { direct: 120, pooler: 600 },
|
||||
large: { direct: 160, pooler: 800 },
|
||||
xl: { direct: 240, pooler: 1000 },
|
||||
'2xl': { direct: 380, pooler: 1500 },
|
||||
'4xl': { direct: 480, pooler: 3000 },
|
||||
'8xl': { direct: 490, pooler: 6000 },
|
||||
'12xl': { direct: 500, pooler: 9000 },
|
||||
'16xl': { direct: 500, pooler: 12000 },
|
||||
}
|
||||
|
||||
return (
|
||||
connectionLimits[computeSize?.toLowerCase() as keyof typeof connectionLimits] ||
|
||||
connectionLimits.medium
|
||||
)
|
||||
}
|
||||
|
||||
// Helper function to get IOPS limits based on compute size
|
||||
export const getIOPSLimits = (computeSize: string = 'medium') => {
|
||||
const iopsLimits = {
|
||||
nano: 250,
|
||||
micro: 500,
|
||||
small: 1000,
|
||||
medium: 2000,
|
||||
large: 3600,
|
||||
xl: 6000,
|
||||
'2xl': 12000,
|
||||
'4xl': 20000,
|
||||
'8xl': 40000,
|
||||
'12xl': 50000,
|
||||
'16xl': 80000,
|
||||
}
|
||||
|
||||
return iopsLimits[computeSize?.toLowerCase() as keyof typeof iopsLimits] || iopsLimits.medium
|
||||
}
|
||||
|
||||
// Helper function to get recommended DB size based on compute size (in GB)
|
||||
export const getRecommendedDbSize = (computeSize: string = 'medium') => {
|
||||
const recommendedSizes = {
|
||||
nano: 0.5, // 500 MB
|
||||
micro: 10,
|
||||
small: 50,
|
||||
medium: 100,
|
||||
large: 200,
|
||||
xl: 500,
|
||||
'2xl': 1024, // 1 TB
|
||||
'4xl': 2048, // 2 TB
|
||||
'8xl': 4096, // 4 TB
|
||||
'12xl': 6144, // 6 TB
|
||||
'16xl': 10240, // 10 TB
|
||||
}
|
||||
|
||||
return (
|
||||
recommendedSizes[computeSize?.toLowerCase() as keyof typeof recommendedSizes] ||
|
||||
recommendedSizes.medium
|
||||
)
|
||||
}
|
||||
@@ -130,8 +130,15 @@ export const formatBytes = (
|
||||
const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
|
||||
if (bytes === 0 || bytes === undefined) return size !== undefined ? `0 ${size}` : '0 bytes'
|
||||
const i = size !== undefined ? sizes.indexOf(size) : Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||
|
||||
// Handle negative values
|
||||
const isNegative = bytes < 0
|
||||
const absBytes = Math.abs(bytes)
|
||||
|
||||
const i = size !== undefined ? sizes.indexOf(size) : Math.floor(Math.log(absBytes) / Math.log(k))
|
||||
const formattedValue = parseFloat((absBytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||
|
||||
return isNegative ? '-' + formattedValue : formattedValue
|
||||
}
|
||||
|
||||
export const snakeToCamel = (str: string) =>
|
||||
|
||||
@@ -37,6 +37,8 @@ import { TIME_PERIODS_INFRA } from 'lib/constants/metrics'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { useOrganizationQuery } from '../../../../data/organizations/organization-query'
|
||||
import { useSelectedOrganization } from '../../../../hooks/misc/useSelectedOrganization'
|
||||
|
||||
const DatabaseReport: NextPageWithLayout = () => {
|
||||
return (
|
||||
@@ -59,12 +61,15 @@ const DatabaseUsage = () => {
|
||||
const { db, chart, ref } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
const isReportsV2 = useFlag('reportsDatabaseV2')
|
||||
const org = useSelectedOrganization()
|
||||
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
|
||||
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
|
||||
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
const defaultStart = dayjs().subtract(7, 'day').toISOString()
|
||||
const defaultStart = dayjs().subtract(1, 'day').toISOString()
|
||||
const defaultEnd = dayjs().toISOString()
|
||||
const [dateRange, setDateRange] = useState<any>({
|
||||
period_start: { date: defaultStart, time_period: '7d' },
|
||||
period_start: { date: defaultStart, time_period: '1d' },
|
||||
period_end: { date: defaultEnd, time_period: 'today' },
|
||||
interval: '1h',
|
||||
})
|
||||
@@ -80,7 +85,7 @@ const DatabaseUsage = () => {
|
||||
|
||||
const { data: databaseSizeData } = useDatabaseSizeQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
connectionString: project?.connectionString || undefined,
|
||||
})
|
||||
const databaseSizeBytes = databaseSizeData ?? 0
|
||||
const currentDiskSize = project?.volumeSizeGb ?? 0
|
||||
@@ -92,11 +97,8 @@ const DatabaseUsage = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
|
||||
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
|
||||
|
||||
const REPORT_ATTRIBUTES = getReportAttributes(isFreePlan)
|
||||
const REPORT_ATTRIBUTES_V2 = getReportAttributesV2(isFreePlan)
|
||||
const REPORT_ATTRIBUTES_V2 = getReportAttributesV2(org!, project!)
|
||||
|
||||
const { isLoading: isUpdatingDiskSize } = useProjectDiskResizeMutation({
|
||||
onSuccess: (_, variables) => {
|
||||
@@ -124,6 +126,21 @@ const DatabaseUsage = () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
if (isReportsV2) {
|
||||
REPORT_ATTRIBUTES_V2.forEach((chart: any) => {
|
||||
chart.attributes.forEach((attr: any) => {
|
||||
queryClient.invalidateQueries(
|
||||
analyticsKeys.infraMonitoring(ref, {
|
||||
attribute: attr.attribute,
|
||||
startDate: period_start.date,
|
||||
endDate: period_start.end,
|
||||
interval,
|
||||
databaseIdentifier: state.selectedDatabaseId,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
if (isReplicaSelected) {
|
||||
queryClient.invalidateQueries(
|
||||
analyticsKeys.infraMonitoring(ref, {
|
||||
@@ -179,7 +196,7 @@ const DatabaseUsage = () => {
|
||||
|
||||
const updateDateRange: UpdateDateRange = (from: string, to: string) => {
|
||||
setDateRange({
|
||||
period_start: { date: from, time_period: '7d' },
|
||||
period_start: { date: from, time_period: '1d' },
|
||||
period_end: { date: to, time_period: 'today' },
|
||||
interval: handleIntervalGranularity(from, to),
|
||||
})
|
||||
@@ -208,7 +225,7 @@ const DatabaseUsage = () => {
|
||||
<div className="flex items-center gap-3">
|
||||
<DateRangePicker
|
||||
loading={false}
|
||||
value={'7d'}
|
||||
value={'1d'}
|
||||
options={TIME_PERIODS_INFRA}
|
||||
currentBillingPeriodStart={undefined}
|
||||
onChange={(values) => {
|
||||
@@ -238,16 +255,21 @@ const DatabaseUsage = () => {
|
||||
{isReportsV2 ? (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{dateRange &&
|
||||
REPORT_ATTRIBUTES_V2.filter((attr) => !attr.hide).map((attr) => (
|
||||
REPORT_ATTRIBUTES_V2.filter((chart) => !chart.hide).map((chart) => (
|
||||
<ComposedChartHandler
|
||||
key={attr.id}
|
||||
{...attr}
|
||||
attributes={attr.attributes as MultiAttribute[]}
|
||||
key={chart.id}
|
||||
{...chart}
|
||||
attributes={chart.attributes as MultiAttribute[]}
|
||||
interval={dateRange.interval}
|
||||
startDate={dateRange?.period_start?.date}
|
||||
endDate={dateRange?.period_end?.date}
|
||||
updateDateRange={updateDateRange}
|
||||
defaultChartStyle={attr.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
|
||||
defaultChartStyle={chart.defaultChartStyle as 'line' | 'bar' | 'stackedAreaLine'}
|
||||
showMaxValue={
|
||||
chart.id === 'client-connections' || chart.id === 'pgbouncer-connections'
|
||||
? true
|
||||
: chart.showMaxValue
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -258,6 +280,7 @@ const DatabaseUsage = () => {
|
||||
REPORT_ATTRIBUTES.filter((attr) => !attr.hide).map((attr) => (
|
||||
<ChartHandler
|
||||
key={attr.id}
|
||||
{...attr}
|
||||
provider="infra-monitoring"
|
||||
attribute={attr.id}
|
||||
label={attr.label}
|
||||
@@ -298,16 +321,16 @@ const DatabaseUsage = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="col-span-4 inline-grid grid-cols-12 gap-12 w-full mt-5">
|
||||
<div className="grid gap-2 col-span-2">
|
||||
<div className="grid gap-2 col-span-4 xl:col-span-2">
|
||||
<h5 className="text-sm">Space used</h5>
|
||||
<span className="text-lg">{formatBytes(databaseSizeBytes, 2, 'GB')}</span>
|
||||
</div>
|
||||
<div className="grid gap-2 col-span-2">
|
||||
<div className="grid gap-2 col-span-4 xl:col-span-3">
|
||||
<h5 className="text-sm">Provisioned disk size</h5>
|
||||
<span className="text-lg">{currentDiskSize} GB</span>
|
||||
</div>
|
||||
|
||||
<div className="col-span-8 text-right">
|
||||
<div className="col-span-full lg:col-span-4 xl:col-span-7 lg:text-right">
|
||||
{project?.cloud_provider === 'AWS' ? (
|
||||
<Button asChild type="default">
|
||||
<Link href={`/project/${ref}/settings/compute-and-disk`}>
|
||||
|
||||
+6
-1
@@ -16401,25 +16401,30 @@ export interface operations {
|
||||
| 'cpu_usage_busy_irqs'
|
||||
| 'cpu_usage_busy_other'
|
||||
| 'cpu_usage_busy_idle'
|
||||
| 'client_connections_pgbouncer'
|
||||
| 'max_cpu_usage'
|
||||
| 'avg_cpu_usage'
|
||||
| 'disk_io_budget'
|
||||
| 'disk_io_consumption'
|
||||
| 'disk_io_usage'
|
||||
| 'disk_iops_read'
|
||||
| 'disk_iops_write'
|
||||
| 'disk_bytes_read'
|
||||
| 'disk_bytes_written'
|
||||
| 'pg_database_size'
|
||||
| 'ram_usage'
|
||||
| 'ram_usage_total'
|
||||
| 'ram_usage_available'
|
||||
| 'ram_usage_used'
|
||||
| 'ram_usage_free'
|
||||
| 'ram_usage_cache_and_buffers'
|
||||
| 'ram_usage_swap'
|
||||
| 'swap_usage'
|
||||
| 'client_connections_pgbouncer'
|
||||
| 'physical_replication_lag_physical_replica_lag_seconds'
|
||||
| 'pg_stat_database_num_backends'
|
||||
| 'supavisor_connections_active'
|
||||
| 'network_receive_bytes'
|
||||
| 'network_transmit_bytes'
|
||||
| 'pgbouncer_pools_client_active_connections'
|
||||
databaseIdentifier?: string
|
||||
endDate: string
|
||||
|
||||
Reference in new issue
Block a user