Merge pull request #14632 from supabase/usage-page-improvements

This commit is contained in:
Kevin Grüneberg authored and GitHub committed 2023-05-29 19:45:17 +02:00
commit a85abef75d
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,
}
)
+16 -3
View File
@@ -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]
}