DiskSpaceBar.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import MotionNumber from '@number-flow/react'
  2. import { useParams } from 'common'
  3. import { AnimatePresence, motion } from 'framer-motion'
  4. import { Info } from 'lucide-react'
  5. import { useTheme } from 'next-themes'
  6. import { useMemo } from 'react'
  7. import { UseFormReturn } from 'react-hook-form'
  8. import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  9. import { DiskStorageSchemaType } from '../DiskManagement.schema'
  10. import { AUTOSCALING_THRESHOLD } from './DiskManagement.constants'
  11. import { useDiskBreakdownQuery } from '@/data/config/disk-breakdown-query'
  12. import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
  13. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  14. import { GB } from '@/lib/constants'
  15. import { formatBytes } from '@/lib/helpers'
  16. interface DiskSpaceBarProps {
  17. form: UseFormReturn<DiskStorageSchemaType>
  18. }
  19. export const DiskSpaceBar = ({ form }: DiskSpaceBarProps) => {
  20. const { ref } = useParams()
  21. const { resolvedTheme } = useTheme()
  22. const { formState, watch } = form
  23. const isDarkMode = resolvedTheme?.includes('dark')
  24. const { data: project } = useSelectedProjectQuery()
  25. const {
  26. data: diskUtil,
  27. // to do, error handling
  28. } = useDiskUtilizationQuery({
  29. projectRef: ref,
  30. })
  31. const { data: diskBreakdown } = useDiskBreakdownQuery({
  32. projectRef: ref,
  33. connectionString: project?.connectionString,
  34. })
  35. const diskBreakdownBytes = useMemo(() => {
  36. return {
  37. availableBytes: diskUtil?.metrics.fs_avail_bytes ?? 0,
  38. totalUsedBytes: diskUtil?.metrics.fs_used_bytes ?? 0,
  39. totalDiskSizeBytes: diskUtil?.metrics.fs_size_bytes,
  40. dbSizeBytes: Math.max(0, diskBreakdown?.db_size_bytes ?? 0),
  41. walSizeBytes: Math.max(0, diskBreakdown?.wal_size_bytes ?? 0),
  42. systemBytes: Math.max(
  43. 0,
  44. (diskUtil?.metrics.fs_used_bytes ?? 0) -
  45. (diskBreakdown?.db_size_bytes ?? 0) -
  46. (diskBreakdown?.wal_size_bytes ?? 0)
  47. ),
  48. }
  49. }, [diskUtil, diskBreakdown])
  50. const showNewSize = formState.dirtyFields.totalSize !== undefined && diskBreakdown
  51. const newTotalSize = watch('totalSize')
  52. const totalSize = formState.defaultValues?.totalSize || 0
  53. const usedSizeTotal = Math.round(((diskBreakdownBytes?.totalUsedBytes ?? 0) / GB) * 100) / 100
  54. const usedTotalPercentage = Math.min((usedSizeTotal / totalSize) * 100, 100)
  55. const usedSizeDatabase = Math.round(((diskBreakdownBytes?.dbSizeBytes ?? 0) / GB) * 100) / 100
  56. const usedPercentageDatabase =
  57. totalSize === 0 ? 0 : Math.min((usedSizeDatabase / totalSize) * 100, 100)
  58. const newUsedPercentageDatabase = Math.min((usedSizeDatabase / newTotalSize) * 100, 100)
  59. const usedSizeWAL = Math.round(((diskBreakdownBytes?.walSizeBytes ?? 0) / GB) * 100) / 100
  60. const usedPercentageWAL = totalSize === 0 ? 0 : Math.min((usedSizeWAL / totalSize) * 100, 100)
  61. const newUsedPercentageWAL = Math.min((usedSizeWAL / newTotalSize) * 100, 100)
  62. const usedSizeSystem = Math.round(((diskBreakdownBytes?.systemBytes ?? 0) / GB) * 100) / 100
  63. const usedPercentageSystem =
  64. totalSize === 0 ? 0 : Math.min((usedSizeSystem / totalSize) * 100, 100)
  65. const newUsedPercentageSystem = Math.min((usedSizeSystem / newTotalSize) * 100, 100)
  66. const resizePercentage = AUTOSCALING_THRESHOLD * 100
  67. const newResizePercentage = AUTOSCALING_THRESHOLD * 100
  68. return (
  69. <div className="flex flex-col gap-2">
  70. <div className="flex items-center h-6 gap-3">
  71. <span className="text-foreground-light text-sm font-mono flex items-center gap-2">
  72. {usedSizeTotal.toFixed(2)}
  73. <span>GB used of </span>
  74. <span className="text-foreground font-semibold mt-[-2px]">
  75. <MotionNumber value={newTotalSize} style={{ lineHeight: 0.8 }} className="font-mono" />
  76. </span>{' '}
  77. GB
  78. </span>
  79. </div>
  80. <div className="relative">
  81. <div
  82. className={cn(
  83. 'h-[35px] relative border rounded-xs w-full transition overflow-visible',
  84. showNewSize ? 'bg-selection border border-brand' : 'bg-surface-300'
  85. )}
  86. >
  87. <AnimatePresence>
  88. <motion.div
  89. key="currentBar"
  90. initial={{ rotateY: 90, zIndex: 2 }}
  91. animate={{ rotateY: 0, zIndex: 1 }}
  92. exit={{ rotateY: -90, zIndex: 2 }}
  93. transition={{ duration: 0.3 }}
  94. style={{ transformOrigin: 'left center', backfaceVisibility: 'hidden' }}
  95. className="absolute inset-0 rounded-xs overflow-hidden"
  96. >
  97. <div className="h-full flex">
  98. <div
  99. className="relative overflow-hidden transition-all duration-500 ease-in-out bg-foreground"
  100. style={{
  101. width: `${showNewSize ? newUsedPercentageDatabase : usedPercentageDatabase}%`,
  102. }}
  103. >
  104. <div
  105. className="absolute inset-0"
  106. style={{
  107. backgroundImage: `repeating-linear-gradient(
  108. -45deg,
  109. ${isDarkMode ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)'},
  110. ${isDarkMode ? 'rgba(0,0,0,0.1) 1px' : 'rgba(255,255,255,0.1) 1px'},
  111. transparent 1px,
  112. transparent 4px
  113. )`,
  114. }}
  115. />
  116. </div>
  117. <div
  118. className="relative overflow-hidden transition-all duration-500 ease-in-out bg-[hsl(var(--secondary-default))]"
  119. style={{
  120. width: `${showNewSize ? newUsedPercentageWAL : usedPercentageWAL}%`,
  121. }}
  122. />
  123. <div
  124. className="relative overflow-hidden transition-all duration-500 ease-in-out bg-destructive-500"
  125. style={{
  126. width: `${showNewSize ? newUsedPercentageSystem : usedPercentageSystem}%`,
  127. }}
  128. />
  129. {!showNewSize && (
  130. <div
  131. className="bg-transparent-800 border-r transition-all duration-500 ease-in-out"
  132. style={{
  133. width: `${resizePercentage - usedTotalPercentage <= 0 ? 0 : resizePercentage - usedTotalPercentage}%`,
  134. }}
  135. />
  136. )}
  137. </div>
  138. </motion.div>
  139. </AnimatePresence>
  140. <AnimatePresence>
  141. {showNewSize && (
  142. <motion.div
  143. initial={{ opacity: 0, x: 4 }}
  144. animate={{ opacity: 1, x: 0 }}
  145. exit={{ opacity: 0, x: 4 }}
  146. transition={{ duration: 0.12, delay: 0.12 }}
  147. className="absolute right-2 top-0 flex items-center h-full"
  148. >
  149. <Badge variant="success">New disk size</Badge>
  150. </motion.div>
  151. )}
  152. </AnimatePresence>
  153. </div>
  154. <AnimatePresence initial={true}>
  155. {!showNewSize && (
  156. <motion.div
  157. key="currentSize"
  158. initial={{ opacity: 0, y: -10 }}
  159. animate={{ opacity: 1, y: 0 }}
  160. exit={{ opacity: 0, y: 10 }}
  161. transition={{ duration: 0.1 }}
  162. className="absolute h-8 w-full mx-[-2px]"
  163. >
  164. <div
  165. className="absolute top-0 left-0 h-full flex items-center transition-all duration-500 ease-in-out"
  166. style={{ left: `${showNewSize ? newResizePercentage : resizePercentage}%` }}
  167. >
  168. <Tooltip>
  169. <TooltipTrigger asChild>
  170. <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">
  171. Autoscaling <Info size={12} />
  172. </div>
  173. </TooltipTrigger>
  174. <TooltipContent side="bottom" className="w-[310px] flex flex-col gap-y-1">
  175. <p>
  176. Briven expands your disk storage automatically when the database reached 90%
  177. of the disk size. However, any disk modifications, including auto-scaling, can
  178. only take place once every 4 hours.
  179. </p>
  180. <p>
  181. If within those 4 hours you reach 95% of the disk space, your project{' '}
  182. <span className="text-destructive-600">will enter read-only mode.</span>
  183. </p>
  184. </TooltipContent>
  185. </Tooltip>
  186. <div className="w-px h-full bg-border" />
  187. </div>
  188. </motion.div>
  189. )}
  190. </AnimatePresence>
  191. </div>
  192. {!showNewSize && (
  193. <div className="flex items-center space-x-3 text-xs text-foreground-lighter">
  194. <LegendItem
  195. name="Database"
  196. size={diskBreakdownBytes.dbSizeBytes}
  197. color="bg-foreground"
  198. description="Total space on disk used by your database (tables, indexes, data, ...)."
  199. />
  200. <LegendItem
  201. name="WAL"
  202. size={diskBreakdownBytes.walSizeBytes}
  203. color="bg-[hsl(var(--secondary-default))]"
  204. description="Total space on disk used by the write-ahead log."
  205. />
  206. <LegendItem
  207. name="System"
  208. size={diskBreakdownBytes.systemBytes}
  209. color="bg-destructive-500"
  210. description="Reserved space for the system to ensure your database runs smoothly. You cannot modify this."
  211. />
  212. <LegendItem
  213. name="Available space"
  214. size={diskBreakdownBytes.availableBytes}
  215. color="bg-border"
  216. description="Total available space on the disk left."
  217. />
  218. </div>
  219. )}
  220. <p className="text-xs text-foreground-lighter my-4">
  221. <span className="font-semibold">Note:</span> Disk Size refers to the total space your
  222. project occupies on disk, including the database itself (currently{' '}
  223. <span>{formatBytes(diskBreakdownBytes?.dbSizeBytes, 2, 'GB')}</span>), additional files like
  224. the write-ahead log (currently{' '}
  225. <span>{formatBytes(diskBreakdownBytes?.walSizeBytes, 2, 'GB')}</span>), and other system
  226. resources (currently <span>{formatBytes(diskBreakdownBytes?.systemBytes, 2, 'GB')}</span>).
  227. Data can take 5 minutes to refresh.
  228. </p>
  229. </div>
  230. )
  231. }
  232. const LegendItem = ({
  233. name,
  234. description,
  235. color,
  236. size,
  237. }: {
  238. name: string
  239. description: string
  240. color: string
  241. size: number
  242. }) => (
  243. <Tooltip>
  244. <TooltipTrigger asChild>
  245. <div className="flex items-center hover:cursor-help z-10">
  246. <div className={cn('w-2 h-2 rounded-full mr-2', color)} />
  247. <span>{name}</span>
  248. </div>
  249. </TooltipTrigger>
  250. <TooltipContent side="bottom" className="flex flex-col gap-y-1 max-w-xs">
  251. <div className="flex items-center">
  252. <div className={cn('w-2 h-2 rounded-full mr-2', color)} />
  253. <span>
  254. {name} - {formatBytes(size, 2, 'GB')}
  255. </span>
  256. </div>
  257. <p>{description}</p>
  258. </TooltipContent>
  259. </Tooltip>
  260. )