mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
Merge pull request #14632 from supabase/usage-page-improvements
This commit is contained in:
12 files changed
+247
-139
No files matched your search
@@ -174,12 +174,10 @@ const BillingBreakdown = ({}: BillingBreakdownProps) => {
|
||||
<IconAlertTriangle size={14} strokeWidth={2} className="text-red-900" />
|
||||
<p className="text-sm text-red-900">Exceeded limit</p>
|
||||
</div>
|
||||
) : isApproachingLimit ? (
|
||||
) : isApproachingLimit && !isUsageBillingEnabled ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle size={14} strokeWidth={2} className="text-amber-900" />
|
||||
<p className="text-sm text-amber-900">
|
||||
Reaching {isUsageBillingEnabled ? 'limit' : 'quota'}
|
||||
</p>
|
||||
<p className="text-sm text-amber-900">Reaching limit</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -16,9 +16,10 @@ import UsageBarChart from './UsageBarChart'
|
||||
import SectionContent from './SectionContent'
|
||||
import SectionHeader from './SectionHeader'
|
||||
import { USAGE_CATEGORIES } from './Usage.constants'
|
||||
import { getUpgradeUrl } from './Usage.utils'
|
||||
import { ChartYFormatterCompactNumber, getUpgradeUrl } from './Usage.utils'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { DataPoint } from 'data/analytics/constants'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
export interface ActivityProps {
|
||||
projectRef: string
|
||||
@@ -32,14 +33,22 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
current_period_start !== undefined
|
||||
? new Date(current_period_start * 1000).toISOString()
|
||||
: undefined
|
||||
const endDate =
|
||||
let endDate =
|
||||
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
|
||||
// If end date is in future, set end date to now
|
||||
if (endDate && dayjs(endDate).isAfter(dayjs())) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
endDate = new Date().toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
|
||||
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'activity')
|
||||
|
||||
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
|
||||
|
||||
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
const isProTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
|
||||
const exceededLimitStyle = isFreeTier || isProTier ? 'text-red-900' : 'text-amber-900'
|
||||
const usageBasedBilling = !isFreeTier && !isProTier
|
||||
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
|
||||
|
||||
const { data: mauData, isLoading: isLoadingMauData } = useDailyStatsQuery({
|
||||
projectRef,
|
||||
@@ -149,27 +158,16 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
|
||||
const chartData = chartMeta[attribute.key]?.data ?? []
|
||||
|
||||
// [Joshen] Ideally this should come from the API imo, foresee some discrepancies
|
||||
const lastZeroValue = chartData.find(
|
||||
(x: any) => x.loopId > 0 && x[attribute.attribute] === 0
|
||||
)
|
||||
const lastKnownValue =
|
||||
lastZeroValue !== undefined && !chartMeta[attribute.key]?.hasNoData
|
||||
? dayjs(lastZeroValue.period_start)
|
||||
.subtract(1, 'day')
|
||||
.format('DD MMM YYYY, HH:mma (ZZ)')
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div id={attribute.anchor} key={attribute.key}>
|
||||
<SectionContent section={attribute} includedInPlan={usageMeta?.available_in_plan}>
|
||||
{usageMeta?.available_in_plan && (
|
||||
{usageMeta?.available_in_plan ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-sm">{attribute.name} quota usage</p>
|
||||
{usageRatio >= 1 ? (
|
||||
{!usageBasedBilling && usageRatio >= 1 ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle
|
||||
size={14}
|
||||
@@ -178,7 +176,7 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
/>
|
||||
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
|
||||
</div>
|
||||
) : usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
|
||||
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle
|
||||
size={14}
|
||||
@@ -189,7 +187,7 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isFreeTier && (
|
||||
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
|
||||
<Link href={upgradeUrl}>
|
||||
<a>
|
||||
<Button type="default" size="tiny">
|
||||
@@ -204,13 +202,15 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
type="horizontal"
|
||||
barClass={clsx(
|
||||
usageRatio >= 1
|
||||
? 'bg-red-900'
|
||||
? usageBasedBilling
|
||||
? 'bg-amber-900'
|
||||
: 'bg-red-900'
|
||||
: usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
? 'bg-amber-900'
|
||||
: 'bg-scale-1100'
|
||||
)}
|
||||
value={usageMeta?.usage ?? 0}
|
||||
max={usageMeta?.limit ?? 0}
|
||||
max={usageMeta?.limit || 1}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
@@ -220,6 +220,8 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
</p>
|
||||
{usageMeta?.limit === -1 ? (
|
||||
<p className="text-xs">None</p>
|
||||
) : usageMeta?.limit === 0 ? (
|
||||
<p className="text-xs">Unlimited</p>
|
||||
) : (
|
||||
<p className="text-xs">{(usageMeta?.limit ?? 0).toLocaleString()}</p>
|
||||
)}
|
||||
@@ -257,18 +259,38 @@ const Activity = ({ projectRef }: ActivityProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<UsageBarChart
|
||||
hasQuota
|
||||
hasQuota={usageMeta.limit > 0}
|
||||
name={attribute.name}
|
||||
unit={attribute.unit}
|
||||
attribute={attribute.attribute}
|
||||
data={chartData}
|
||||
yLimit={usageMeta?.limit ?? 0}
|
||||
yLeftMargin={chartMeta[attribute.key].margin}
|
||||
yFormatter={(value) => value.toLocaleString()}
|
||||
yFormatter={value => ChartYFormatterCompactNumber(value, attribute.unit)}
|
||||
quotaWarningType={isFreeTier || isProTier ? 'danger' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Not included in plan</p>
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
You need to be on a higher plan in order to use this feature.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/settings/billing/subscription`}>
|
||||
<a>
|
||||
<Button type="primary">Upgrade plan</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
</SectionContent>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Button, IconAlertTriangle } from 'ui'
|
||||
import SectionContent from './SectionContent'
|
||||
import SectionHeader from './SectionHeader'
|
||||
import { USAGE_CATEGORIES } from './Usage.constants'
|
||||
import { getUpgradeUrl } from './Usage.utils'
|
||||
import { ChartYFormatterCompactNumber, getUpgradeUrl } from './Usage.utils'
|
||||
import UsageBarChart from './UsageBarChart'
|
||||
|
||||
export interface BandwidthProps {
|
||||
@@ -32,14 +32,21 @@ const Bandwidth = ({ projectRef }: BandwidthProps) => {
|
||||
current_period_start !== undefined
|
||||
? new Date(current_period_start * 1000).toISOString()
|
||||
: undefined
|
||||
const endDate =
|
||||
let endDate =
|
||||
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
|
||||
// If end date is in future, set end date to now
|
||||
if (endDate && dayjs(endDate).isAfter(dayjs())) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
endDate = new Date().toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
|
||||
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'bandwidth')
|
||||
|
||||
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
|
||||
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
const isProTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
|
||||
const exceededLimitStyle = isFreeTier || isProTier ? 'text-red-900' : 'text-amber-900'
|
||||
const usageBasedBilling = !isFreeTier && !isProTier
|
||||
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
|
||||
|
||||
const { data: dbEgressData, isLoading: isLoadingDbEgressData } = useDailyStatsQuery({
|
||||
projectRef,
|
||||
@@ -88,25 +95,14 @@ const Bandwidth = ({ projectRef }: BandwidthProps) => {
|
||||
|
||||
const chartData = chartMeta[attribute.key]?.data ?? []
|
||||
|
||||
// [Joshen] Ideally this should come from the API imo, foresee some discrepancies
|
||||
const lastZeroValue = chartData.find(
|
||||
(x: any) => x.loopId > 0 && x[attribute.attribute] === 0
|
||||
)
|
||||
const lastKnownValue =
|
||||
lastZeroValue !== undefined && !chartMeta[attribute.key]?.hasNoData
|
||||
? dayjs(lastZeroValue.period_start)
|
||||
.subtract(1, 'day')
|
||||
.format('DD MMM YYYY, HH:mma (ZZ)')
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div id={attribute.anchor} key={attribute.key}>
|
||||
<SectionContent section={attribute} lastKnownValue={lastKnownValue}>
|
||||
<SectionContent section={attribute}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-sm">{attribute.name} quota usage</p>
|
||||
{usageRatio >= 1 ? (
|
||||
{!usageBasedBilling && usageRatio >= 1 ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle
|
||||
size={14}
|
||||
@@ -115,14 +111,14 @@ const Bandwidth = ({ projectRef }: BandwidthProps) => {
|
||||
/>
|
||||
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
|
||||
</div>
|
||||
) : usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
|
||||
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle size={14} strokeWidth={2} className="text-amber-900" />
|
||||
<p className="text-sm text-amber-900">Approaching limit</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isFreeTier && (
|
||||
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
|
||||
<Link href={upgradeUrl}>
|
||||
<a>
|
||||
<Button type="default" size="tiny">
|
||||
@@ -137,13 +133,15 @@ const Bandwidth = ({ projectRef }: BandwidthProps) => {
|
||||
type="horizontal"
|
||||
barClass={clsx(
|
||||
usageRatio >= 1
|
||||
? 'bg-red-900'
|
||||
? usageBasedBilling
|
||||
? 'bg-amber-900'
|
||||
: 'bg-red-900'
|
||||
: usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
? 'bg-amber-900'
|
||||
: 'bg-scale-1100'
|
||||
)}
|
||||
value={usageMeta?.usage ?? 0}
|
||||
max={usageMeta?.limit ?? 0}
|
||||
max={usageMeta?.limit || 1}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
@@ -192,7 +190,7 @@ const Bandwidth = ({ projectRef }: BandwidthProps) => {
|
||||
data={chartData}
|
||||
yLimit={usageMeta?.limit ?? 0}
|
||||
yLeftMargin={chartMeta[attribute.key].margin}
|
||||
yFormatter={(value) => formatBytes(value, 1, 'GB').replace(/\s/g, '')}
|
||||
yFormatter={value => ChartYFormatterCompactNumber(value, attribute.unit)}
|
||||
quotaWarningType={isFreeTier || isProTier ? 'danger' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import SectionHeader from './SectionHeader'
|
||||
import { COMPUTE_INSTANCE_SPECS, USAGE_CATEGORIES } from './Usage.constants'
|
||||
import { getUpgradeUrl } from './Usage.utils'
|
||||
import UsageBarChart from './UsageBarChart'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
export interface InfrastructureProps {
|
||||
projectRef: string
|
||||
@@ -25,8 +26,15 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
|
||||
current_period_start !== undefined
|
||||
? new Date(current_period_start * 1000).toISOString()
|
||||
: undefined
|
||||
const endDate =
|
||||
let endDate =
|
||||
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
|
||||
|
||||
// If end date is in future, set end date to now
|
||||
if (endDate && dayjs(endDate).isAfter(dayjs())) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
endDate = new Date().toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
|
||||
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'infra')
|
||||
|
||||
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
|
||||
@@ -37,28 +45,43 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
|
||||
const currentComputeInstanceSpecs =
|
||||
COMPUTE_INSTANCE_SPECS[currentComputeInstance?.supabase_prod_id ?? 'addon_instance_micro']
|
||||
|
||||
// Switch to hourly interval, if the timeframe is <48 hours
|
||||
let interval: '1d' | '1h' = '1d'
|
||||
let dateFormat = 'DD MMM'
|
||||
if (startDate && endDate) {
|
||||
const diffInHours = dayjs(endDate).diff(startDate, 'hours')
|
||||
|
||||
if (diffInHours <= 48) {
|
||||
interval = '1h'
|
||||
dateFormat = 'HH a'
|
||||
}
|
||||
}
|
||||
|
||||
const { data: cpuUsageData, isLoading: isLoadingCpuUsageData } = useInfraMonitoringQuery({
|
||||
projectRef,
|
||||
attribute: 'cpu_usage',
|
||||
interval: '1d',
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat,
|
||||
})
|
||||
|
||||
const { data: memoryUsageData, isLoading: isLoadingMemoryUsageData } = useInfraMonitoringQuery({
|
||||
projectRef,
|
||||
attribute: 'ram_usage',
|
||||
interval: '1d',
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat,
|
||||
})
|
||||
|
||||
const { data: ioBudgetData, isLoading: isLoadingIoBudgetData } = useInfraMonitoringQuery({
|
||||
projectRef,
|
||||
attribute: 'disk_io_budget',
|
||||
interval: '1d',
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat,
|
||||
})
|
||||
|
||||
const currentDayIoBudget = Number(
|
||||
@@ -90,20 +113,9 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
|
||||
{categoryMeta.attributes.map((attribute) => {
|
||||
const chartData = chartMeta[attribute.key]?.data ?? []
|
||||
|
||||
// [Joshen] Ideally this should come from the API imo, foresee some discrepancies
|
||||
const lastZeroValue = chartData.find(
|
||||
(x: any) => x.loopId > 0 && x[attribute.attribute] === 0
|
||||
)
|
||||
const lastKnownValue =
|
||||
lastZeroValue !== undefined
|
||||
? dayjs(lastZeroValue.period_start)
|
||||
.subtract(1, 'day')
|
||||
.format('DD MMM YYYY, HH:mma (ZZ)')
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div id={attribute.anchor} key={attribute.key}>
|
||||
<SectionContent section={attribute} lastKnownValue={lastKnownValue}>
|
||||
<SectionContent section={attribute}>
|
||||
{attribute.key === 'disk_io_budget' && (
|
||||
<>
|
||||
{currentDayIoBudget <= 0 ? (
|
||||
@@ -172,17 +184,27 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
{attribute.key === 'disk_io_budget' ? (
|
||||
<p>IO Budget remaining each day</p>
|
||||
) : (
|
||||
<p>
|
||||
Max{' '}
|
||||
<span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
|
||||
{attribute.name}
|
||||
</span>{' '}
|
||||
usage each day
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-row justify-between">
|
||||
{attribute.key === 'disk_io_budget' ? (
|
||||
<p>IO Budget remaining</p>
|
||||
) : (
|
||||
<p>
|
||||
Max{' '}
|
||||
<span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
|
||||
{attribute.name}
|
||||
</span>{' '}
|
||||
usage
|
||||
</p>
|
||||
)}
|
||||
<Link href={`/project/${projectRef}/settings/billing/subscription`}>
|
||||
<a>
|
||||
<Button type="default" size="tiny">
|
||||
Upgrade compute
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
|
||||
<p key={`para-${idx}`} className="text-sm text-scale-1000">
|
||||
{paragraph}
|
||||
@@ -195,7 +217,7 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
) : (
|
||||
) : chartData.length ? (
|
||||
<UsageBarChart
|
||||
name={attribute.name}
|
||||
unit={attribute.unit}
|
||||
@@ -204,6 +226,17 @@ const Infrastructure = ({ projectRef }: InfrastructureProps) => {
|
||||
yFormatter={(value) => `${value}%`}
|
||||
yLimit={100}
|
||||
/>
|
||||
) : (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<p>No data</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
There is no data in the timeframe available
|
||||
</p>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
</SectionContent>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,14 @@ import Link from 'next/link'
|
||||
export interface SectionContent {
|
||||
section: CategoryAttribute
|
||||
includedInPlan?: boolean
|
||||
lastKnownValue?: string
|
||||
}
|
||||
|
||||
const SectionContent = ({
|
||||
section,
|
||||
includedInPlan,
|
||||
lastKnownValue,
|
||||
children,
|
||||
}: PropsWithChildren<SectionContent>) => {
|
||||
const { name, description, docsUrl } = section
|
||||
const { name, description, links } = section
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
@@ -29,27 +27,28 @@ const SectionContent = ({
|
||||
{includedInPlan === false && <Badge color="gray">Not included</Badge>}
|
||||
</div>
|
||||
{description.split('\n').map((value, idx) => (
|
||||
<p key={`desc-${idx}`} className="text-sm text-scale-1000">
|
||||
<p key={`desc-${idx}`} className="text-sm text-scale-1000 pr-8">
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{docsUrl !== undefined && (
|
||||
<div>
|
||||
{links && links.length && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-scale-1100 mb-2">More information</p>
|
||||
<Link href={docsUrl}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
|
||||
<p className="text-sm">Documentation</p>
|
||||
<IconExternalLink size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
{links.map((link) => (
|
||||
<div key={link.url}>
|
||||
<Link href={link.url}>
|
||||
<a target="_blank" rel="noreferrer">
|
||||
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
|
||||
<p className="text-sm">{link.name}</p>
|
||||
<IconExternalLink size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{lastKnownValue !== undefined && (
|
||||
<p className="text-xs text-scale-1000">Last updated at: {lastKnownValue}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-7 space-y-6">{children}</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import UsageBarChart from './UsageBarChart'
|
||||
import SectionContent from './SectionContent'
|
||||
import SectionHeader from './SectionHeader'
|
||||
import { USAGE_CATEGORIES } from './Usage.constants'
|
||||
import { getUpgradeUrl } from './Usage.utils'
|
||||
import { ChartYFormatterCompactNumber, getUpgradeUrl } from './Usage.utils'
|
||||
import { DataPoint } from 'data/analytics/constants'
|
||||
|
||||
export interface SizeAndCountsProps {
|
||||
@@ -33,14 +33,21 @@ const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
|
||||
current_period_start !== undefined
|
||||
? new Date(current_period_start * 1000).toISOString()
|
||||
: undefined
|
||||
const endDate =
|
||||
let endDate =
|
||||
current_period_end !== undefined ? new Date(current_period_end * 1000).toISOString() : undefined
|
||||
// If end date is in future, set end date to now
|
||||
if (endDate && dayjs(endDate).isAfter(dayjs())) {
|
||||
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
||||
endDate = new Date().toISOString().slice(0, -5) + 'Z'
|
||||
}
|
||||
|
||||
const categoryMeta = USAGE_CATEGORIES.find((category) => category.key === 'sizeCount')
|
||||
|
||||
const upgradeUrl = getUpgradeUrl(projectRef, subscription)
|
||||
const isFreeTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.FREE
|
||||
const isProTier = subscription?.tier.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.PRO
|
||||
const exceededLimitStyle = isFreeTier || isProTier ? 'text-red-900' : 'text-amber-900'
|
||||
const usageBasedBilling = !isFreeTier && !isProTier
|
||||
const exceededLimitStyle = !usageBasedBilling ? 'text-red-900' : 'text-amber-900'
|
||||
|
||||
const { data: dbSizeData, isLoading: isLoadingDbSizeData } = useDailyStatsQuery({
|
||||
projectRef,
|
||||
@@ -103,25 +110,14 @@ const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
|
||||
|
||||
const chartData = chartMeta[attribute.key]?.data ?? []
|
||||
|
||||
// [Joshen] Ideally this should come from the API imo, foresee some discrepancies
|
||||
const lastZeroValue = chartData.find(
|
||||
(x: any) => x.loopId > 0 && x[attribute.attribute] === 0
|
||||
)
|
||||
const lastKnownValue =
|
||||
lastZeroValue !== undefined && !chartMeta[attribute.key]?.hasNoData
|
||||
? dayjs(lastZeroValue.period_start)
|
||||
.subtract(1, 'day')
|
||||
.format('DD MMM YYYY, HH:mma (ZZ)')
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div id={attribute.anchor} key={attribute.key}>
|
||||
<SectionContent section={attribute} lastKnownValue={lastKnownValue}>
|
||||
<SectionContent section={attribute}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-sm">{attribute.name} quota usage</p>
|
||||
{usageRatio >= 1 ? (
|
||||
{!usageBasedBilling && usageRatio >= 1 ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle
|
||||
size={14}
|
||||
@@ -130,14 +126,15 @@ const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
|
||||
/>
|
||||
<p className={`text-sm ${exceededLimitStyle}`}>Exceeded limit</p>
|
||||
</div>
|
||||
) : usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
|
||||
) : !usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD ? (
|
||||
<div className="flex items-center space-x-2 min-w-[115px]">
|
||||
<IconAlertTriangle size={14} strokeWidth={2} className="text-amber-900" />
|
||||
<p className="text-sm text-amber-900">Approaching limit</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{isFreeTier && (
|
||||
|
||||
{!usageBasedBilling && usageRatio >= USAGE_APPROACHING_THRESHOLD && (
|
||||
<Link href={upgradeUrl}>
|
||||
<a>
|
||||
<Button type="default" size="tiny">
|
||||
@@ -152,13 +149,15 @@ const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
|
||||
type="horizontal"
|
||||
barClass={clsx(
|
||||
usageRatio >= 1
|
||||
? 'bg-red-900'
|
||||
? usageBasedBilling
|
||||
? 'bg-amber-900'
|
||||
: 'bg-red-900'
|
||||
: usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
? 'bg-amber-900'
|
||||
: 'bg-scale-1100'
|
||||
)}
|
||||
value={usageMeta?.usage ?? 0}
|
||||
max={usageMeta?.limit ?? 0}
|
||||
max={usageMeta?.limit || 1}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
@@ -233,18 +232,14 @@ const SizeAndCounts = ({ projectRef }: SizeAndCountsProps) => {
|
||||
</div>
|
||||
) : (
|
||||
<UsageBarChart
|
||||
hasQuota
|
||||
hasQuota={usageMeta.limit > 0}
|
||||
name={attribute.name}
|
||||
unit={attribute.unit}
|
||||
attribute={attribute.attribute}
|
||||
data={chartData}
|
||||
yLimit={usageMeta?.limit ?? 0}
|
||||
yLeftMargin={chartMeta[attribute.key].margin}
|
||||
yFormatter={(value) =>
|
||||
attribute.unit === 'bytes'
|
||||
? formatBytes(value, 1, 'GB').replace(/\s/g, '')
|
||||
: value.toLocaleString()
|
||||
}
|
||||
yFormatter={(value) => ChartYFormatterCompactNumber(value, attribute.unit)}
|
||||
quotaWarningType={isFreeTier || isProTier ? 'danger' : 'warning'}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -12,7 +12,10 @@ export interface CategoryAttribute {
|
||||
attribute: string // For querying against stats-daily / infra-monitoring
|
||||
name: string
|
||||
unit: 'bytes' | 'absolute' | 'percentage'
|
||||
docsUrl?: string
|
||||
links?: {
|
||||
name: string
|
||||
url: string
|
||||
}[]
|
||||
description: string
|
||||
chartDescription: string
|
||||
}
|
||||
@@ -35,7 +38,14 @@ export const USAGE_CATEGORIES: {
|
||||
name: 'CPU',
|
||||
unit: 'percentage',
|
||||
description: 'CPU usage of your server',
|
||||
chartDescription: 'The data shown here is refreshed over a period of 24 hours.',
|
||||
chartDescription: '',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-Ons',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons',
|
||||
},
|
||||
{ name: 'High CPU Usage', url: 'https://supabase.com/docs/guides/platform/exhaust-cpu' },
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'ram',
|
||||
@@ -44,7 +54,13 @@ export const USAGE_CATEGORIES: {
|
||||
name: 'Memory',
|
||||
unit: 'percentage',
|
||||
description: 'Memory usage of your server',
|
||||
chartDescription: 'The data shown here is refreshed over a period of 24 hours.',
|
||||
chartDescription: '',
|
||||
links: [
|
||||
{
|
||||
name: 'Compute Add-Ons',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
anchor: 'disk_io_budget',
|
||||
@@ -52,11 +68,15 @@ export const USAGE_CATEGORIES: {
|
||||
attribute: 'disk_io_budget',
|
||||
name: 'Disk IO bandwidth',
|
||||
unit: 'percentage',
|
||||
docsUrl: 'https://supabase.com/docs/guides/platform/compute-add-ons#disk-io-bandwidth',
|
||||
links: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://supabase.com/docs/guides/platform/compute-add-ons#disk-io-bandwidth',
|
||||
},
|
||||
],
|
||||
description:
|
||||
'SSD Disks are attached to your servers and the disk performance of your workload is determined by the Disk IO bandwidth of this connection.',
|
||||
chartDescription:
|
||||
'The amount of remaining bandwidth resets at the beginning of each day, and the data shown here is refreshed over a period of 24 hours.',
|
||||
chartDescription: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -82,7 +102,7 @@ export const USAGE_CATEGORIES: {
|
||||
name: 'Storage egress',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
'Contains any outgoing traffic (egress) from your storage buckets (only download operations are counted). We currently do not differentiate between no-cache and cache hits.',
|
||||
'All requests to download/view your storage items go through our CDN.\nWe sum up all outgoing traffic (egress) for storage related requests through our CDN.\nWe do not differentiate between cache and no cache hits.',
|
||||
chartDescription:
|
||||
'Billing is based on the total amount of egress in GB throughout your billing period. The data shown here is refreshed over a period of 24 hours.',
|
||||
},
|
||||
@@ -100,7 +120,12 @@ export const USAGE_CATEGORIES: {
|
||||
name: 'Database size',
|
||||
unit: 'bytes',
|
||||
description: "Size of your project's database",
|
||||
docsUrl: 'https://supabase.com/docs/guides/platform/database-usage',
|
||||
links: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://supabase.com/docs/guides/platform/database-size',
|
||||
},
|
||||
],
|
||||
chartDescription:
|
||||
'Billing is based on the average daily database size in GB throughout the billing period. The data shown here is refreshed over a period of 24 hours.',
|
||||
},
|
||||
@@ -154,17 +179,17 @@ export const USAGE_CATEGORIES: {
|
||||
'The data shown here is refreshed over a period of 24 hours and resets at the beginning of every billing period.',
|
||||
},
|
||||
{
|
||||
anchor:'storageImageTransformations',
|
||||
anchor: 'storageImageTransformations',
|
||||
key: 'storage_image_render_count',
|
||||
attribute: 'total_storage_image_render_count',
|
||||
name: 'Storage image transformations',
|
||||
unit: 'absolute',
|
||||
description:
|
||||
'We distinctly count all images that were transformed in the billing period, ignoring any transformations.\nIf you transform one image with different transformations, it only counts as one. We only count the unique (origin) images being transformed.',
|
||||
'We distinctly count all images that were transformed in the billing period, ignoring any transformations.\nIf you transform one image with different transformations, it only counts as one.\nWe only count the unique (origin) images being transformed.',
|
||||
chartDescription: 'The data shown here is refreshed over a period of 24 hours.',
|
||||
},
|
||||
{
|
||||
anchor:'functionInvocations',
|
||||
anchor: 'functionInvocations',
|
||||
key: 'func_invocations',
|
||||
attribute: 'total_func_invocations',
|
||||
name: 'Edge function invocations',
|
||||
@@ -175,7 +200,7 @@ export const USAGE_CATEGORIES: {
|
||||
'Billing is based on the sum of all invocations throughout your billing period. The data shown here is refreshed over a period of 24 hours.',
|
||||
},
|
||||
{
|
||||
anchor:'realtimeMessageCount',
|
||||
anchor: 'realtimeMessageCount',
|
||||
key: 'realtime_message_count',
|
||||
attribute: 'total_realtime_message_count',
|
||||
name: 'Realtime message count',
|
||||
|
||||
@@ -13,13 +13,14 @@ import SizeAndCounts from './SizeAndCounts'
|
||||
import { USAGE_CATEGORIES, USAGE_STATUS } from './Usage.constants'
|
||||
import { getUsageStatus } from './Usage.utils'
|
||||
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
|
||||
const Usage = () => {
|
||||
const { ref } = useParams()
|
||||
|
||||
// [Joshen] Using a state here for now as in the future, we'd have this usage page support showing
|
||||
// stats for different projects. So we'll pass the selected ref as a prop into the individual components
|
||||
const [selectedProjectRef, setSelectedProjectRef] = useState<string>(ref as string)
|
||||
const [selectedProjectRef] = useState<string>(ref as string)
|
||||
|
||||
const infrastructureRef = useRef<HTMLDivElement>(null)
|
||||
const bandwidthRef = useRef<HTMLDivElement>(null)
|
||||
@@ -54,6 +55,10 @@ const Usage = () => {
|
||||
const billingCycleStart = dayjs.unix(subscription?.billing?.current_period_start ?? 0).utc()
|
||||
const billingCycleEnd = dayjs.unix(subscription?.billing?.current_period_end ?? 0).utc()
|
||||
|
||||
const subscriptionTierId = subscription?.tier?.supabase_prod_id
|
||||
const usageBillingEnabled =
|
||||
subscriptionTierId === PRICING_TIER_PRODUCT_IDS.FREE || PRICING_TIER_PRODUCT_IDS.PRO
|
||||
|
||||
const scrollTo = (id: 'infra' | 'bandwidth' | 'sizeCount' | 'activity') => {
|
||||
switch (id) {
|
||||
case 'infra':
|
||||
@@ -104,7 +109,8 @@ const Usage = () => {
|
||||
Project is on {subscription.tier.name}
|
||||
</p>
|
||||
<p className="text-sm text-scale-1000">
|
||||
{billingCycleStart.format('DD MMM YY')} - {billingCycleEnd.format('DD MMM YY')}
|
||||
{billingCycleStart.format('DD MMM YYYY')} -{' '}
|
||||
{billingCycleEnd.format('DD MMM YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -140,7 +146,7 @@ const Usage = () => {
|
||||
onClick={() => scrollTo(category.key)}
|
||||
className="flex items-center opacity-50 space-x-2 py-3 hover:opacity-100 transition cursor-pointer"
|
||||
>
|
||||
{status === USAGE_STATUS.NORMAL ? (
|
||||
{status === USAGE_STATUS.NORMAL || usageBillingEnabled ? (
|
||||
<IconCheckCircle size={15} strokeWidth={2} className="text-scale-1100" />
|
||||
) : status === USAGE_STATUS.APPROACHING ? (
|
||||
<IconAlertCircle size={15} strokeWidth={2} className="text-amber-900" />
|
||||
@@ -155,8 +161,8 @@ const Usage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
[Joshen] Could potentially run a map here based on USAGE_CATEGORIES, rather than defining each section
|
||||
{/*
|
||||
[Joshen] Could potentially run a map here based on USAGE_CATEGORIES, rather than defining each section
|
||||
but thinking it's gonna "cover up" too much details and make it harder to add attribute specific components
|
||||
e.g for database size, we also need to show disk volume size. Not to mention that are little nuances across
|
||||
each attribute RE formatting (bytes vs locale string)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { USAGE_APPROACHING_THRESHOLD } from '../Billing.constants'
|
||||
import { CategoryAttribute, USAGE_STATUS } from './Usage.constants'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { formatBytes } from 'lib/helpers'
|
||||
|
||||
// [Joshen] This is just for development to generate some test data for chart rendering
|
||||
export const generateUsageData = (attribute: string, days: number): DataPoint[] => {
|
||||
@@ -46,3 +47,20 @@ export const getUpgradeUrl = (projectRef: string, subscription?: StripeSubscript
|
||||
? `/project/${projectRef}/settings/billing/update`
|
||||
: `/project/${projectRef}/settings/billing/update/pro`
|
||||
}
|
||||
|
||||
const compactNumberFormatter = new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
})
|
||||
|
||||
export const ChartYFormatterCompactNumber = (number: number | string, unit: string) => {
|
||||
if (typeof number === 'string') return number
|
||||
|
||||
if (unit === 'bytes') {
|
||||
const formattedBytes = formatBytes(number, 0).replace(/\s/g, '')
|
||||
|
||||
return formattedBytes === '0bytes' ? '0' : formattedBytes
|
||||
} else {
|
||||
return compactNumberFormatter.format(number)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ const UsageBarChart = ({
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-scale-800" />
|
||||
<XAxis dataKey="periodStartFormatted" />
|
||||
<YAxis
|
||||
width={28}
|
||||
width={40}
|
||||
axisLine={false}
|
||||
tickLine={{ stroke: 'none' }}
|
||||
domain={yDomain}
|
||||
|
||||
@@ -161,6 +161,7 @@ export const useDailyStatsQuery = <TData = DailyStatsData>(
|
||||
} as TData
|
||||
}
|
||||
},
|
||||
staleTime: 1000 * 60 * 60, // default good for an hour for now
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { LogsEndpointParams } from 'components/interfaces/Settings/Logs'
|
||||
|
||||
export const analyticsKeys = {
|
||||
functionsInvStats: (
|
||||
projectRef: string | undefined,
|
||||
@@ -14,7 +12,17 @@ export const analyticsKeys = {
|
||||
interval,
|
||||
}: { attribute?: string; startDate?: string; endDate?: string; interval?: string }
|
||||
) =>
|
||||
['projects', projectRef, 'daily-stats', { attribute, startDate, endDate, interval }] as const,
|
||||
[
|
||||
'projects',
|
||||
projectRef,
|
||||
'daily-stats',
|
||||
{
|
||||
attribute,
|
||||
startDate: isoDateStringToDate(startDate),
|
||||
endDate: isoDateStringToDate(endDate),
|
||||
interval,
|
||||
},
|
||||
] as const,
|
||||
infraMonitoring: (
|
||||
projectRef: string | undefined,
|
||||
{
|
||||
@@ -34,3 +42,8 @@ export const analyticsKeys = {
|
||||
['projects', projectRef, 'usage.api-counts', interval] as const,
|
||||
}
|
||||
|
||||
function isoDateStringToDate(isoDateString: string | undefined): string | undefined {
|
||||
if (!isoDateString) return isoDateString
|
||||
|
||||
return isoDateString.split('T')[0]
|
||||
}
|
||||
Reference in new issue
Block a user