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,
+ }
+}