Usage.tsx353 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import dayjs from 'dayjs'
4import { Check, ChevronDown } from 'lucide-react'
5import Link from 'next/link'
6import { useQueryState } from 'nuqs'
7import { useMemo, useState } from 'react'
8import { Button, cn, CommandGroup, CommandItem } from 'ui'
9import { Admonition } from 'ui-patterns'
10import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
11
12import { Restriction } from '../BillingSettings/Restriction'
13import ActiveCompute from './ActiveCompute'
14import Activity from './Activity'
15import Compute from './Compute'
16import Egress from './Egress'
17import OrgLogUsage from './OrgLogUsage'
18import SizeAndCounts from './SizeAndCounts'
19import { TotalUsage } from './TotalUsage'
20import {
21 ScaffoldContainer,
22 ScaffoldHeader,
23 ScaffoldSection,
24 ScaffoldTitle,
25} from '@/components/layouts/Scaffold'
26import AlertError from '@/components/ui/AlertError'
27import DateRangePicker from '@/components/ui/DateRangePicker'
28import NoPermission from '@/components/ui/NoPermission'
29import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
30import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query'
31import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
32import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
33import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
34import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
35
36export const Usage = () => {
37 const { slug } = useParams()
38
39 const [dateRange, setDateRange] = useState<any>()
40
41 const [selectedProjectRef, setSelectedProjectRef] = useQueryState('projectRef')
42 const [openProjectSelector, setOpenProjectSelector] = useState(false)
43
44 const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
45 PermissionAction.BILLING_READ,
46 'stripe.subscriptions'
47 )
48
49 const {
50 data: subscription,
51 error: subscriptionError,
52 isPending: isLoadingSubscription,
53 isError: isErrorSubscription,
54 isSuccess: isSuccessSubscription,
55 } = useOrgSubscriptionQuery({ orgSlug: slug })
56
57 const { data: selectedProject } = useProjectDetailQuery({
58 ref: selectedProjectRef ?? undefined,
59 })
60
61 const billingCycleStart = useMemo(() => {
62 return dayjs.unix(subscription?.current_period_start ?? 0).utc()
63 }, [subscription])
64
65 const billingCycleEnd = useMemo(() => {
66 return dayjs.unix(subscription?.current_period_end ?? 0).utc()
67 }, [subscription])
68
69 const currentBillingCycleSelected = useMemo(() => {
70 // Selected by default
71 if (!dateRange?.period_start || !dateRange?.period_end) return true
72
73 return (
74 dayjs(dateRange.period_start.date).isSame(billingCycleStart) &&
75 dayjs(dateRange.period_end.date).isSame(billingCycleEnd)
76 )
77 }, [dateRange, billingCycleStart, billingCycleEnd])
78
79 const startDate = useMemo(() => {
80 // If end date is in future, set end date to now
81 if (!dateRange?.period_start?.date) {
82 return undefined
83 } else {
84 // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
85 return new Date(dateRange?.period_start?.date).toISOString().slice(0, -5) + 'Z'
86 }
87 // eslint-disable-next-line react-hooks/exhaustive-deps
88 }, [dateRange, subscription])
89
90 const endDate = useMemo(() => {
91 // If end date is in future, set end date to end of current day
92 if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
93 // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
94 // In order to have full days from Prometheus metrics when using 1d interval,
95 // the time needs to be greater or equal than the time of the start date
96 return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
97 } else if (dateRange?.period_end?.date) {
98 // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
99 return new Date(dateRange.period_end.date).toISOString().slice(0, -5) + 'Z'
100 }
101 // eslint-disable-next-line react-hooks/exhaustive-deps
102 }, [dateRange, subscription])
103
104 const {
105 data: orgDailyStats,
106 error: orgDailyStatsError,
107 isPending: isLoadingOrgDailyStats,
108 isError: isErrorOrgDailyStats,
109 } = useOrgDailyStatsQuery({
110 orgSlug: slug,
111 projectRef: selectedProjectRef ?? undefined,
112 startDate,
113 endDate,
114 })
115
116 return (
117 <>
118 <ScaffoldContainer>
119 <ScaffoldHeader className="pt-8">
120 <ScaffoldTitle>Usage</ScaffoldTitle>
121 </ScaffoldHeader>
122 </ScaffoldContainer>
123 <div className="sticky top-0 border-b bg-sidebar z-1">
124 <ScaffoldContainer>
125 <div className="py-4 flex items-center space-x-4">
126 {isLoadingSubscription || isLoadingPermissions ? (
127 <div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
128 <div className="flex items-center gap-2">
129 <ShimmeringLoader className="w-48" />
130 <ShimmeringLoader className="w-48" />
131 </div>
132 <ShimmeringLoader className="w-[280px]" />
133 </div>
134 ) : !canReadSubscriptions ? (
135 <NoPermission resourceText="view organization usage" />
136 ) : null}
137
138 {isErrorSubscription && (
139 <AlertError
140 className="w-full"
141 subject="Failed to retrieve usage data"
142 error={subscriptionError}
143 />
144 )}
145
146 {isSuccessSubscription && (
147 <div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
148 <div className="flex items-center gap-2">
149 <DateRangePicker
150 onChange={setDateRange}
151 value={TIME_PERIODS_BILLING[0].key}
152 options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
153 loading={isLoadingSubscription}
154 currentBillingPeriodStart={subscription?.current_period_start}
155 currentBillingPeriodEnd={subscription?.current_period_end}
156 className="w-48!"
157 />
158
159 <OrganizationProjectSelector
160 open={openProjectSelector}
161 setOpen={setOpenProjectSelector}
162 selectedRef={selectedProjectRef}
163 onSelect={(project) => {
164 setSelectedProjectRef(project.ref)
165 }}
166 renderTrigger={({ listboxId, open }) => {
167 return (
168 <Button
169 block
170 type="default"
171 role="combobox"
172 size="tiny"
173 aria-expanded={open}
174 aria-controls={listboxId}
175 className="justify-between w-[180px]"
176 iconRight={<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
177 >
178 {!selectedProject ? 'All projects' : selectedProject?.name}
179 </Button>
180 )
181 }}
182 renderRow={(project) => {
183 const isSelected = selectedProjectRef === project.ref
184 return (
185 <div className="w-full flex items-center justify-between">
186 <span className={cn('truncate', isSelected ? 'max-w-60' : 'max-w-64')}>
187 {project.name}
188 </span>
189 {isSelected && <Check size={16} />}
190 </div>
191 )
192 }}
193 renderActions={() => (
194 <CommandGroup>
195 <CommandItem
196 className="cursor-pointer flex items-center justify-between w-full"
197 onSelect={() => {
198 setOpenProjectSelector(false)
199 setSelectedProjectRef(null)
200 }}
201 onClick={() => {
202 setOpenProjectSelector(false)
203 setSelectedProjectRef(null)
204 }}
205 >
206 All projects
207 {!selectedProjectRef && <Check size={16} />}
208 </CommandItem>
209 </CommandGroup>
210 )}
211 />
212 </div>
213
214 <div className="flex items-center gap-2">
215 <p className={cn('text-sm transition', isLoadingSubscription && 'opacity-50')}>
216 Organization is on the{' '}
217 <span className="font-medium text-brand">{subscription.plan.name} Plan</span>
218 </p>
219 <span className="text-border-stronger">
220 <svg
221 viewBox="0 0 24 24"
222 width="16"
223 height="16"
224 stroke="currentColor"
225 strokeWidth="1"
226 strokeLinecap="round"
227 strokeLinejoin="round"
228 fill="none"
229 shapeRendering="geometricPrecision"
230 >
231 <path d="M16 3.549L7.12 20.600" />
232 </svg>
233 </span>
234 <p className="text-sm text-foreground-light">
235 {billingCycleStart.format('DD MMM YYYY')} -{' '}
236 {billingCycleEnd.format('DD MMM YYYY')}
237 </p>
238 </div>
239 </div>
240 )}
241 </div>
242 </ScaffoldContainer>
243 </div>
244
245 {isErrorOrgDailyStats && (
246 <ScaffoldContainer>
247 <ScaffoldSection isFullWidth className="pb-0">
248 <AlertError
249 error={orgDailyStatsError}
250 subject="Failed to retrieve usage statistics for organization"
251 />
252 </ScaffoldSection>
253 </ScaffoldContainer>
254 )}
255
256 {selectedProject ? (
257 <ScaffoldContainer className="mt-5">
258 <Admonition
259 type="default"
260 title="Usage filtered by project"
261 description={
262 <div>
263 You are currently viewing usage for the{' '}
264 <span className="font-medium text-foreground">
265 {selectedProject?.name || selectedProjectRef}
266 </span>{' '}
267 project. Briven uses{' '}
268 <Link
269 href="/docs/guides/platform/billing-on-briven#organization-based-billing"
270 target="_blank"
271 >
272 organization-level billing
273 </Link>{' '}
274 and quotas. For billing purposes, we sum up usage from all your projects. To view
275 your usage quota, set the project filter above back to "All Projects".
276 </div>
277 }
278 />
279 </ScaffoldContainer>
280 ) : (
281 <ScaffoldContainer id="restriction" className="mt-5">
282 <Restriction />
283 </ScaffoldContainer>
284 )}
285
286 <TotalUsage
287 orgSlug={slug as string}
288 projectRef={selectedProjectRef}
289 subscription={subscription}
290 startDate={startDate}
291 endDate={endDate}
292 currentBillingCycleSelected={currentBillingCycleSelected}
293 />
294
295 {subscription?.plan.id !== 'free' && (
296 <Compute orgDailyStats={orgDailyStats} isLoadingOrgDailyStats={isLoadingOrgDailyStats} />
297 )}
298
299 {subscription?.plan.id === 'platform' && (
300 <ActiveCompute
301 orgDailyStats={orgDailyStats}
302 isLoadingOrgDailyStats={isLoadingOrgDailyStats}
303 />
304 )}
305
306 <Egress
307 orgSlug={slug as string}
308 projectRef={selectedProjectRef}
309 subscription={subscription}
310 currentBillingCycleSelected={currentBillingCycleSelected}
311 orgDailyStats={orgDailyStats}
312 isLoadingOrgDailyStats={isLoadingOrgDailyStats}
313 startDate={startDate}
314 endDate={endDate}
315 />
316
317 <SizeAndCounts
318 orgSlug={slug as string}
319 projectRef={selectedProjectRef}
320 subscription={subscription}
321 currentBillingCycleSelected={currentBillingCycleSelected}
322 orgDailyStats={orgDailyStats}
323 isLoadingOrgDailyStats={isLoadingOrgDailyStats}
324 startDate={startDate}
325 endDate={endDate}
326 />
327
328 <Activity
329 orgSlug={slug as string}
330 projectRef={selectedProjectRef}
331 subscription={subscription}
332 startDate={startDate}
333 endDate={endDate}
334 currentBillingCycleSelected={currentBillingCycleSelected}
335 orgDailyStats={orgDailyStats}
336 isLoadingOrgDailyStats={isLoadingOrgDailyStats}
337 />
338
339 {subscription?.plan.id === 'platform' && (
340 <OrgLogUsage
341 orgSlug={slug as string}
342 projectRef={selectedProjectRef}
343 subscription={subscription}
344 startDate={startDate}
345 endDate={endDate}
346 currentBillingCycleSelected={currentBillingCycleSelected}
347 orgDailyStats={orgDailyStats}
348 isLoadingOrgDailyStats={isLoadingOrgDailyStats}
349 />
350 )}
351 </>
352 )
353}