plan usage

This commit is contained in:
Saxon Fletcher committed 2025-05-02 07:30:49 +10:00
1 parent 039c277e96
commit 94a0918d3d
4 files changed
+323 -36

No files matched your search

@@ -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
<div className="relative z-10">
<div>
{/* Main Content Area */}
<div className="max-w-7xl w-full mx-auto">
<div className="max-w-7xl w-full mx-auto flex items-center justify-between">
<div>
{/* Filter and Total Count (Moved to the right) */}
<div className="col-span-1 md:col-span-2">
@@ -329,13 +330,14 @@ const ProjectLogsChart = ({ projectRef, startDate, endDate, interval }: ProjectL
</div>
</div>
</div>
<ProjectUsageBars projectRef={projectRef} />
</div>
</div>
{/* Chart Area */}
<div className="p-0">
{isLoading && (
<div className="space-y-2 mt-8">
<div className="space-y-2 mt-12">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
</div>
@@ -352,7 +354,7 @@ const ProjectLogsChart = ({ projectRef, startDate, endDate, interval }: ProjectL
<p className="text-xs text-foreground-lighter">In the last two hours</p>
</div>
) : (
<div>
<div className="pt-12">
<LogsBarChart isSubtle barHeight={300} data={chartData} />
</div>
)}
@@ -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 (
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<div className={cn('flex items-center gap-4 w-48 cursor-default', className)}>
<span className="text-xs text-foreground-light w-10 text-right shrink-0">{label}</span>
<div className="relative w-full h-1.5 bg-surface-300 rounded-full overflow-hidden">
{isLoading && (
<ShimmeringLoader className="absolute inset-0 h-full w-full rounded-full !bg-surface-300" />
)}
{!isLoading && !error && (
<div
className={cn('absolute left-0 top-0 h-full rounded-full', barColor)}
style={{ width: barWidth }}
/>
)}
{!isLoading && error && (
<div className="absolute inset-0 flex items-center justify-center">
<WifiOff size={10} strokeWidth={1.5} className="text-destructive" />
</div>
)}
</div>
<span className="text-xs font-mono w-8 text-right">
{isLoading ? '...' : displayValue}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{isLoading
? `Loading ${label} usage...`
: error
? `Failed to load ${label} usage: ${error.message}`
: `${label} Usage: ${usage !== undefined ? `${usage.toFixed(2)}${unit}` : 'N/A'}`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
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 (
<div className="flex flex-col gap-1.5">
<UsageBar
label="Disk"
usage={diskUsagePercent}
isLoading={isLoadingDiskUtil}
error={diskUtilError as Error | null}
warningThreshold={80} // Adjusted thresholds for disk
dangerThreshold={95}
/>
<UsageBar
label="Memory"
usage={latestMemoryUsage ? Number(latestMemoryUsage) : undefined}
isLoading={isLoadingMemoryUsage}
error={memoryUsageError as Error | null}
/>
<UsageBar
label="CPU"
usage={latestCpuUsage ? Number(latestCpuUsage) : undefined}
isLoading={isLoadingCpuUsage}
error={cpuUsageError as Error | null}
/>
<UsageBar
label="IO"
usage={latestIoBudgetUsage ? Number(latestIoBudgetUsage) : undefined}
isLoading={isLoadingIoBudget}
error={ioBudgetError as Error | null}
/>
</div>
)
}
+86 -32
View File
@@ -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 (
<div className="mx-auto w-full mb-12 md:mb-16 max-w-full">
<div className="relative pt-16">
{project?.status !== PROJECT_STATUS.INACTIVE && project?.cloud_provider !== 'FLY' && (
{/* Apply dynamic opacity and scale to the sticky header with transition */}
<div
className="relative pt-12 bg-gradient-to-b from-background-surface-100/50 to-background-surface-57 top-0 transition-all duration-200 ease-out origin-top"
style={{
opacity: headerOpacity,
transform: `scale(${headerScale})`,
transformOrigin: 'top', // Ensure scaling originates from the top
}}
>
{/* {project?.status !== PROJECT_STATUS.INACTIVE && project?.cloud_provider !== 'FLY' && (
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-background-surface-75" />
<InstanceDiagram height={300} />
</div>
)}
<div className="relative z-10 flex flex-col md:flex-row md:items-center gap-3 max-w-7xl mx-auto w-full mb-6">
<h1 className="relative z-10">{projectName}</h1>
{isOrioleDb && (
<Tooltip>
<TooltipTrigger>
<Badge variant="warning">OrioleDB</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-80 text-center">
This project is using Postgres with OrioleDB which is currently in preview and not
suitable for production workloads. View our{' '}
<InlineLink href="https://supabase.com/docs/guides/database/orioledb">
documentation
</InlineLink>{' '}
for all limitations.
</TooltipContent>
</Tooltip>
)}
<ComputeBadgeWrapper
project={{
ref: project?.ref,
organization_slug: organization?.slug,
cloud_provider: project?.cloud_provider,
infra_compute_size: project?.infra_compute_size,
}}
/>
)} */}
<div className="relative z-10 flex flex-row items-start justify-between gap-3 max-w-7xl mx-auto w-full mb-6">
{/* Left side: Title and Badges */}
<div className="flex flex-col md:flex-row md:items-center gap-3">
<h1 className="relative z-10">{projectName}</h1>
<div className="flex items-center gap-x-3">
{isOrioleDb && (
<Tooltip>
<TooltipTrigger>
<Badge variant="warning">OrioleDB</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" align="start" className="max-w-80 text-center">
This project is using Postgres with OrioleDB which is currently in preview and
not suitable for production workloads. View our{' '}
<InlineLink href="https://supabase.com/docs/guides/database/orioledb">
documentation
</InlineLink>{' '}
for all limitations.
</TooltipContent>
</Tooltip>
)}
<ComputeBadgeWrapper
project={{
ref: project?.ref,
organization_slug: organization?.slug,
cloud_provider: project?.cloud_provider,
infra_compute_size: project?.infra_compute_size,
}}
/>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto w-full mb-6 relative z-10">
<svg
@@ -231,7 +285,7 @@ const Home: NextPageWithLayout = () => {
{project?.status !== PROJECT_STATUS.INACTIVE && (
<>
<div className="max-w-7xl mx-auto w-full mt-12">
<div className="max-w-7xl mx-auto w-full mt-12 relative z-10 bg-surface-75">
<GettingStarted projectRef={project?.ref!} />
{/* [Charmer End] Getting Started guide */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
@@ -331,7 +385,7 @@ const Home: NextPageWithLayout = () => {
)}
{!isLoadingReports && reportContent && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{reportContent.layout
.sort((a, b) => {
if (a.y !== b.y) {
+3 -1
View File
@@ -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)
}}
>
<CartesianGrid vertical={false} />
<YAxis
tick={false}
width={0}