mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
## Problem The `_Shadcn_` suffix isn't needed anymore on `Command` components ## Solution - Remove the `_Shadcn_` suffix - Simplify UI package exports - Apply prettier <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Simplified command component imports and exports across the UI library by removing internal naming aliases and adopting direct component references. Updated the public UI package barrel export to use wildcard re-exports for cleaner API surface. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46153?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
354 lines
13 KiB
TypeScript
354 lines
13 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import dayjs from 'dayjs'
|
|
import { Check, ChevronDown } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useQueryState } from 'nuqs'
|
|
import { useMemo, useState } from 'react'
|
|
import { Button, cn, CommandGroup, CommandItem } from 'ui'
|
|
import { Admonition } from 'ui-patterns'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { Restriction } from '../BillingSettings/Restriction'
|
|
import ActiveCompute from './ActiveCompute'
|
|
import Activity from './Activity'
|
|
import Compute from './Compute'
|
|
import Egress from './Egress'
|
|
import OrgLogUsage from './OrgLogUsage'
|
|
import SizeAndCounts from './SizeAndCounts'
|
|
import { TotalUsage } from './TotalUsage'
|
|
import {
|
|
ScaffoldContainer,
|
|
ScaffoldHeader,
|
|
ScaffoldSection,
|
|
ScaffoldTitle,
|
|
} from '@/components/layouts/Scaffold'
|
|
import AlertError from '@/components/ui/AlertError'
|
|
import DateRangePicker from '@/components/ui/DateRangePicker'
|
|
import NoPermission from '@/components/ui/NoPermission'
|
|
import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
|
|
import { useOrgDailyStatsQuery } from '@/data/analytics/org-daily-stats-query'
|
|
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
|
|
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
|
|
|
|
export const Usage = () => {
|
|
const { slug } = useParams()
|
|
|
|
const [dateRange, setDateRange] = useState<any>()
|
|
|
|
const [selectedProjectRef, setSelectedProjectRef] = useQueryState('projectRef')
|
|
const [openProjectSelector, setOpenProjectSelector] = useState(false)
|
|
|
|
const { can: canReadSubscriptions, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
|
|
PermissionAction.BILLING_READ,
|
|
'stripe.subscriptions'
|
|
)
|
|
|
|
const {
|
|
data: subscription,
|
|
error: subscriptionError,
|
|
isPending: isLoadingSubscription,
|
|
isError: isErrorSubscription,
|
|
isSuccess: isSuccessSubscription,
|
|
} = useOrgSubscriptionQuery({ orgSlug: slug })
|
|
|
|
const { data: selectedProject } = useProjectDetailQuery({
|
|
ref: selectedProjectRef ?? undefined,
|
|
})
|
|
|
|
const billingCycleStart = useMemo(() => {
|
|
return dayjs.unix(subscription?.current_period_start ?? 0).utc()
|
|
}, [subscription])
|
|
|
|
const billingCycleEnd = useMemo(() => {
|
|
return dayjs.unix(subscription?.current_period_end ?? 0).utc()
|
|
}, [subscription])
|
|
|
|
const currentBillingCycleSelected = useMemo(() => {
|
|
// Selected by default
|
|
if (!dateRange?.period_start || !dateRange?.period_end) return true
|
|
|
|
return (
|
|
dayjs(dateRange.period_start.date).isSame(billingCycleStart) &&
|
|
dayjs(dateRange.period_end.date).isSame(billingCycleEnd)
|
|
)
|
|
}, [dateRange, billingCycleStart, billingCycleEnd])
|
|
|
|
const startDate = useMemo(() => {
|
|
// If end date is in future, set end date to now
|
|
if (!dateRange?.period_start?.date) {
|
|
return undefined
|
|
} else {
|
|
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
|
return new Date(dateRange?.period_start?.date).toISOString().slice(0, -5) + 'Z'
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dateRange, subscription])
|
|
|
|
const endDate = useMemo(() => {
|
|
// If end date is in future, set end date to end of current day
|
|
if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
|
|
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
|
// In order to have full days from Prometheus metrics when using 1d interval,
|
|
// the time needs to be greater or equal than the time of the start date
|
|
return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
|
|
} else if (dateRange?.period_end?.date) {
|
|
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
|
return new Date(dateRange.period_end.date).toISOString().slice(0, -5) + 'Z'
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [dateRange, subscription])
|
|
|
|
const {
|
|
data: orgDailyStats,
|
|
error: orgDailyStatsError,
|
|
isPending: isLoadingOrgDailyStats,
|
|
isError: isErrorOrgDailyStats,
|
|
} = useOrgDailyStatsQuery({
|
|
orgSlug: slug,
|
|
projectRef: selectedProjectRef ?? undefined,
|
|
startDate,
|
|
endDate,
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<ScaffoldContainer>
|
|
<ScaffoldHeader className="pt-8">
|
|
<ScaffoldTitle>Usage</ScaffoldTitle>
|
|
</ScaffoldHeader>
|
|
</ScaffoldContainer>
|
|
<div className="sticky top-0 border-b bg-sidebar z-1">
|
|
<ScaffoldContainer>
|
|
<div className="py-4 flex items-center space-x-4">
|
|
{isLoadingSubscription || isLoadingPermissions ? (
|
|
<div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
|
|
<div className="flex items-center gap-2">
|
|
<ShimmeringLoader className="w-48" />
|
|
<ShimmeringLoader className="w-48" />
|
|
</div>
|
|
<ShimmeringLoader className="w-[280px]" />
|
|
</div>
|
|
) : !canReadSubscriptions ? (
|
|
<NoPermission resourceText="view organization usage" />
|
|
) : null}
|
|
|
|
{isErrorSubscription && (
|
|
<AlertError
|
|
className="w-full"
|
|
subject="Failed to retrieve usage data"
|
|
error={subscriptionError}
|
|
/>
|
|
)}
|
|
|
|
{isSuccessSubscription && (
|
|
<div className="flex lg:items-center items-start gap-3 flex-col lg:flex-row lg:justify-between w-full">
|
|
<div className="flex items-center gap-2">
|
|
<DateRangePicker
|
|
onChange={setDateRange}
|
|
value={TIME_PERIODS_BILLING[0].key}
|
|
options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
|
|
loading={isLoadingSubscription}
|
|
currentBillingPeriodStart={subscription?.current_period_start}
|
|
currentBillingPeriodEnd={subscription?.current_period_end}
|
|
className="w-48!"
|
|
/>
|
|
|
|
<OrganizationProjectSelector
|
|
open={openProjectSelector}
|
|
setOpen={setOpenProjectSelector}
|
|
selectedRef={selectedProjectRef}
|
|
onSelect={(project) => {
|
|
setSelectedProjectRef(project.ref)
|
|
}}
|
|
renderTrigger={({ listboxId, open }) => {
|
|
return (
|
|
<Button
|
|
block
|
|
type="default"
|
|
role="combobox"
|
|
size="tiny"
|
|
aria-expanded={open}
|
|
aria-controls={listboxId}
|
|
className="justify-between w-[180px]"
|
|
iconRight={<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
|
>
|
|
{!selectedProject ? 'All projects' : selectedProject?.name}
|
|
</Button>
|
|
)
|
|
}}
|
|
renderRow={(project) => {
|
|
const isSelected = selectedProjectRef === project.ref
|
|
return (
|
|
<div className="w-full flex items-center justify-between">
|
|
<span className={cn('truncate', isSelected ? 'max-w-60' : 'max-w-64')}>
|
|
{project.name}
|
|
</span>
|
|
{isSelected && <Check size={16} />}
|
|
</div>
|
|
)
|
|
}}
|
|
renderActions={() => (
|
|
<CommandGroup>
|
|
<CommandItem
|
|
className="cursor-pointer flex items-center justify-between w-full"
|
|
onSelect={() => {
|
|
setOpenProjectSelector(false)
|
|
setSelectedProjectRef(null)
|
|
}}
|
|
onClick={() => {
|
|
setOpenProjectSelector(false)
|
|
setSelectedProjectRef(null)
|
|
}}
|
|
>
|
|
All projects
|
|
{!selectedProjectRef && <Check size={16} />}
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<p className={cn('text-sm transition', isLoadingSubscription && 'opacity-50')}>
|
|
Organization is on the{' '}
|
|
<span className="font-medium text-brand">{subscription.plan.name} Plan</span>
|
|
</p>
|
|
<span className="text-border-stronger">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
stroke="currentColor"
|
|
strokeWidth="1"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="none"
|
|
shapeRendering="geometricPrecision"
|
|
>
|
|
<path d="M16 3.549L7.12 20.600" />
|
|
</svg>
|
|
</span>
|
|
<p className="text-sm text-foreground-light">
|
|
{billingCycleStart.format('DD MMM YYYY')} -{' '}
|
|
{billingCycleEnd.format('DD MMM YYYY')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScaffoldContainer>
|
|
</div>
|
|
|
|
{isErrorOrgDailyStats && (
|
|
<ScaffoldContainer>
|
|
<ScaffoldSection isFullWidth className="pb-0">
|
|
<AlertError
|
|
error={orgDailyStatsError}
|
|
subject="Failed to retrieve usage statistics for organization"
|
|
/>
|
|
</ScaffoldSection>
|
|
</ScaffoldContainer>
|
|
)}
|
|
|
|
{selectedProject ? (
|
|
<ScaffoldContainer className="mt-5">
|
|
<Admonition
|
|
type="default"
|
|
title="Usage filtered by project"
|
|
description={
|
|
<div>
|
|
You are currently viewing usage for the{' '}
|
|
<span className="font-medium text-foreground">
|
|
{selectedProject?.name || selectedProjectRef}
|
|
</span>{' '}
|
|
project. Supabase uses{' '}
|
|
<Link
|
|
href="/docs/guides/platform/billing-on-supabase#organization-based-billing"
|
|
target="_blank"
|
|
>
|
|
organization-level billing
|
|
</Link>{' '}
|
|
and quotas. For billing purposes, we sum up usage from all your projects. To view
|
|
your usage quota, set the project filter above back to "All Projects".
|
|
</div>
|
|
}
|
|
/>
|
|
</ScaffoldContainer>
|
|
) : (
|
|
<ScaffoldContainer id="restriction" className="mt-5">
|
|
<Restriction />
|
|
</ScaffoldContainer>
|
|
)}
|
|
|
|
<TotalUsage
|
|
orgSlug={slug as string}
|
|
projectRef={selectedProjectRef}
|
|
subscription={subscription}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
currentBillingCycleSelected={currentBillingCycleSelected}
|
|
/>
|
|
|
|
{subscription?.plan.id !== 'free' && (
|
|
<Compute orgDailyStats={orgDailyStats} isLoadingOrgDailyStats={isLoadingOrgDailyStats} />
|
|
)}
|
|
|
|
{subscription?.plan.id === 'platform' && (
|
|
<ActiveCompute
|
|
orgDailyStats={orgDailyStats}
|
|
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
|
/>
|
|
)}
|
|
|
|
<Egress
|
|
orgSlug={slug as string}
|
|
projectRef={selectedProjectRef}
|
|
subscription={subscription}
|
|
currentBillingCycleSelected={currentBillingCycleSelected}
|
|
orgDailyStats={orgDailyStats}
|
|
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
/>
|
|
|
|
<SizeAndCounts
|
|
orgSlug={slug as string}
|
|
projectRef={selectedProjectRef}
|
|
subscription={subscription}
|
|
currentBillingCycleSelected={currentBillingCycleSelected}
|
|
orgDailyStats={orgDailyStats}
|
|
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
/>
|
|
|
|
<Activity
|
|
orgSlug={slug as string}
|
|
projectRef={selectedProjectRef}
|
|
subscription={subscription}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
currentBillingCycleSelected={currentBillingCycleSelected}
|
|
orgDailyStats={orgDailyStats}
|
|
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
|
/>
|
|
|
|
{subscription?.plan.id === 'platform' && (
|
|
<OrgLogUsage
|
|
orgSlug={slug as string}
|
|
projectRef={selectedProjectRef}
|
|
subscription={subscription}
|
|
startDate={startDate}
|
|
endDate={endDate}
|
|
currentBillingCycleSelected={currentBillingCycleSelected}
|
|
orgDailyStats={orgDailyStats}
|
|
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|