DiskUsage.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import MotionNumber from '@number-flow/react'
  2. import Link from 'next/link'
  3. import { useMemo } from 'react'
  4. import { Alert, AlertDescription, AlertTitle, Button, CriticalIcon } from 'ui'
  5. import { InfoTooltip } from 'ui-patterns/info-tooltip'
  6. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  7. import { SectionContent } from '../SectionContent'
  8. import { CategoryAttribute } from '../Usage.constants'
  9. import AlertError from '@/components/ui/AlertError'
  10. import Panel from '@/components/ui/Panel'
  11. import { PricingMetric } from '@/data/analytics/org-daily-stats-query'
  12. import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
  13. import type { OrgSubscription } from '@/data/subscriptions/types'
  14. import { OrgUsageResponse } from '@/data/usage/org-usage-query'
  15. import { PROJECT_STATUS } from '@/lib/constants'
  16. export interface DiskUsageProps {
  17. slug: string
  18. projectRef?: string | null
  19. attribute: CategoryAttribute
  20. subscription?: OrgSubscription
  21. usage?: OrgUsageResponse
  22. currentBillingCycleSelected: boolean
  23. }
  24. export const DiskUsage = ({
  25. slug,
  26. projectRef,
  27. attribute,
  28. subscription,
  29. usage,
  30. currentBillingCycleSelected,
  31. }: DiskUsageProps) => {
  32. const {
  33. data,
  34. isError,
  35. isPending: isLoading,
  36. isSuccess,
  37. error,
  38. } = useOrgProjectsInfiniteQuery({ slug }, { enabled: currentBillingCycleSelected })
  39. const projects = useMemo(() => data?.pages.flatMap((page) => page.projects) || [], [data?.pages])
  40. const relevantProjects = useMemo(() => {
  41. return isSuccess
  42. ? projects
  43. .filter((project) => {
  44. // We do want to show branches that are exceeding the 8 GB limit, as people could have persistent or very long-living branches
  45. const isBranchExceedingFreeQuota =
  46. project.is_branch && project.databases.some((db) => (db.disk_volume_size_gb ?? 8) > 8)
  47. const isActiveProject = project.status !== PROJECT_STATUS.INACTIVE
  48. const isHostedOnAws = project.databases.every((db) => db.cloud_provider === 'AWS')
  49. return (
  50. (!project.is_branch || isBranchExceedingFreeQuota) && isActiveProject && isHostedOnAws
  51. )
  52. })
  53. .filter((it) => it.ref === projectRef || !projectRef)
  54. : []
  55. // eslint-disable-next-line react-hooks/exhaustive-deps
  56. }, [isSuccess, projects, projectRef])
  57. const hasProjectsExceedingDiskSize = useMemo(() => {
  58. return relevantProjects.some((it) =>
  59. it.databases.some(
  60. (db) => db.type === 'READ_REPLICA' || (db.disk_volume_size_gb && db.disk_volume_size_gb > 8)
  61. )
  62. )
  63. }, [relevantProjects])
  64. const gp3UsageInPeriod = usage?.usages.find(
  65. (it) => it.metric === PricingMetric.DISK_SIZE_GB_HOURS_GP3
  66. )
  67. const io2UsageInPeriod = usage?.usages.find(
  68. (it) => it.metric === PricingMetric.DISK_SIZE_GB_HOURS_IO2
  69. )
  70. return (
  71. <div id={attribute.anchor} className="scroll-my-12">
  72. <SectionContent section={attribute}>
  73. {isLoading && (
  74. <div className="space-y-2">
  75. <ShimmeringLoader />
  76. <ShimmeringLoader className="w-3/4" />
  77. <ShimmeringLoader className="w-1/2" />
  78. </div>
  79. )}
  80. {isError && <AlertError subject="Failed to retrieve usage data" error={error} />}
  81. {isSuccess && (
  82. <div className="space-y-4">
  83. {currentBillingCycleSelected &&
  84. subscription?.usage_billing_enabled === false &&
  85. hasProjectsExceedingDiskSize && (
  86. <Alert variant="warning">
  87. <CriticalIcon />
  88. <AlertTitle>Projects exceeding quota</AlertTitle>
  89. <AlertDescription>
  90. You have projects that are exceeding 8 GB of provisioned disk size, but do not
  91. allow any overages with the Spend Cap on. Reduce the disk size or disable the
  92. spend cap.
  93. </AlertDescription>
  94. </Alert>
  95. )}
  96. <div>
  97. <div className="flex items-center justify-between">
  98. <div className="flex items-center space-x-4">
  99. <p className="text-sm">{attribute.name} usage</p>
  100. </div>
  101. </div>
  102. <div className="flex items-center justify-between border-b py-1">
  103. <p className="text-xs text-foreground-light">
  104. Included in {subscription?.plan?.name} Plan
  105. </p>
  106. <p className="text-xs">8 GB GP3 disk per project</p>
  107. </div>
  108. <div className="flex items-center justify-between">
  109. <p className="text-xs text-foreground-light">Overages in period</p>
  110. <p className="text-xs">
  111. {(gp3UsageInPeriod?.usage ?? 0).toLocaleString()} GP3 GB-Hrs
  112. {io2UsageInPeriod?.usage
  113. ? ` / ${io2UsageInPeriod.usage.toLocaleString()} IO2 GB-Hrs`
  114. : ``}
  115. </p>
  116. </div>
  117. </div>
  118. {currentBillingCycleSelected ? (
  119. <div className="space-y-4">
  120. <div className="space-y-1">
  121. <p className="text-sm">Current disk size per project</p>
  122. <p className="text-sm text-foreground-light">
  123. Breakdown of disk per project. Head to your project's disk management section to
  124. see database size used.
  125. </p>
  126. </div>
  127. {relevantProjects.length === 0 && (
  128. <Panel>
  129. <Panel.Content>
  130. <div className="flex flex-col items-center justify-center">
  131. <p className="text-sm">No active projects</p>
  132. <p className="text-sm text-foreground-light">
  133. You don't have any active projects in this organization.
  134. </p>
  135. </div>
  136. </Panel.Content>
  137. </Panel>
  138. )}
  139. {relevantProjects.map((project, idx) => {
  140. const primaryDiskUsage = project.databases
  141. .filter((it) => it.type === 'PRIMARY')
  142. .reduce((acc, curr) => acc + (curr.disk_volume_size_gb ?? 8), 0)
  143. const replicaDbs = project.databases.filter((it) => it.type !== 'PRIMARY')
  144. const replicaDiskUsage = replicaDbs.reduce(
  145. (acc, curr) => acc + (curr.disk_volume_size_gb ?? 8),
  146. 0
  147. )
  148. const totalDiskUsage = primaryDiskUsage + replicaDiskUsage
  149. return (
  150. <div
  151. key={`usage-project-${project.ref}`}
  152. className={idx !== relevantProjects.length - 1 ? 'border-b pb-2' : ''}
  153. >
  154. <div className="flex justify-between">
  155. <span className="text-foreground-light flex items-center gap-2">
  156. {project.name}
  157. </span>
  158. <Button asChild type="default" size={'tiny'}>
  159. <Link href={`/project/${project.ref}/settings/compute-and-disk`}>
  160. Manage Disk
  161. </Link>
  162. </Button>
  163. </div>
  164. <div className="flex flex-col gap-2">
  165. <div className="flex items-center h-6 gap-3">
  166. <span className="text-foreground-light text-sm font-mono flex items-center gap-2">
  167. <span className="text-foreground font-semibold mt-[-2px]">
  168. <MotionNumber
  169. value={totalDiskUsage}
  170. style={{ lineHeight: 0.8 }}
  171. className="font-mono"
  172. />
  173. </span>{' '}
  174. GB Disk provisioned
  175. </span>
  176. <InfoTooltip side="top">
  177. <p>{primaryDiskUsage} GB for Primary Database</p>
  178. {replicaDbs.length > 0 && (
  179. <>
  180. <p>
  181. {replicaDiskUsage} GB for {replicaDbs.length} Read{' '}
  182. {replicaDbs.length === 1 ? 'Replica' : 'Replicas'}
  183. </p>
  184. <p className="mt-1">
  185. Read replicas have their own disk and use 25% more disk to account
  186. for WAL files.
  187. </p>
  188. </>
  189. )}
  190. </InfoTooltip>
  191. </div>
  192. </div>
  193. </div>
  194. )
  195. })}
  196. </div>
  197. ) : (
  198. <Panel>
  199. <Panel.Content>
  200. <div className="flex flex-col items-center justify-center">
  201. <p className="text-sm">Data not available</p>
  202. <p className="text-sm text-foreground-light">
  203. Switch to current billing cycle to see current disk size per project.
  204. </p>
  205. </div>
  206. </Panel.Content>
  207. </Panel>
  208. )}
  209. </div>
  210. )}
  211. </SectionContent>
  212. </div>
  213. )
  214. }