DiskManagementReadReplicas.tsx 8.7 KB

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