diff --git a/apps/studio/components/interfaces/Home/ProjectLogsChart.tsx b/apps/studio/components/interfaces/Home/ProjectLogsChart.tsx index 50b6d3bb837..891487cf5fa 100644 --- a/apps/studio/components/interfaces/Home/ProjectLogsChart.tsx +++ b/apps/studio/components/interfaces/Home/ProjectLogsChart.tsx @@ -28,6 +28,7 @@ import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' import { LogsBarChart } from 'ui-patterns/LogsBarChart' import AlertError from 'components/ui/AlertError' import { useParams } from 'common' +import { ProjectUsageBars } from './ProjectUsageBars' interface ProjectLogsChartProps { projectRef: string @@ -244,7 +245,7 @@ const ProjectLogsChart = ({ projectRef, startDate, endDate, interval }: ProjectL
{/* Main Content Area */} -
+
{/* Filter and Total Count (Moved to the right) */}
@@ -329,13 +330,14 @@ const ProjectLogsChart = ({ projectRef, startDate, endDate, interval }: ProjectL
+
{/* Chart Area */}
{isLoading && ( -
+
@@ -352,7 +354,7 @@ const ProjectLogsChart = ({ projectRef, startDate, endDate, interval }: ProjectL

In the last two hours

) : ( -
+
)} 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) }} > + +