From dfd3eec8e9fb6be2842139fd2d8ffab9491e3756 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:15:11 +0200 Subject: [PATCH] feat(studio): compute metrics on project diagram Primary Database card (#45274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The Primary Database card in the project homepage diagram showed region and instance size, but no live health data. Users had no quick way to spot a high-disk or high-CPU situation without navigating to the database report. ## Fix Added a clickable metrics row at the bottom of the Primary Database card showing CPU, Disk, and RAM as percentages, plus active/max connections when available. Each metric is color-coded (warning at 80%, destructive at 90%). Clicking the row navigates to the database observability report. The metrics are powered by a new \`useComputeMetrics\` hook that wraps the existing \`useInfraMonitoringAttributesQuery\` and \`useMaxConnectionsQuery\`, reusing the parse utilities already used by the database infrastructure section. The \`metricColor\` threshold logic is extracted into a separate util with unit tests. ## How to test - Open the project homepage for a running project - The Primary Database card should show a new bottom row: "CPU X% · Disk X% · RAM X% · Y/Z conns" - Values above 80% should appear in amber, above 90% in red - Click the metrics row and confirm it navigates to \`/project//observability/database\` - While metrics are loading, a spinner should appear in the row - If the infra monitoring API is unavailable, the row should show "Metrics unavailable" instead of zeroes ## Summary by CodeRabbit ## Release Notes * **New Features** * Infrastructure configuration page now displays real-time compute metrics (CPU, disk, memory usage) with color-coded usage indicators based on thresholds. * Connection information is displayed when available. * Includes loading states and error handling for metric retrieval. * **Tests** * Added test coverage for metric color-coding logic. --------- Co-authored-by: Claude Sonnet 4.6 --- .../InstanceNode.tsx | 53 +++++++++++++ .../InstanceNode.utils.test.ts | 29 ++++++++ .../InstanceNode.utils.ts | 5 ++ .../hooks/analytics/useComputeMetrics.ts | 74 +++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.utils.test.ts create mode 100644 apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.utils.ts create mode 100644 apps/studio/hooks/analytics/useComputeMetrics.ts diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx index 30fb3142c4f..ff815bf908f 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.tsx @@ -29,6 +29,7 @@ import { ReplicaNodeData, } from './InstanceConfiguration.constants' import { formatSeconds } from './InstanceConfiguration.utils' +import { metricColor } from './InstanceNode.utils' import SparkBar from '@/components/ui/SparkBar' import { DatabaseInitEstimations, @@ -36,6 +37,7 @@ import { useReadReplicasStatusesQuery, } from '@/data/read-replicas/replicas-status-query' import { formatDatabaseID } from '@/data/read-replicas/replicas.utils' +import { useComputeMetrics } from '@/hooks/analytics/useComputeMetrics' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { BASE_PATH } from '@/lib/constants' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' @@ -85,11 +87,25 @@ export const LoadBalancerNode = ({ data }: NodeProps>) => export const PrimaryNode = ({ data }: NodeProps>) => { // [Joshen] Just FYI Handles cannot be conditionally rendered const { region, computeSize, numReplicas, numRegions, hasLoadBalancer } = data + const { ref } = useParams() const { projectHomepageShowInstanceSize } = useIsFeatureEnabled([ 'project_homepage:show_instance_size', ]) + const { + cpu, + disk, + memory, + connections, + isLoading: metricsLoading, + isError: metricsError, + } = useComputeMetrics({ + projectRef: ref, + }) + + const observabilityUrl = `/project/${ref}/observability/database` + return ( <> >) => {

)} + + + + {metricsLoading ? ( +
+ ) : metricsError ? ( + Metrics unavailable + ) : ( + <> + + CPU {cpu.toFixed(0)}% + + · + + Disk {disk.toFixed(0)}% + + · + + RAM {memory.toFixed(0)}% + + {connections.max > 0 && ( + <> + · + + {connections.current}/{connections.max} conns + + + )} + + )} + + + Go to Database Report +
{ + it('returns warning color at 80%', () => { + expect(metricColor(80)).toBe('text-warning') + }) + + it('returns warning color between 80% and 90%', () => { + expect(metricColor(85)).toBe('text-warning') + expect(metricColor(89)).toBe('text-warning') + }) + + it('returns destructive color at 90%', () => { + expect(metricColor(90)).toBe('text-destructive') + }) + + it('returns destructive color above 90%', () => { + expect(metricColor(95)).toBe('text-destructive') + expect(metricColor(100)).toBe('text-destructive') + }) + + it('returns light foreground color below 80%', () => { + expect(metricColor(0)).toBe('text-foreground-light') + expect(metricColor(50)).toBe('text-foreground-light') + expect(metricColor(79)).toBe('text-foreground-light') + }) +}) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.utils.ts b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.utils.ts new file mode 100644 index 00000000000..560a30e78d9 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceNode.utils.ts @@ -0,0 +1,5 @@ +export function metricColor(value: number): string { + if (value >= 90) return 'text-destructive' + if (value >= 80) return 'text-warning' + return 'text-foreground-light' +} diff --git a/apps/studio/hooks/analytics/useComputeMetrics.ts b/apps/studio/hooks/analytics/useComputeMetrics.ts new file mode 100644 index 00000000000..37b84320451 --- /dev/null +++ b/apps/studio/hooks/analytics/useComputeMetrics.ts @@ -0,0 +1,74 @@ +import dayjs from 'dayjs' +import { useMemo } from 'react' + +import { + parseConnectionsData, + parseInfrastructureMetrics, +} from '@/components/interfaces/Observability/DatabaseInfrastructureSection.utils' +import { useInfraMonitoringAttributesQuery } from '@/data/analytics/infra-monitoring-query' +import { useMaxConnectionsQuery } from '@/data/database/max-connections-query' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' + +export type ComputeMetrics = { + cpu: number + disk: number + memory: number + connections: { current: number; max: number } + isLoading: boolean + isError: boolean +} + +export function useComputeMetrics({ projectRef }: { projectRef?: string }): ComputeMetrics { + const { data: project } = useSelectedProjectQuery() + + // Intentionally anchored to mount time so the query key stays stable across re-renders. + // React Query's staleTime handles background refresh without shifting the window. + // eslint-disable-next-line react-hooks/exhaustive-deps + const { startDate, endDate } = useMemo(() => { + const now = dayjs() + return { + startDate: now.subtract(1, 'hour').toISOString(), + endDate: now.toISOString(), + } + }, []) + + const { + data: infraData, + isLoading: infraLoading, + isError, + } = useInfraMonitoringAttributesQuery({ + projectRef, + attributes: [ + 'avg_cpu_usage', + 'ram_usage', + 'disk_fs_used_system', + 'disk_fs_used_wal', + 'pg_database_size', + 'disk_fs_size', + 'pg_stat_database_num_backends', + ], + startDate, + endDate, + interval: '1h', + }) + + const { data: maxConnectionsData, isLoading: connectionsLoading } = useMaxConnectionsQuery({ + projectRef, + connectionString: project?.connectionString, + }) + + const metrics = useMemo(() => parseInfrastructureMetrics(infraData), [infraData]) + const connections = useMemo( + () => parseConnectionsData(infraData, maxConnectionsData), + [infraData, maxConnectionsData] + ) + + return { + cpu: metrics?.cpu.current ?? 0, + disk: metrics?.disk.current ?? 0, + memory: metrics?.ram.current ?? 0, + connections, + isLoading: infraLoading || connectionsLoading, + isError, + } +}