mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Small UI fix to stop things bundling into each other on smaller viewports. Autoscaling marker moves from bottom to top to preserve structural integrity. | Before | After | |--------|--------| | <img width="698" height="246" alt="Screenshot 2026-07-15 at 11 28 07" src="https://github.com/user-attachments/assets/9bbb1bb1-a361-4658-9757-30e012098d0c" /> | <img width="687" height="252" alt="Screenshot 2026-07-15 at 11 28 15" src="https://github.com/user-attachments/assets/ca6b6c0a-b511-4136-835f-4321a30765b8" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed the Autoscaling tooltip placement on the disk space bar so it consistently appears in the correct spot. * Updated the tooltip trigger to use an info icon with a static positioned tooltip, removing the previous animated trigger behavior while preserving existing tooltip content and visibility rules. * Improved tooltip/marker positioning accuracy by recalculating the horizontal placement from the independently-rounded usage segments to prevent drift. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
import MotionNumber from '@number-flow/react'
|
|
import { useParams } from 'common'
|
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { Info } from 'lucide-react'
|
|
import { useTheme } from 'next-themes'
|
|
import { useMemo } from 'react'
|
|
import { UseFormReturn } from 'react-hook-form'
|
|
import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
|
|
import { DiskStorageSchemaType } from '../DiskManagement.schema'
|
|
import { AUTOSCALING_THRESHOLD } from './DiskManagement.constants'
|
|
import { useDiskBreakdownQuery } from '@/data/config/disk-breakdown-query'
|
|
import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { GB } from '@/lib/constants'
|
|
import { formatBytes } from '@/lib/helpers'
|
|
|
|
interface DiskSpaceBarProps {
|
|
form: UseFormReturn<DiskStorageSchemaType>
|
|
}
|
|
|
|
export const DiskSpaceBar = ({ form }: DiskSpaceBarProps) => {
|
|
const { ref } = useParams()
|
|
const { resolvedTheme } = useTheme()
|
|
const { formState, watch } = form
|
|
const isDarkMode = resolvedTheme?.includes('dark')
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
const {
|
|
data: diskUtil,
|
|
// to do, error handling
|
|
} = useDiskUtilizationQuery({
|
|
projectRef: ref,
|
|
})
|
|
|
|
const { data: diskBreakdown } = useDiskBreakdownQuery({
|
|
projectRef: ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
|
|
const diskBreakdownBytes = useMemo(() => {
|
|
return {
|
|
availableBytes: diskUtil?.metrics.fs_avail_bytes ?? 0,
|
|
totalUsedBytes: diskUtil?.metrics.fs_used_bytes ?? 0,
|
|
totalDiskSizeBytes: diskUtil?.metrics.fs_size_bytes,
|
|
dbSizeBytes: Math.max(0, diskBreakdown?.db_size_bytes ?? 0),
|
|
walSizeBytes: Math.max(0, diskBreakdown?.wal_size_bytes ?? 0),
|
|
systemBytes: Math.max(
|
|
0,
|
|
(diskUtil?.metrics.fs_used_bytes ?? 0) -
|
|
(diskBreakdown?.db_size_bytes ?? 0) -
|
|
(diskBreakdown?.wal_size_bytes ?? 0)
|
|
),
|
|
}
|
|
}, [diskUtil, diskBreakdown])
|
|
|
|
const showNewSize = formState.dirtyFields.totalSize !== undefined && diskBreakdown
|
|
const newTotalSize = watch('totalSize')
|
|
|
|
const totalSize = formState.defaultValues?.totalSize || 0
|
|
const usedSizeTotal = Math.round(((diskBreakdownBytes?.totalUsedBytes ?? 0) / GB) * 100) / 100
|
|
const usedTotalPercentage = Math.min((usedSizeTotal / totalSize) * 100, 100)
|
|
|
|
const usedSizeDatabase = Math.round(((diskBreakdownBytes?.dbSizeBytes ?? 0) / GB) * 100) / 100
|
|
const usedPercentageDatabase =
|
|
totalSize === 0 ? 0 : Math.min((usedSizeDatabase / totalSize) * 100, 100)
|
|
const newUsedPercentageDatabase = Math.min((usedSizeDatabase / newTotalSize) * 100, 100)
|
|
|
|
const usedSizeWAL = Math.round(((diskBreakdownBytes?.walSizeBytes ?? 0) / GB) * 100) / 100
|
|
const usedPercentageWAL = totalSize === 0 ? 0 : Math.min((usedSizeWAL / totalSize) * 100, 100)
|
|
const newUsedPercentageWAL = Math.min((usedSizeWAL / newTotalSize) * 100, 100)
|
|
|
|
const usedSizeSystem = Math.round(((diskBreakdownBytes?.systemBytes ?? 0) / GB) * 100) / 100
|
|
const usedPercentageSystem =
|
|
totalSize === 0 ? 0 : Math.min((usedSizeSystem / totalSize) * 100, 100)
|
|
const newUsedPercentageSystem = Math.min((usedSizeSystem / newTotalSize) * 100, 100)
|
|
|
|
const resizePercentage = AUTOSCALING_THRESHOLD * 100
|
|
// Anchored to the actual right edge of the highlighted "border-r" zone below, rather than
|
|
// trusting resizePercentage alone, since usedPercentage* are each rounded independently and
|
|
// can drift a fraction of a percent from usedTotalPercentage.
|
|
const resizeMarkerLeftPercentage =
|
|
usedPercentageDatabase +
|
|
usedPercentageWAL +
|
|
usedPercentageSystem +
|
|
Math.max(0, resizePercentage - usedTotalPercentage)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center h-6 gap-3">
|
|
<span className="text-foreground-light text-sm font-mono flex items-center gap-2">
|
|
{usedSizeTotal.toFixed(2)}
|
|
<span>GB used of </span>
|
|
<span className="text-foreground font-semibold mt-[-2px]">
|
|
<MotionNumber value={newTotalSize} style={{ lineHeight: 0.8 }} className="font-mono" />
|
|
</span>{' '}
|
|
GB
|
|
</span>
|
|
</div>
|
|
<div className="relative">
|
|
<div
|
|
className={cn(
|
|
'h-[35px] relative border rounded-xs w-full transition overflow-visible',
|
|
showNewSize ? 'bg-selection border border-brand' : 'bg-surface-300'
|
|
)}
|
|
>
|
|
<AnimatePresence>
|
|
<motion.div
|
|
key="currentBar"
|
|
initial={{ rotateY: 90, zIndex: 2 }}
|
|
animate={{ rotateY: 0, zIndex: 1 }}
|
|
exit={{ rotateY: -90, zIndex: 2 }}
|
|
transition={{ duration: 0.3 }}
|
|
style={{ transformOrigin: 'left center', backfaceVisibility: 'hidden' }}
|
|
className="absolute inset-0 rounded-xs overflow-hidden"
|
|
>
|
|
<div className="h-full flex">
|
|
<div
|
|
className="relative overflow-hidden transition-all duration-500 ease-in-out bg-foreground"
|
|
style={{
|
|
width: `${showNewSize ? newUsedPercentageDatabase : usedPercentageDatabase}%`,
|
|
}}
|
|
>
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
backgroundImage: `repeating-linear-gradient(
|
|
-45deg,
|
|
${isDarkMode ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)'},
|
|
${isDarkMode ? 'rgba(0,0,0,0.1) 1px' : 'rgba(255,255,255,0.1) 1px'},
|
|
transparent 1px,
|
|
transparent 4px
|
|
)`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
className="relative overflow-hidden transition-all duration-500 ease-in-out bg-[hsl(var(--secondary-default))]"
|
|
style={{
|
|
width: `${showNewSize ? newUsedPercentageWAL : usedPercentageWAL}%`,
|
|
}}
|
|
/>
|
|
|
|
<div
|
|
className="relative overflow-hidden transition-all duration-500 ease-in-out bg-destructive-500"
|
|
style={{
|
|
width: `${showNewSize ? newUsedPercentageSystem : usedPercentageSystem}%`,
|
|
}}
|
|
/>
|
|
|
|
{!showNewSize && (
|
|
<div
|
|
className="bg-transparent-800 border-r transition-all duration-500 ease-in-out"
|
|
style={{
|
|
width: `${resizePercentage - usedTotalPercentage <= 0 ? 0 : resizePercentage - usedTotalPercentage}%`,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
<AnimatePresence>
|
|
{showNewSize && (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 4 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
exit={{ opacity: 0, x: 4 }}
|
|
transition={{ duration: 0.12, delay: 0.12 }}
|
|
className="absolute right-2 top-0 flex items-center h-full"
|
|
>
|
|
<Badge variant="success">New disk size</Badge>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
{!showNewSize && (
|
|
<div
|
|
className="absolute z-10"
|
|
style={{
|
|
left: `${resizeMarkerLeftPercentage}%`,
|
|
top: '-20px',
|
|
transform: 'translateX(-50%)',
|
|
}}
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center justify-center cursor-help text-foreground-lighter hover:text-foreground transition-colors">
|
|
<Info size={14} />
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent
|
|
side="top"
|
|
className="w-[310px] flex flex-col gap-y-1 leading-relaxed text-foreground-light"
|
|
>
|
|
<h3 className="text-sm font-medium text-foreground">Autoscaling</h3>
|
|
<p>
|
|
Supabase expands your disk storage automatically when the database reaches 90%
|
|
of the disk size. However, disk modifications, including auto-scaling, are
|
|
limited to 4 within a rolling 24-hour window.
|
|
</p>
|
|
<p>
|
|
If you exhaust these modifications and reach 95% of the disk space, your project{' '}
|
|
<span className="text-destructive-600">will enter read-only mode.</span>
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{!showNewSize && (
|
|
<div className="flex items-center space-x-3 text-xs text-foreground-lighter">
|
|
<LegendItem
|
|
name="Database"
|
|
size={diskBreakdownBytes.dbSizeBytes}
|
|
color="bg-foreground"
|
|
description="Total space on disk used by your database (tables, indexes, data, ...)."
|
|
/>
|
|
<LegendItem
|
|
name="WAL"
|
|
size={diskBreakdownBytes.walSizeBytes}
|
|
color="bg-[hsl(var(--secondary-default))]"
|
|
description="Total space on disk used by the write-ahead log."
|
|
/>
|
|
|
|
<LegendItem
|
|
name="System"
|
|
size={diskBreakdownBytes.systemBytes}
|
|
color="bg-destructive-500"
|
|
description="Reserved space for the system to ensure your database runs smoothly. You cannot modify this."
|
|
/>
|
|
|
|
<LegendItem
|
|
name="Available space"
|
|
size={diskBreakdownBytes.availableBytes}
|
|
color="bg-border"
|
|
description="Total available space on the disk left."
|
|
/>
|
|
</div>
|
|
)}
|
|
<p className="text-xs text-foreground-lighter my-4">
|
|
<span className="font-semibold">Note:</span> Disk Size refers to the total space your
|
|
project occupies on disk, including the database itself (currently{' '}
|
|
<span>{formatBytes(diskBreakdownBytes?.dbSizeBytes, 2, 'GB')}</span>), additional files like
|
|
the write-ahead log (currently{' '}
|
|
<span>{formatBytes(diskBreakdownBytes?.walSizeBytes, 2, 'GB')}</span>), and other system
|
|
resources (currently <span>{formatBytes(diskBreakdownBytes?.systemBytes, 2, 'GB')}</span>).
|
|
Data can take 5 minutes to refresh.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const LegendItem = ({
|
|
name,
|
|
description,
|
|
color,
|
|
size,
|
|
}: {
|
|
name: string
|
|
description: string
|
|
color: string
|
|
size: number
|
|
}) => (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center hover:cursor-help z-10">
|
|
<div className={cn('w-2 h-2 rounded-full mr-2', color)} />
|
|
<span>{name}</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="flex flex-col gap-y-1 max-w-xs">
|
|
<div className="flex items-center">
|
|
<div className={cn('w-2 h-2 rounded-full mr-2', color)} />
|
|
<span>
|
|
{name} - {formatBytes(size, 2, 'GB')}
|
|
</span>
|
|
</div>
|
|
<p>{description}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|