Restriction.tsx222 lines · main
1import dayjs from 'dayjs'
2import { ExternalLink } from 'lucide-react'
3import Link from 'next/link'
4import { usePathname } from 'next/navigation'
5import { Alert, AlertDescription, AlertTitle, Button, CriticalIcon, WarningIcon } from 'ui'
6
7import { PricingMetric } from '@/data/analytics/org-daily-stats-query'
8import { VIOLATION_TYPE_LABELS } from '@/data/usage/constants'
9import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
10import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
11import { DOCS_URL } from '@/lib/constants'
12
13export const Restriction = () => {
14 const { data: org } = useSelectedOrganizationQuery()
15 const { data: usage, isSuccess: isSuccessOrgUsage } = useOrgUsageQuery({ orgSlug: org?.slug })
16
17 const pathname = usePathname()
18 const isUsagePage = pathname?.endsWith('/usage')
19
20 const hasExceededAnyLimits = Boolean(
21 usage?.usages.find(
22 (metric) =>
23 metric.metric !== PricingMetric.DISK_SIZE_GB_HOURS_GP3 &&
24 !metric.unlimited &&
25 metric.capped &&
26 metric.usage > (metric?.pricing_free_units ?? 0)
27 )
28 )
29
30 // don't show any alerts until everything has been fetched
31 if (!isSuccessOrgUsage || !org) {
32 return null
33 }
34
35 let shownAlert: 'exceededLimits' | 'gracePeriod' | 'gracePeriodOver' | 'restricted' | null = null
36
37 if (hasExceededAnyLimits && !org?.restriction_status) {
38 shownAlert = 'exceededLimits'
39 } else if (org?.restriction_status === 'grace_period') {
40 shownAlert = 'gracePeriod'
41 } else if (org?.restriction_status === 'grace_period_over') {
42 shownAlert = 'gracePeriodOver'
43 } else if (org?.restriction_status === 'restricted') {
44 shownAlert = 'restricted'
45 }
46
47 if (shownAlert === null || !org?.restriction_data) {
48 return null
49 }
50
51 const violationLabels =
52 Array.isArray(org.restriction_data['violations']) &&
53 org.restriction_data['violations'].length > 0
54 ? `(${org.restriction_data['violations']
55 .map((violation: string) => VIOLATION_TYPE_LABELS[violation] || violation)
56 .join(', ')})`
57 : ''
58
59 return (
60 <>
61 {shownAlert === 'exceededLimits' && (
62 <Alert variant="destructive">
63 <CriticalIcon />
64
65 <AlertTitle>Your organization's usage has exceeded its included quota</AlertTitle>
66 <AlertDescription>
67 <p>
68 Your projects can become unresponsive or enter read-only mode.{' '}
69 {org.plan.id === 'free'
70 ? 'Please upgrade to the Pro Plan to ensure that your projects remain available.'
71 : 'Please disable spend cap to ensure that your projects remain available.'}
72 </p>
73 <div className="flex items-center gap-x-2 mt-3">
74 <Button key="upgrade-button" asChild type="default">
75 <Link
76 href={`/org/${org?.slug}/billing?panel=${
77 org.plan.id === 'free' ? 'subscriptionPlan' : 'costControl'
78 }`}
79 >
80 {org.plan.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}
81 </Link>
82 </Button>
83 {!isUsagePage && (
84 <Button key="view-usage-button" asChild type="default">
85 <Link href={`/org/${org?.slug}/usage`}>View usage</Link>
86 </Button>
87 )}
88 <Button asChild type="default" icon={<ExternalLink />}>
89 <a href={`${DOCS_URL}/guides/platform/cost-control#spend-cap`}>About spend cap</a>
90 </Button>
91 </div>
92 </AlertDescription>
93 </Alert>
94 )}
95 {shownAlert === 'gracePeriod' && (
96 <Alert variant="warning">
97 <WarningIcon />
98 <AlertTitle>Your grace period has started.</AlertTitle>
99 <AlertDescription>
100 <p className="leading-tight">
101 Your organization went over its quota in the previous billing cycle
102 {violationLabels && ` ${violationLabels}`}. You can continue with your projects until
103 your grace period ends on{' '}
104 <span className="text-foreground">
105 {dayjs(org.restriction_data['grace_period_end']).format('DD MMM, YYYY')}
106 </span>
107 . After that, the Fair Use Policy will apply. If you plan to maintain this level of
108 usage, {org.plan.id === 'free' ? 'upgrade your plan' : 'disable spend cap'} to avoid
109 any restrictions. If restrictions are applied, requests to your projects will return a
110 402 status code.
111 </p>
112 <div className="flex items-center gap-x-2 mt-3">
113 <Button asChild key="upgrade-button" type="default">
114 <Link
115 href={`/org/${org?.slug}/billing?panel=${
116 org.plan.id === 'free'
117 ? 'subscriptionPlan&source=fairUseGracePeriodStarted'
118 : 'costControl&source=fairUseGracePeriodStarted'
119 }`}
120 >
121 {org.plan.id === 'free' ? 'Upgrade plan' : 'Disable spend cap'}
122 </Link>
123 </Button>
124
125 {!isUsagePage && (
126 <Button key="view-usage-button" asChild type="default">
127 <Link href={`/org/${org?.slug}/usage`}>View usage</Link>
128 </Button>
129 )}
130
131 <Button asChild type="default" icon={<ExternalLink />}>
132 <a href={`${DOCS_URL}/guides/platform/billing-faq#fair-use-policy`}>
133 About Fair Use Policy
134 </a>
135 </Button>
136 </div>
137 </AlertDescription>
138 </Alert>
139 )}
140 {shownAlert === 'gracePeriodOver' && (
141 <Alert variant="warning">
142 <WarningIcon />
143 <AlertTitle>Your grace period is over.</AlertTitle>
144 <AlertDescription>
145 <p>
146 Your grace period ended on{' '}
147 <span className="text-foreground">
148 {dayjs(org.restriction_data['grace_period_end']).format('DD MMM, YYYY')}
149 </span>
150 . Fair Use Policy applies now. Stay below your plan's quota or{' '}
151 {org.plan.id === 'free' ? 'upgrade your plan' : 'disable spend cap'} if you expect to
152 exceed it. If you exceed your quota, requests will respond with a 402 status code.
153 </p>
154 <div className="flex items-center gap-x-2 mt-3">
155 <Button key="upgrade-button" asChild type="default">
156 <Link
157 href={`/org/${org?.slug}/billing?panel=${
158 org.plan.id === 'free'
159 ? 'subscriptionPlan&source=fairUseGracePeriodOver'
160 : 'costControl&source=fairUseGracePeriodOver'
161 }`}
162 >
163 {org.plan.id === 'free' ? 'Upgrade plan' : 'Disable spend cap'}
164 </Link>
165 </Button>
166 {!isUsagePage && (
167 <Button key="view-usage-button" asChild type="default">
168 <Link href={`/org/${org?.slug}/usage`}>View usage</Link>
169 </Button>
170 )}
171 <Button asChild type="default" icon={<ExternalLink />}>
172 <a href={`${DOCS_URL}/guides/platform/billing-faq#fair-use-policy`}>
173 About Fair Use Policy
174 </a>
175 </Button>
176 </div>
177 </AlertDescription>
178 </Alert>
179 )}
180 {shownAlert === 'restricted' && (
181 <Alert variant="destructive">
182 <CriticalIcon />
183 <AlertTitle>All services are restricted.</AlertTitle>
184 <AlertDescription>
185 <p>
186 Fair Use Policy applies and your service is restricted. Your projects are not able to
187 serve requests and will respond with a 402 status code. You have exceeded your plan's
188 quota{violationLabels && ` ${violationLabels}`}.{' '}
189 {org.plan.id === 'free' ? 'Upgrade your plan' : 'Disable spend cap'} to lift
190 restrictions immediately, or wait until your quota refills at the start of your next
191 billing period. Note that there may be a short delay after your billing period resets
192 before restrictions are fully lifted.
193 </p>
194 <div className="flex items-center gap-x-2 mt-3">
195 <Button key="upgrade-button" asChild type="default">
196 <Link
197 href={`/org/${org?.slug}/billing?panel=${
198 org.plan.id === 'free'
199 ? 'subscriptionPlan&source=fairUseRestricted'
200 : 'costControl&source=fairUseRestricted'
201 }`}
202 >
203 {org.plan.id === 'free' ? 'Upgrade plan' : 'Disable spend cap'}
204 </Link>
205 </Button>
206 {!isUsagePage && (
207 <Button key="view-usage-button" asChild type="default">
208 <Link href={`/org/${org?.slug}/usage`}>View usage</Link>
209 </Button>
210 )}
211 <Button asChild type="default" icon={<ExternalLink />}>
212 <a href={`${DOCS_URL}/guides/platform/billing-faq#fair-use-policy`}>
213 About Fair Use Policy
214 </a>
215 </Button>
216 </div>
217 </AlertDescription>
218 </Alert>
219 )}
220 </>
221 )
222}