DiskManagementReadReplicas.tsx244 lines · main
1import { useParams } from 'common'
2import { AnimatePresence, motion } from 'framer-motion'
3import { Alert, AlertDescription, AlertTitle, InfoIcon } from 'ui'
4
5import { BillingChangeBadge } from './BillingChangeBadge'
6import { DISK_LIMITS, DISK_PRICING, DiskType } from './DiskManagement.constants'
7import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
8import { formatDatabaseID } from '@/data/read-replicas/replicas.utils'
9
10interface DiskManagementDiskSizeReadReplicasProps {
11 isDirty: boolean
12 totalSize: number
13 usedSize: number
14 newTotalSize: number
15 oldStorageType: DiskType
16 newStorageType: DiskType
17}
18
19export const DiskManagementDiskSizeReadReplicas = ({
20 isDirty,
21 totalSize,
22 usedSize: _usedSize,
23 newTotalSize,
24 oldStorageType,
25 newStorageType,
26}: DiskManagementDiskSizeReadReplicasProps) => {
27 const { ref: projectRef } = useParams()
28
29 const { data: databases } = useReadReplicasQuery({ projectRef })
30 const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
31 const beforePrice = totalSize * DISK_PRICING[oldStorageType]?.storage
32 const afterPrice = newTotalSize * DISK_PRICING[newStorageType]?.storage
33
34 if (readReplicas.length === 0) return null
35
36 return (
37 <>
38 <AnimatePresence>
39 {isDirty && (
40 <motion.div
41 initial={{ opacity: 0, height: 0 }}
42 animate={{ opacity: 1, height: 'auto' }}
43 exit={{ opacity: 0, height: 0 }}
44 transition={{ duration: 0.3 }}
45 >
46 <Alert variant="default" className="bg-transparent">
47 <InfoIcon />
48 <AlertTitle>
49 Read replicas are provisioned with extra 25% disk size to account for WAL files
50 </AlertTitle>
51 <AlertDescription>
52 Each replica will have a disk size of {newTotalSize}GB, and are billed separately
53 <ul className="list-disc pl-4 my-3 flex flex-col gap-2">
54 {readReplicas.map((replica, index) => (
55 <li key={index} className="marker:text-foreground-light">
56 <div className="flex items-center gap-2">
57 <span>
58 ID: {formatDatabaseID(replica.identifier)} ({replica.region}):
59 </span>
60 <BillingChangeBadge
61 show
62 beforePrice={beforePrice}
63 afterPrice={afterPrice}
64 />
65 </div>
66 </li>
67 ))}
68 </ul>
69 </AlertDescription>
70 </Alert>
71 </motion.div>
72 )}
73 </AnimatePresence>
74 {/* Hide for now until we have the utilization for each RR specifically */}
75 {/* <Collapsible open={isOpen} onOpenChange={setIsOpen}>
76 <CollapsibleTrigger asChild>
77 <div className="flex items-center cursor-pointer rounded gap-2 mt-3 text-foreground-light hover:text-foreground data-open:text-foreground group">
78 <h3 className="text-sm">Read replica disk size information</h3>
79 <ChevronDown
80 className={`h-4 w-4 transition-transform duration-200 group-data-open:transform group-data-open:rotate-180 group-data-open:text-foreground`}
81 />
82 </div>
83 </CollapsibleTrigger>
84 <AnimatePresence initial={false}>
85 {isOpen && (
86 <motion.div
87 key="content"
88 initial="collapsed"
89 animate="open"
90 exit="collapsed"
91 variants={{
92 open: { opacity: 1, height: 'auto' },
93 collapsed: { opacity: 0, height: 0 },
94 }}
95 transition={{ duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] }}
96 >
97 <CollapsibleContent className="pt-3">
98 <motion.div
99 initial={{ opacity: 0 }}
100 animate={{ opacity: 1 }}
101 exit={{ opacity: 0 }}
102 transition={{ duration: 0.2 }}
103 >
104 <p className="flex flex-col gap-y-1 text-sm text-foreground-light mb-3">
105 <span className="text-foreground-lighter">
106 Read replicas have 25% more disk size than the primary database to account for
107 WAL files{' '}
108 </span>
109 </p>
110 <DiskSpaceBar
111 showNewBar={isDirty}
112 totalSize={totalSize}
113 usedSize={usedSize}
114 newTotalSize={newTotalSize}
115 />
116 </motion.div>
117 </CollapsibleContent>
118 </motion.div>
119 )}
120 </AnimatePresence>
121 </Collapsible> */}
122 </>
123 )
124}
125
126export const DiskManagementIOPSReadReplicas = ({
127 isDirty,
128 oldIOPS,
129 newIOPS,
130 oldStorageType,
131 newStorageType,
132}: {
133 isDirty: boolean
134 oldIOPS: number
135 newIOPS: number
136 oldStorageType: DiskType
137 newStorageType: DiskType
138}) => {
139 const { ref: projectRef } = useParams()
140 const { data: databases } = useReadReplicasQuery({ projectRef })
141 const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
142
143 const beforePrice =
144 (oldIOPS - DISK_LIMITS[oldStorageType]?.includedIops) * DISK_PRICING[oldStorageType]?.iops
145 const afterPrice =
146 (newIOPS - DISK_LIMITS[newStorageType]?.includedIops) * DISK_PRICING[newStorageType]?.iops
147
148 if (readReplicas.length === 0) return null
149
150 return (
151 <AnimatePresence initial={false}>
152 {isDirty && (
153 <motion.div
154 initial={{ opacity: 0, height: 0 }}
155 animate={{ opacity: 1, height: 'auto' }}
156 exit={{ opacity: 0, height: 0 }}
157 transition={{ duration: 0.3 }}
158 >
159 <Alert variant="default" className="bg-transparent">
160 <InfoIcon />
161 <AlertTitle>Read replica IOPS will also be updated to the same value</AlertTitle>
162 <AlertDescription>
163 <ul className="list-disc pl-4 my-3 flex flex-col gap-2">
164 {readReplicas.map((replica, index) => (
165 <li key={index} className="marker:text-foreground-light">
166 <div className="flex items-center gap-2">
167 <span>
168 ID: {formatDatabaseID(replica.identifier)} ({replica.region}):
169 </span>
170 <BillingChangeBadge show beforePrice={beforePrice} afterPrice={afterPrice} />
171 </div>
172 </li>
173 ))}
174 </ul>
175 </AlertDescription>
176 </Alert>
177 </motion.div>
178 )}
179 </AnimatePresence>
180 )
181}
182
183export const DiskManagementThroughputReadReplicas = ({
184 isDirty,
185 oldThroughput,
186 newThroughput,
187 oldStorageType,
188 newStorageType,
189}: {
190 isDirty: boolean
191 oldThroughput: number
192 newThroughput: number
193 oldStorageType: DiskType
194 newStorageType: DiskType
195}) => {
196 const { ref: projectRef } = useParams()
197 const { data: databases } = useReadReplicasQuery({ projectRef })
198 const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
199
200 const beforePrice =
201 oldStorageType === DiskType.GP3
202 ? (oldThroughput - DISK_LIMITS[oldStorageType].includedThroughput) *
203 DISK_PRICING[oldStorageType]?.throughput
204 : 0
205 const afterPrice =
206 newStorageType === DiskType.GP3
207 ? (newThroughput - DISK_LIMITS[newStorageType].includedThroughput) *
208 DISK_PRICING[newStorageType]?.throughput
209 : 0
210
211 if (readReplicas.length === 0) return null
212
213 return (
214 <AnimatePresence initial={false}>
215 {isDirty && (
216 <motion.div
217 initial={{ opacity: 0, height: 0 }}
218 animate={{ opacity: 1, height: 'auto' }}
219 exit={{ opacity: 0, height: 0 }}
220 transition={{ duration: 0.3 }}
221 >
222 <Alert variant="default" className="bg-transparent">
223 <InfoIcon />
224 <AlertTitle>Read replica throughput will also be updated to the same value</AlertTitle>
225 <AlertDescription>
226 <ul className="list-disc pl-4 my-3 flex flex-col gap-2">
227 {readReplicas.map((replica, index) => (
228 <li key={index} className="marker:text-foreground-light">
229 <div className="flex items-center gap-2">
230 <span>
231 ID: {formatDatabaseID(replica.identifier)} ({replica.region}):
232 </span>
233 <BillingChangeBadge show beforePrice={beforePrice} afterPrice={afterPrice} />
234 </div>
235 </li>
236 ))}
237 </ul>
238 </AlertDescription>
239 </Alert>
240 </motion.div>
241 )}
242 </AnimatePresence>
243 )
244}