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:
Francesco Sansalvadore authored and GitHub committed 2025-05-23 10:32:33 +02:00
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) {
+2
View File
@@ -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
+428 -176
View File
@@ -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
)
}
+9 -2
View File
@@ -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
View File
@@ -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