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