mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
404 lines
17 KiB
TypeScript
404 lines
17 KiB
TypeScript
import Link from 'next/link'
|
|
import { FC, useEffect } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import * as Tooltip from '@radix-ui/react-tooltip'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
IconAlertCircle,
|
|
IconInfo,
|
|
Loading,
|
|
IconExternalLink,
|
|
Alert,
|
|
IconBookOpen,
|
|
} from 'ui'
|
|
|
|
import { checkPermissions, useStore } from 'hooks'
|
|
import { formatBytes } from 'lib/helpers'
|
|
import { PRICING_TIER_PRODUCT_IDS, USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
|
|
import SparkBar from 'components/ui/SparkBar'
|
|
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
|
import InformationBox from 'components/ui/InformationBox'
|
|
import { USAGE_BASED_PRODUCTS } from 'components/interfaces/Billing/Billing.constants'
|
|
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
|
import { useProjectReadOnlyQuery } from 'data/config/project-read-only-query'
|
|
import { ProjectUsageResponseUsageKeys, useProjectUsageQuery } from 'data/usage/project-usage-query'
|
|
|
|
interface Props {
|
|
projectRef?: string
|
|
}
|
|
|
|
const ProjectUsage: FC<Props> = ({ projectRef }) => {
|
|
const { ui } = useStore()
|
|
const { data: usage, error, isLoading } = useProjectUsageQuery({ projectRef })
|
|
const router = useRouter()
|
|
|
|
const { project } = useProjectContext()
|
|
const { data: isReadOnlyMode } = useProjectReadOnlyQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
|
|
const canUpdateSubscription = checkPermissions(
|
|
PermissionAction.BILLING_WRITE,
|
|
'stripe.subscriptions'
|
|
)
|
|
|
|
const subscriptionTier = ui.selectedProject?.subscription_tier
|
|
const projectHasNoLimits =
|
|
subscriptionTier === PRICING_TIER_PRODUCT_IDS.PAYG ||
|
|
subscriptionTier === PRICING_TIER_PRODUCT_IDS.TEAM ||
|
|
subscriptionTier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE
|
|
|
|
const showUsageExceedMessage = subscriptionTier !== undefined && !projectHasNoLimits
|
|
|
|
const planNames = {
|
|
[PRICING_TIER_PRODUCT_IDS.FREE]: 'Free',
|
|
[PRICING_TIER_PRODUCT_IDS.PRO]: 'Pro',
|
|
[PRICING_TIER_PRODUCT_IDS.PAYG]: 'Pro',
|
|
[PRICING_TIER_PRODUCT_IDS.TEAM]: 'Team',
|
|
[PRICING_TIER_PRODUCT_IDS.ENTERPRISE]: 'Enterprise',
|
|
}
|
|
|
|
const planName = subscriptionTier ? planNames[subscriptionTier] || 'current' : 'current'
|
|
|
|
useEffect(() => {
|
|
if (error) {
|
|
ui.setNotification({
|
|
category: 'error',
|
|
message: `Failed to get project's usage data: ${(error as any)?.message ?? 'unknown'}`,
|
|
})
|
|
}
|
|
}, [error])
|
|
|
|
if (!isLoading && error) {
|
|
return (
|
|
<InformationBox
|
|
hideCollapse
|
|
defaultVisibility
|
|
icon={<IconAlertCircle strokeWidth={2} />}
|
|
title="There was an issue loading the usage details of your project"
|
|
/>
|
|
)
|
|
}
|
|
|
|
const isPaidTier = subscriptionTier !== PRICING_TIER_PRODUCT_IDS.FREE
|
|
|
|
const featureFootnotes: Record<string, JSX.Element | null> = {
|
|
db_size: isPaidTier ? (
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex flex-row space-x-4 text-scale-1000">
|
|
{usage?.disk_volume_size_gb && <span>Disk Size: {usage.disk_volume_size_gb} GB</span>}
|
|
<Badge>Auto-Scaling</Badge>
|
|
</div>
|
|
|
|
<Button type="default" icon={<IconExternalLink size={14} strokeWidth={1.5} />}>
|
|
<a target="_blank" href="https://supabase.com/docs/guides/platform/database-usage">
|
|
What is disk size?
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
) : null,
|
|
}
|
|
|
|
return (
|
|
<Loading active={isLoading}>
|
|
{usage && (
|
|
<div>
|
|
{USAGE_BASED_PRODUCTS.map((product) => {
|
|
const isExceededUsage =
|
|
showUsageExceedMessage &&
|
|
product.features
|
|
.map((feature) => {
|
|
const featureUsage = usage[feature.key as ProjectUsageResponseUsageKeys]
|
|
return (featureUsage.usage ?? 0) / featureUsage.limit > 1
|
|
})
|
|
.some((x) => x === true)
|
|
|
|
return (
|
|
<div
|
|
key={product.title}
|
|
className={[
|
|
'mb-8 overflow-hidden rounded border',
|
|
'border-panel-border-light dark:border-panel-border-dark',
|
|
].join(' ')}
|
|
>
|
|
<table className="w-full bg-panel-body-light dark:bg-panel-body-dark">
|
|
{/* Header */}
|
|
<thead className="bg-panel-header-light dark:bg-panel-header-dark">
|
|
<tr className="overflow-hidden rounded">
|
|
<th className="w-1/4 px-6 py-3 text-left">
|
|
<div className="flex items-center space-x-4">
|
|
<div
|
|
className={[
|
|
'flex h-8 w-8 items-center justify-center',
|
|
'rounded bg-scale-500 dark:bg-white',
|
|
].join(' ')}
|
|
>
|
|
{product.icon}
|
|
</div>
|
|
<h5 className="mb-0">{product.title}</h5>
|
|
</div>
|
|
</th>
|
|
{/* Plan Limits */}
|
|
<th className="hidden p-3 text-xs font-medium leading-4 text-left text-gray-400 lg:table-cell">
|
|
{isExceededUsage && <Badge color="red">Exceeded usage</Badge>}
|
|
</th>
|
|
{/* Usage */}
|
|
<th className="p-3 text-xs font-medium leading-4 text-left text-gray-400" />
|
|
</tr>
|
|
</thead>
|
|
{/* Line items */}
|
|
{usage === undefined ? (
|
|
<div className="px-4 pt-1 pb-4 w-96">
|
|
<ShimmeringLoader />
|
|
</div>
|
|
) : (
|
|
<tbody>
|
|
{product.features.map((feature) => {
|
|
const featureUsage = usage[feature.key as ProjectUsageResponseUsageKeys]
|
|
const usageValue = featureUsage.usage || 0
|
|
const usageRatio = usageValue / featureUsage.limit
|
|
const isApproaching = usageRatio >= USAGE_APPROACHING_THRESHOLD
|
|
const isExceeded = showUsageExceedMessage && usageRatio >= 1
|
|
const isAvailableInPlan = featureUsage.available_in_plan
|
|
|
|
let usageElement
|
|
if (!isAvailableInPlan) {
|
|
usageElement = (
|
|
<div className="flex items-center justify-between">
|
|
<span>Not included in {planName} tier</span>
|
|
{canUpdateSubscription && (
|
|
<Button
|
|
size="tiny"
|
|
onClick={() =>
|
|
router.push(`/project/${projectRef}/settings/billing/update`)
|
|
}
|
|
>
|
|
Upgrade to Pro
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)
|
|
} else if (showUsageExceedMessage) {
|
|
usageElement = (
|
|
<SparkBar
|
|
type="horizontal"
|
|
barClass={`${
|
|
isExceeded
|
|
? 'bg-red-900'
|
|
: isApproaching
|
|
? 'bg-yellow-900'
|
|
: 'bg-brand-900'
|
|
}`}
|
|
value={usageValue}
|
|
max={featureUsage.limit}
|
|
labelBottom={
|
|
feature.units === 'bytes'
|
|
? formatBytes(usageValue)
|
|
: usageValue.toLocaleString()
|
|
}
|
|
labelTop={
|
|
feature.units === 'bytes'
|
|
? formatBytes(featureUsage.limit)
|
|
: featureUsage.limit.toLocaleString()
|
|
}
|
|
/>
|
|
)
|
|
} else {
|
|
usageElement = (
|
|
<span>
|
|
{feature.units === 'bytes' ? formatBytes(usageValue) : usageValue}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
return [
|
|
<tr
|
|
key={feature.title}
|
|
className="border-t border-panel-border-light dark:border-panel-border-dark"
|
|
>
|
|
<td className="px-6 py-3 text-sm whitespace-nowrap text-scale-1200">
|
|
{feature.title}
|
|
{feature.tooltip && (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger>
|
|
<IconInfo className="ml-2" size={14} strokeWidth={2} />
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'max-w-md', // size
|
|
'flex items-center justify-center',
|
|
'rounded bg-scale-100 py-1 px-2 leading-none shadow', // background
|
|
'border border-scale-200', //border
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-center text-scale-1200">
|
|
{feature.tooltip}
|
|
</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
)}
|
|
</td>
|
|
{ui.selectedProject?.subscription_tier !== undefined && (
|
|
<>
|
|
{showUsageExceedMessage && (
|
|
<td className="hidden w-1/5 p-3 text-sm whitespace-nowrap text-scale-1200 lg:table-cell">
|
|
{isAvailableInPlan ? (
|
|
<>{(usageRatio * 100).toFixed(2)} %</>
|
|
) : (
|
|
<>-</>
|
|
)}
|
|
</td>
|
|
)}
|
|
<td className="px-6 py-3 text-sm text-scale-1200">
|
|
{usageElement}
|
|
</td>
|
|
</>
|
|
)}
|
|
</tr>,
|
|
featureFootnotes[feature.key] && (
|
|
<tr key={`${feature.title}-footnote`}>
|
|
<td
|
|
className="whitespace-nowrap px-6 py-3 text-sm text-scale-1200"
|
|
colSpan={3}
|
|
>
|
|
{featureFootnotes[feature.key]}
|
|
</td>
|
|
</tr>
|
|
),
|
|
]
|
|
})}
|
|
</tbody>
|
|
)}
|
|
</table>
|
|
|
|
{isReadOnlyMode && product.title === 'Database' && (
|
|
<div className="p-6">
|
|
<Alert title={'Database is in read-only mode'} variant="danger" withIcon>
|
|
<p>
|
|
{isPaidTier ? (
|
|
<>
|
|
Your disk has reached 95% capacity and has entered{' '}
|
|
<a
|
|
href={`https://supabase.com/docs/guides/platform/database-usage#paid-tier-disk-auto-scaling`}
|
|
className="underline transition hover:text-scale-1200"
|
|
>
|
|
read-only mode
|
|
</a>
|
|
.
|
|
</>
|
|
) : (
|
|
<>
|
|
You have exceeded the 500mb Database size limit and your project is now
|
|
in{' '}
|
|
<a
|
|
href={`https://supabase.com/docs/guides/platform/database-usage#free-tier-project-read-only-mode`}
|
|
className="underline transition hover:text-scale-1200"
|
|
>
|
|
read-only mode
|
|
</a>
|
|
.
|
|
</>
|
|
)}
|
|
</p>
|
|
{isPaidTier ? (
|
|
<>
|
|
<p>
|
|
For Pro and Enterprise projects,{' '}
|
|
<a
|
|
href="https://supabase.com/docs/guides/platform/database-usage#paid-tier-disk-auto-scaling"
|
|
className="underline transition hover:text-scale-1200"
|
|
>
|
|
Disk Size expands automatically
|
|
</a>{' '}
|
|
when it reaches 90% capacity, but can only occur once every six hours.
|
|
If the disk size has already expanded and then reaches 95% capacity
|
|
within 6 hours, then{' '}
|
|
<a
|
|
href="https://supabase.com/docs/guides/platform/database-usage#paid-tier-project-read-only-mode"
|
|
className="underline transition hover:text-scale-1200"
|
|
>
|
|
your disk will enter read-only mode
|
|
</a>{' '}
|
|
until it can resize again after 6 hours.
|
|
</p>
|
|
<p className="mt-2">
|
|
If you require help or need your disk changed to read/write mode so you
|
|
can delete data,{' '}
|
|
<Link
|
|
href={`/support/new?ref=${
|
|
projectRef ? projectRef : ''
|
|
}&category=Database_unresponsive&Subject=Read%20only%20mode%20issue`}
|
|
>
|
|
<a className="underline transition hover:text-scale-1200">
|
|
you can contact the support team
|
|
</a>
|
|
</Link>
|
|
.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<p>
|
|
You can either{' '}
|
|
<a
|
|
href="https://supabase.com/docs/guides/platform/database-usage#increasing-available-disk-size"
|
|
className="underline transition hover:text-scale-1200"
|
|
>
|
|
reduce your disk usage below 500mb
|
|
</a>{' '}
|
|
or{' '}
|
|
<Link
|
|
href={`/project/${
|
|
projectRef ? projectRef : ''
|
|
}/settings/billing/subscription`}
|
|
>
|
|
<a className="underline transition hover:text-scale-1200">
|
|
upgrade your project
|
|
</a>
|
|
</Link>
|
|
.
|
|
</p>
|
|
)}
|
|
|
|
{!isPaidTier ? (
|
|
<Button type="danger" className="mt-3">
|
|
<a
|
|
target="_blank"
|
|
href="https://supabase.com/docs/guides/platform/database-usage#database-storage-management"
|
|
>
|
|
Upgrade this project
|
|
</a>
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="danger"
|
|
icon={<IconBookOpen size={14} strokeWidth={1.5} />}
|
|
className="mt-3"
|
|
>
|
|
<a
|
|
target="_blank"
|
|
href="https://supabase.com/docs/guides/platform/database-usage#database-storage-management"
|
|
>
|
|
Database storage management docs
|
|
</a>
|
|
</Button>
|
|
)}
|
|
</Alert>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</Loading>
|
|
)
|
|
}
|
|
|
|
export default ProjectUsage
|