Usage.tsx353 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useParams } from 'common' |
| 3 | import dayjs from 'dayjs' |
| 4 | import { Check, ChevronDown } from 'lucide-react' |
| 5 | import Link from 'next/link' |
| 6 | import { useQueryState } from 'nuqs' |
| 7 | import { useMemo, useState } from 'react' |
| 8 | import { Button, cn, CommandGroup, CommandItem } from 'ui' |
| 9 | import { Admonition } from 'ui-patterns' |
| 10 | import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' |
| 11 | |
| 12 | import { Restriction } from '../BillingSettings/Restriction' |
| 13 | import ActiveCompute from './ActiveCompute' |
| 14 | import Activity from './Activity' |
| 15 | import Compute from './Compute' |
| 16 | import Egress from './Egress' |
| 17 | import OrgLogUsage from './OrgLogUsage' |
| 18 | import SizeAndCounts from './SizeAndCounts' |
| 19 | import { TotalUsage } from './TotalUsage' |
| 20 | import { |
| 21 | ScaffoldContainer, |
| 22 | ScaffoldHeader, |
| 23 | ScaffoldSection, |
| 24 | ScaffoldTitle, |
| 25 | } from '@/components/layouts/Scaffold' |
| 26 | import AlertError from '@/components/ui/AlertError' |
| 27 | import DateRangePicker from '@/components/ui/DateRangePicker' |
| 28 | import NoPermission from '@/components/ui/NoPermission' |
| 29 | import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector' |
| 30 | import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query' |
| 31 | import { useProjectDetailQuery } from '@/data/projects/project-detail-query' |
| 32 | import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' |
| 33 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 34 | import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics' |
| 35 | |
| 36 | export 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 | } |