| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- 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
- const newResizePercentage = AUTOSCALING_THRESHOLD * 100
- 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>
- </div>
- <AnimatePresence initial={true}>
- {!showNewSize && (
- <motion.div
- key="currentSize"
- initial={{ opacity: 0, y: -10 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: 10 }}
- transition={{ duration: 0.1 }}
- className="absolute h-8 w-full mx-[-2px]"
- >
- <div
- className="absolute top-0 left-0 h-full flex items-center transition-all duration-500 ease-in-out"
- style={{ left: `${showNewSize ? newResizePercentage : resizePercentage}%` }}
- >
- <Tooltip>
- <TooltipTrigger asChild>
- <div className="absolute right-full bottom-0 border mr-2 px-2 py-1 bg-surface-400 rounded-sm text-xs text-foreground-light whitespace-nowrap flex items-center gap-x-1">
- Autoscaling <Info size={12} />
- </div>
- </TooltipTrigger>
- <TooltipContent side="bottom" className="w-[310px] flex flex-col gap-y-1">
- <p>
- Briven expands your disk storage automatically when the database reached 90%
- of the disk size. However, any disk modifications, including auto-scaling, can
- only take place once every 4 hours.
- </p>
- <p>
- If within those 4 hours you reach 95% of the disk space, your project{' '}
- <span className="text-destructive-600">will enter read-only mode.</span>
- </p>
- </TooltipContent>
- </Tooltip>
- <div className="w-px h-full bg-border" />
- </div>
- </motion.div>
- )}
- </AnimatePresence>
- </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>
- )
|