Files
K-Dog (Kevin) a5ad2ce745 feat: log query/ingest insights (#50570)
We are doing a soft rollout for log pricing including log ingest and log
querying. We currently only want to display usage/soft warnings, which
is why the metrics are filtered out in some components.
2026-09-25 11:45:46 +08:00

382 lines
14 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import dayjs from 'dayjs'
import { ChartArea, 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/Admonition'
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 { Pipelines } from './Pipelines'
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 { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState'
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 { resolveHighAvailability } from '@/hooks/misc/useHighAvailability'
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 { data: selectedProject, isPending: isLoadingSelectedProject } = useProjectDetailQuery({
ref: selectedProjectRef ?? undefined,
})
const isHighAvailability = resolveHighAvailability(selectedProject)
const canLoadUsage = !selectedProjectRef || (!isLoadingSelectedProject && !isHighAvailability)
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 }, { enabled: canLoadUsage })
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,
},
{ enabled: canLoadUsage }
)
if (isHighAvailability) {
return (
<div className="flex flex-1 items-center justify-center p-6">
<HighAvailabilityDisabledEmptyState
icon={ChartArea}
title="Usage unavailable on High Availability projects"
description="Usage insights for High Availability projects are coming soon."
/>
</div>
)
}
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
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-primary">{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}
/>
<OrgLogUsage
orgSlug={slug as string}
projectRef={selectedProjectRef}
subscription={subscription}
startDate={startDate}
endDate={endDate}
currentBillingCycleSelected={currentBillingCycleSelected}
orgDailyStats={orgDailyStats}
isLoadingOrgDailyStats={isLoadingOrgDailyStats || isLoadingSubscription}
/>
<Pipelines
orgSlug={slug as string}
projectRef={selectedProjectRef}
subscription={subscription}
startDate={startDate}
endDate={endDate}
currentBillingCycleSelected={currentBillingCycleSelected}
orgDailyStats={orgDailyStats}
isLoadingOrgDailyStats={isLoadingOrgDailyStats}
/>
</>
)
}