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