+
)}
diff --git a/apps/studio/components/interfaces/Home/ProjectUsageBars.tsx b/apps/studio/components/interfaces/Home/ProjectUsageBars.tsx
new file mode 100644
index 00000000000..d2304cda9ea
--- /dev/null
+++ b/apps/studio/components/interfaces/Home/ProjectUsageBars.tsx
@@ -0,0 +1,229 @@
+import { useMemo } from 'react'
+import dayjs from 'dayjs'
+import { WifiOff } from 'lucide-react'
+
+import { useParams } from 'common'
+import { useDiskUtilizationQuery } from 'data/config/disk-utilization-query'
+import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
+import { GB } from 'lib/constants'
+import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider, cn } from 'ui'
+import ShimmeringLoader from 'components/ui/ShimmeringLoader'
+
+interface UsageBarProps {
+ label: string
+ usage: number | undefined // Percentage
+ limit?: number // Upper limit for the bar (e.g., 100)
+ unit?: string
+ isLoading: boolean
+ error: Error | null
+ warningThreshold?: number
+ dangerThreshold?: number
+ className?: string
+}
+
+const UsageBar = ({
+ label,
+ usage,
+ limit = 100,
+ unit = '%',
+ isLoading,
+ error,
+ warningThreshold = 75,
+ dangerThreshold = 90,
+ className,
+}: UsageBarProps) => {
+ const usagePercentage = usage !== undefined ? Math.min(Math.max(usage, 0), limit) : 0
+ const barWidth = `${(usagePercentage / limit) * 100}%`
+
+ let barColor = 'bg-foreground-lighter' // Default/healthy color
+ if (usage !== undefined) {
+ if (usage >= dangerThreshold) {
+ barColor = 'bg-destructive'
+ } else if (usage >= warningThreshold) {
+ barColor = 'bg-warning'
+ }
+ }
+
+ const displayValue = usage !== undefined ? `${usage.toFixed(0)}${unit}` : '--'
+
+ return (
+
+
+
+
+
{label}
+
+ {isLoading && (
+
+ )}
+ {!isLoading && !error && (
+
+ )}
+ {!isLoading && error && (
+
+
+
+ )}
+
+
+ {isLoading ? '...' : displayValue}
+
+
+
+
+ {isLoading
+ ? `Loading ${label} usage...`
+ : error
+ ? `Failed to load ${label} usage: ${error.message}`
+ : `${label} Usage: ${usage !== undefined ? `${usage.toFixed(2)}${unit}` : 'N/A'}`}
+
+
+
+ )
+}
+
+interface ProjectUsageBarsProps {
+ projectRef: string | undefined
+}
+
+export const ProjectUsageBars = ({ projectRef }: ProjectUsageBarsProps) => {
+ const interval = '1h'
+ const now = dayjs()
+ // Fetch data slightly further back to ensure data points are available
+ const startDate = now.subtract(2, 'hour').toISOString().slice(0, -5) + 'Z'
+ const endDate = now.toISOString().slice(0, -5) + 'Z'
+ const dateFormat = 'YYYY-MM-DDTHH:mm:ssZ' // Not really used for display, but needed for query
+
+ // Disk Size Usage
+ const {
+ data: diskUtil,
+ error: diskUtilError,
+ isLoading: isLoadingDiskUtil,
+ } = useDiskUtilizationQuery(
+ { projectRef },
+ {
+ enabled: !!projectRef,
+ // Refetch relatively frequently as disk usage can change
+ refetchInterval: 5 * 60 * 1000, // 5 minutes
+ }
+ )
+ const diskUsagePercent = useMemo(() => {
+ if (!diskUtil) return undefined
+ const { metrics } = diskUtil
+ const used = metrics?.fs_used_bytes ?? 0
+ // Use fs_size_bytes for the total size
+ const total = metrics?.fs_size_bytes ?? 0
+ return total > 0 ? (used / total) * 100 : 0
+ }, [diskUtil])
+
+ // CPU Usage
+ const {
+ data: cpuUsageData,
+ error: cpuUsageError,
+ isLoading: isLoadingCpuUsage,
+ } = useInfraMonitoringQuery(
+ {
+ projectRef,
+ attribute: 'max_cpu_usage',
+ interval,
+ startDate,
+ endDate,
+ dateFormat,
+ },
+ {
+ enabled: !!projectRef,
+ // Refetch relatively frequently
+ refetchInterval: 5 * 60 * 1000, // 5 minutes
+ }
+ )
+ const latestCpuUsage = useMemo(
+ () => cpuUsageData?.data?.slice(-1)?.[0]?.max_cpu_usage,
+ [cpuUsageData]
+ )
+
+ // Memory Usage
+ const {
+ data: memoryUsageData,
+ error: memoryUsageError,
+ isLoading: isLoadingMemoryUsage,
+ } = useInfraMonitoringQuery(
+ {
+ projectRef,
+ attribute: 'ram_usage',
+ interval,
+ startDate,
+ endDate,
+ dateFormat,
+ },
+ {
+ enabled: !!projectRef,
+ // Refetch relatively frequently
+ refetchInterval: 5 * 60 * 1000, // 5 minutes
+ }
+ )
+ const latestMemoryUsage = useMemo(
+ () => memoryUsageData?.data?.slice(-1)?.[0]?.ram_usage,
+ [memoryUsageData]
+ )
+
+ // Disk IO Usage (Budget Consumption)
+ const {
+ data: ioBudgetData,
+ error: ioBudgetError,
+ isLoading: isLoadingIoBudget,
+ } = useInfraMonitoringQuery(
+ {
+ projectRef,
+ attribute: 'disk_io_consumption',
+ interval,
+ startDate,
+ endDate,
+ dateFormat,
+ },
+ {
+ enabled: !!projectRef,
+ // Refetch relatively frequently
+ refetchInterval: 5 * 60 * 1000, // 5 minutes
+ }
+ )
+ const latestIoBudgetUsage = useMemo(
+ () => ioBudgetData?.data?.slice(-1)?.[0]?.disk_io_consumption,
+ [ioBudgetData]
+ )
+
+ if (!projectRef) return null // Don't render if projectRef is missing
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/pages/project/[ref]/home.tsx b/apps/studio/pages/project/[ref]/home.tsx
index cfa084bcecc..fdb3cc78188 100644
--- a/apps/studio/pages/project/[ref]/home.tsx
+++ b/apps/studio/pages/project/[ref]/home.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useMemo } from 'react'
+import { useEffect, useRef, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import Link from 'next/link'
import { Check, ChevronRight, FileText } from 'lucide-react'
@@ -59,6 +59,7 @@ import { ExternalLink } from 'lucide-react'
import Image from 'next/image'
import AssistantButton from 'components/layouts/AppLayout/AssistantButton'
import { GettingStarted } from 'components/interfaces/Home/GettingStarted'
+import { ProjectUsageBars } from 'components/interfaces/Home/ProjectUsageBars'
const Home: NextPageWithLayout = () => {
const organization = useSelectedOrganization()
@@ -67,6 +68,10 @@ const Home: NextPageWithLayout = () => {
const snap = useAppStateSnapshot()
const { enableBranching } = useParams()
+ // State to manage header opacity and scale based on scroll
+ const [headerOpacity, setHeaderOpacity] = useState(1)
+ const [headerScale, setHeaderScale] = useState(1)
+
const hasShownEnableBranchingModalRef = useRef(false)
useEffect(() => {
if (enableBranching && !hasShownEnableBranchingModalRef.current) {
@@ -76,6 +81,43 @@ const Home: NextPageWithLayout = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enableBranching])
+ // Effect to handle scroll and update opacity/scale
+ // useEffect(() => {
+ // // Try to find the main scrollable element
+ // // Adjust the selector if your main scroll container is different
+ // const scrollContainer = document.querySelector('main') as HTMLElement | null
+
+ // const handleScroll = () => {
+ // let scrollPosition = 0
+ // if (scrollContainer) {
+ // scrollPosition = scrollContainer.scrollTop
+ // } else {
+ // // Fallback to window scroll if main isn't found
+ // scrollPosition = window.scrollY
+ // }
+ // // Fade out opacity over the first 200 pixels, min 0.15
+ // const newOpacity = Math.max(0.0, 1 - scrollPosition / 200)
+ // setHeaderOpacity(newOpacity)
+
+ // // Scale down over the first 200 pixels, min 0.98
+ // const scaleFactor = 0.02 // Amount to scale down (1 - 0.98)
+ // const newScale = Math.max(0.98, 1 - (scrollPosition / 200) * scaleFactor)
+ // setHeaderScale(newScale)
+ // }
+
+ // const targetElement = scrollContainer || window
+ // targetElement.addEventListener('scroll', handleScroll)
+
+ // // Initial call to set opacity based on initial scroll position
+ // handleScroll()
+
+ // // Cleanup function to remove the event listener
+ // return () => {
+ // targetElement.removeEventListener('scroll', handleScroll)
+ // }
+ // // Re-run effect if scrollContainer reference changes (though unlikely with querySelector)
+ // }, [])
+
const projectName =
project?.ref !== 'default' && project?.name !== undefined
? project?.name
@@ -140,40 +182,52 @@ const Home: NextPageWithLayout = () => {
return (
-
- {project?.status !== PROJECT_STATUS.INACTIVE && project?.cloud_provider !== 'FLY' && (
+ {/* Apply dynamic opacity and scale to the sticky header with transition */}
+
+ {/* {project?.status !== PROJECT_STATUS.INACTIVE && project?.cloud_provider !== 'FLY' && (
- )}
-
-
{projectName}
-
- {isOrioleDb && (
-
-
- OrioleDB
-
-
- This project is using Postgres with OrioleDB which is currently in preview and not
- suitable for production workloads. View our{' '}
-
- documentation
- {' '}
- for all limitations.
-
-
- )}
-
+ )} */}
+
+ {/* Left side: Title and Badges */}
+
+
{projectName}
+
+ {isOrioleDb && (
+
+
+ OrioleDB
+
+
+ This project is using Postgres with OrioleDB which is currently in preview and
+ not suitable for production workloads. View our{' '}
+
+ documentation
+ {' '}
+ for all limitations.
+
+
+ )}
+
+
+
{
{project?.status !== PROJECT_STATUS.INACTIVE && (
<>
-
+
{/* [Charmer End] Getting Started guide */}
@@ -331,7 +385,7 @@ const Home: NextPageWithLayout = () => {
)}
{!isLoadingReports && reportContent && (
-
+
{reportContent.layout
.sort((a, b) => {
if (a.y !== b.y) {
diff --git a/packages/ui-patterns/LogsBarChart/index.tsx b/packages/ui-patterns/LogsBarChart/index.tsx
index 562b2439ba4..f9d00c64446 100644
--- a/packages/ui-patterns/LogsBarChart/index.tsx
+++ b/packages/ui-patterns/LogsBarChart/index.tsx
@@ -1,7 +1,7 @@
'use client'
import dayjs from 'dayjs'
import { ReactNode, useState } from 'react'
-import { Bar, Cell, BarChart as RechartBarChart, XAxis, YAxis } from 'recharts'
+import { Bar, CartesianGrid, Cell, BarChart as RechartBarChart, XAxis, YAxis } from 'recharts'
import type { CategoricalChartState } from 'recharts/types/chart/types'
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent, cn } from 'ui'
@@ -78,6 +78,8 @@ export const LogsBarChart = ({
if (onBarClick) onBarClick(datum, tooltipData)
}}
>
+
+