mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
plan usage
This commit is contained in:
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>
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user