feat: new disk size usage section (#29862)
This commit is contained in:
@@ -95,10 +95,11 @@ export const BILLING_BREAKDOWN_METRICS: Metric[] = [
|
||||
{
|
||||
key: PricingMetric.DISK_SIZE_GB_HOURS_GP3,
|
||||
name: 'Disk Size',
|
||||
anchor: 'diskSize',
|
||||
units: 'absolute',
|
||||
unitName: 'GB-Hrs',
|
||||
category: 'Database',
|
||||
tip: 'Each project gets provisioned with 8 GB of GP3 disk for free. When you get close to the disk size limit, we autoscale your disk by 1.5x. Each GB of provisioned disk size beyond 8 GB incurs a GB-Hr every hour. Each extra GB is billed at $0.125/month, prorated down to the hour.',
|
||||
tip: 'Each project gets provisioned with 8 GB of GP3 disk for free. When you get close to the disk size limit, we autoscale your disk by 1.5x. Each GB of provisioned disk size beyond 8 GB incurs a GB-Hr every hour. Each extra GB is billed at $0.125/month ($0.000171/GB-Hr), prorated down to the hour.',
|
||||
docLink: {
|
||||
title: 'Read more',
|
||||
url: 'https://supabase.com/docs/guides/platform/org-based-billing#disk-size',
|
||||
@@ -107,6 +108,7 @@ export const BILLING_BREAKDOWN_METRICS: Metric[] = [
|
||||
{
|
||||
key: PricingMetric.DISK_SIZE_GB_HOURS_IO2,
|
||||
name: 'Disk Size IO2',
|
||||
anchor: 'diskSize',
|
||||
units: 'absolute',
|
||||
unitName: 'GB-Hrs',
|
||||
category: 'Database',
|
||||
|
||||
@@ -174,7 +174,7 @@ const BillingMetric = ({
|
||||
{subscription.usage_billing_enabled === false &&
|
||||
relativeToSubscription &&
|
||||
(isApproachingLimit || isExceededLimit) && (
|
||||
<div className="mt-2">
|
||||
<div className="my-2">
|
||||
<p className="text-sm">
|
||||
Exceeding your plans included usage will lead to restrictions to your project.
|
||||
Upgrade to a usage-based plan or disable the spend cap to avoid restrictions.
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
|
||||
import { CriticalIcon, WarningIcon } from 'ui'
|
||||
import { PricingMetric } from 'data/analytics/org-daily-stats-query'
|
||||
|
||||
export const Restriction = () => {
|
||||
const org = useSelectedOrganization()
|
||||
@@ -18,7 +19,10 @@ export const Restriction = () => {
|
||||
const hasExceededAnyLimits = Boolean(
|
||||
usage?.usages.find(
|
||||
(metric) =>
|
||||
!metric.unlimited && metric.capped && metric.usage > (metric?.pricing_free_units ?? 0)
|
||||
metric.metric !== PricingMetric.DISK_SIZE_GB_HOURS_GP3 &&
|
||||
!metric.unlimited &&
|
||||
metric.capped &&
|
||||
metric.usage > (metric?.pricing_free_units ?? 0)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -100,73 +100,93 @@ export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[
|
||||
name: 'Database & Storage Size',
|
||||
description: 'Amount of resources your project is consuming',
|
||||
attributes: [
|
||||
{
|
||||
anchor: 'dbSize',
|
||||
key: PricingMetric.DATABASE_SIZE,
|
||||
attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }],
|
||||
name: 'Database size',
|
||||
chartPrefix: 'Average',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
subscription?.usage_based_billing_project_addons === true
|
||||
? 'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks and are billed based on provisioned disk size, rather than database space used.'
|
||||
: 'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks.\nBilling is based on the average daily database size used in GB throughout the billing period. Billing is independent of the provisioned disk size.',
|
||||
links: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://supabase.com/docs/guides/platform/database-size',
|
||||
subscription?.plan.id === 'free' || subscription?.usage_based_billing_project_addons !== true
|
||||
? {
|
||||
anchor: 'dbSize',
|
||||
key: PricingMetric.DATABASE_SIZE,
|
||||
attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }],
|
||||
name: 'Database size',
|
||||
chartPrefix: 'Average',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
'Database size refers to the monthly average database space usage, as reported by Postgres. Paid Plans use auto-scaling disks.\nBilling is based on the average daily database size used in GB throughout the billing period. Billing is independent of the provisioned disk size.',
|
||||
links: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://supabase.com/docs/guides/platform/database-size',
|
||||
},
|
||||
...(subscription?.usage_based_billing_project_addons === true
|
||||
? [
|
||||
{
|
||||
name: 'Disk Management',
|
||||
url: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
chartDescription: 'The data refreshes every 24 hours.',
|
||||
additionalInfo: (usage?: OrgUsageResponse) => {
|
||||
const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE)
|
||||
const usageRatio =
|
||||
typeof usageMeta !== 'number'
|
||||
? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0)
|
||||
: 0
|
||||
const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0
|
||||
|
||||
const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
const isExceededLimit = hasLimit && usageRatio >= 1
|
||||
const isCapped = usageMeta?.capped
|
||||
|
||||
const onFreePlan = subscription?.plan?.name === 'Free'
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(isApproachingLimit || isExceededLimit) && isCapped && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant={isExceededLimit ? 'danger' : 'warning'}
|
||||
title={
|
||||
isExceededLimit
|
||||
? 'Exceeding database size limit'
|
||||
: 'Nearing database size limit'
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
|
||||
<div>
|
||||
When you reach your database size limit, your project can go into
|
||||
read-only mode.{' '}
|
||||
{onFreePlan
|
||||
? 'Please upgrade your Plan.'
|
||||
: "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."}
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
: {
|
||||
anchor: 'diskSize',
|
||||
key: 'diskSize',
|
||||
attributes: [],
|
||||
name: 'Disk size',
|
||||
chartPrefix: 'Average',
|
||||
unit: 'bytes',
|
||||
description:
|
||||
"Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).",
|
||||
links: [
|
||||
{
|
||||
name: 'Documentation',
|
||||
url: 'https://supabase.com/docs/guides/platform/org-based-billing#disk-size',
|
||||
},
|
||||
{
|
||||
name: 'Disk Management',
|
||||
url: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
|
||||
},
|
||||
],
|
||||
chartDescription: '',
|
||||
},
|
||||
...(subscription?.usage_based_billing_project_addons === true
|
||||
? [
|
||||
{
|
||||
name: 'Disk Management',
|
||||
url: 'https://supabase.com/docs/guides/platform/database-size#disk-management',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
chartDescription: 'The data refreshes every 24 hours.',
|
||||
additionalInfo: (usage?: OrgUsageResponse) => {
|
||||
const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE)
|
||||
const usageRatio =
|
||||
typeof usageMeta !== 'number'
|
||||
? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0)
|
||||
: 0
|
||||
const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0
|
||||
|
||||
const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD
|
||||
const isExceededLimit = hasLimit && usageRatio >= 1
|
||||
const isCapped = usageMeta?.capped
|
||||
|
||||
const onFreePlan = subscription?.plan?.name === 'Free'
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(isApproachingLimit || isExceededLimit) && isCapped && (
|
||||
<Alert
|
||||
withIcon
|
||||
variant={isExceededLimit ? 'danger' : 'warning'}
|
||||
title={
|
||||
isExceededLimit
|
||||
? 'Exceeding database size limit'
|
||||
: 'Nearing database size limit'
|
||||
}
|
||||
>
|
||||
<div className="flex w-full items-center flex-col justify-center space-y-2 md:flex-row md:justify-between">
|
||||
<div>
|
||||
When you reach your database size limit, your project can go into read-only
|
||||
mode.{' '}
|
||||
{onFreePlan
|
||||
? 'Please upgrade your Plan.'
|
||||
: 'Disable your spend cap to scale seamlessly and pay for over-usage beyond your Plans quota.'}
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
anchor: 'storageSize',
|
||||
key: PricingMetric.STORAGE_SIZE,
|
||||
|
||||
@@ -57,20 +57,23 @@ const Usage = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectRef, isSuccess])
|
||||
|
||||
const billingCycleStart = dayjs.unix(subscription?.current_period_start ?? 0).utc()
|
||||
const billingCycleEnd = dayjs.unix(subscription?.current_period_end ?? 0).utc()
|
||||
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 || !subscription) return true
|
||||
|
||||
const { current_period_start, current_period_end } = subscription
|
||||
if (!dateRange?.period_start || !dateRange?.period_end) return true
|
||||
|
||||
return (
|
||||
dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) &&
|
||||
dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000))
|
||||
dayjs(dateRange.period_start.date).isSame(billingCycleStart) &&
|
||||
dayjs(dateRange.period_end.date).isSame(billingCycleEnd)
|
||||
)
|
||||
}, [dateRange, subscription])
|
||||
}, [dateRange, billingCycleStart, billingCycleEnd])
|
||||
|
||||
const startDate = useMemo(() => {
|
||||
// If end date is in future, set end date to now
|
||||
|
||||
@@ -174,7 +174,7 @@ const AttributeUsage = ({
|
||||
{usageMeta && (
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-foreground-light">
|
||||
Included in {subscription?.plan?.name.toLowerCase()} plan
|
||||
Included in {subscription?.plan?.name} Plan
|
||||
</p>
|
||||
{usageMeta.unlimited ? (
|
||||
<p className="text-xs">Unlimited</p>
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import type { OrgSubscription } from 'data/subscriptions/types'
|
||||
import SectionContent from '../SectionContent'
|
||||
import { CategoryAttribute } from '../Usage.constants'
|
||||
import { useOrgProjectsQuery } from 'data/projects/org-projects'
|
||||
import { PROJECT_STATUS } from 'lib/constants'
|
||||
import {
|
||||
Button,
|
||||
Alert_Shadcn_,
|
||||
CriticalIcon,
|
||||
AlertTitle_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
} from 'ui'
|
||||
import MotionNumber from 'motion-number'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import { InfoTooltip } from 'ui-patterns/info-tooltip'
|
||||
import { OrgUsageResponse } from 'data/usage/org-usage-query'
|
||||
import { PricingMetric } from 'data/analytics/org-daily-stats-query'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
export interface DiskUsageProps {
|
||||
slug: string
|
||||
projectRef?: string
|
||||
attribute: CategoryAttribute
|
||||
subscription?: OrgSubscription
|
||||
usage?: OrgUsageResponse
|
||||
|
||||
currentBillingCycleSelected: boolean
|
||||
}
|
||||
|
||||
const DiskUsage = ({
|
||||
slug,
|
||||
projectRef,
|
||||
attribute,
|
||||
subscription,
|
||||
usage,
|
||||
currentBillingCycleSelected,
|
||||
}: DiskUsageProps) => {
|
||||
const {
|
||||
data: diskUsage,
|
||||
isError,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
error,
|
||||
} = useOrgProjectsQuery(
|
||||
{
|
||||
orgSlug: slug,
|
||||
},
|
||||
{
|
||||
enabled: currentBillingCycleSelected,
|
||||
}
|
||||
)
|
||||
|
||||
const hasProjectsExceedingDiskSize = useMemo(() => {
|
||||
if (diskUsage) {
|
||||
return diskUsage.projects.some((it) =>
|
||||
it.databases.some(
|
||||
(db) =>
|
||||
db.type === 'READ_REPLICA' || (db.disk_volume_size_gb && db.disk_volume_size_gb > 8)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [diskUsage])
|
||||
|
||||
const gp3UsageInPeriod = usage?.usages.find((it) => it.metric === PricingMetric.DISK_IOPS_GP3)
|
||||
const io2UsageInPeriod = usage?.usages.find((it) => it.metric === PricingMetric.DISK_IOPS_IO2)
|
||||
|
||||
const relevantProjects = useMemo(() => {
|
||||
return diskUsage
|
||||
? diskUsage.projects
|
||||
.filter((it) => !it.is_branch && it.status !== PROJECT_STATUS.INACTIVE)
|
||||
.filter((it) => it.ref === projectRef || !projectRef)
|
||||
: []
|
||||
}, [diskUsage, projectRef])
|
||||
|
||||
return (
|
||||
<div id={attribute.anchor} className="scroll-my-12">
|
||||
<SectionContent section={attribute}>
|
||||
{isLoading && (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
{isError && <AlertError subject="Failed to retrieve usage data" error={error} />}
|
||||
{isSuccess && (
|
||||
<div className="space-y-4">
|
||||
{currentBillingCycleSelected &&
|
||||
subscription?.usage_billing_enabled === false &&
|
||||
hasProjectsExceedingDiskSize && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<CriticalIcon />
|
||||
<AlertTitle_Shadcn_>Projects exceeding quota</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
You have projects that are exceeding 8 GB of provisioned disk size, but do not
|
||||
allow any overages with the Spend Cap on. Reduce the disk size or disable the
|
||||
spend cap.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-sm">{attribute.name} usage</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-b py-1">
|
||||
<p className="text-xs text-foreground-light">
|
||||
Included in {subscription?.plan?.name} Plan
|
||||
</p>
|
||||
<p className="text-xs">8 GB GP3 disk per project</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-foreground-light">Overages in period</p>
|
||||
<p className="text-xs">
|
||||
{gp3UsageInPeriod?.usage ?? 0} GP3 GB-Hrs
|
||||
{io2UsageInPeriod?.usage ? ` / ${io2UsageInPeriod.usage} IO2 GB-Hrs` : ``}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentBillingCycleSelected ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm">Current disk size per project</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Breakdown of disk per project. Head to your project's disk management section to
|
||||
see database space used.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{relevantProjects.length === 0 && (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-sm">No active projects</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
You don't have any active projects in this organization.
|
||||
</p>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
|
||||
{relevantProjects.map((project, idx) => {
|
||||
const primaryDiskUsage = project.databases
|
||||
.filter((it) => it.type === 'PRIMARY')
|
||||
.reduce((acc, curr) => acc + (curr.disk_volume_size_gb ?? 8), 0)
|
||||
const replicaDbs = project.databases.filter((it) => it.type !== 'PRIMARY')
|
||||
const replicaDiskUsage = replicaDbs.reduce(
|
||||
(acc, curr) => acc + (curr.disk_volume_size_gb ?? 8),
|
||||
0
|
||||
)
|
||||
|
||||
const totalDiskUsage = primaryDiskUsage + replicaDiskUsage
|
||||
|
||||
return (
|
||||
<div className={idx !== relevantProjects.length - 1 ? 'border-b pb-2' : ''}>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-foreground-light flex items-center gap-2">
|
||||
{project.name}
|
||||
</span>
|
||||
<Button asChild type="default" size={'tiny'}>
|
||||
<Link href={`/project/${project.ref}/settings/database#disk-management`}>
|
||||
Manage Disk
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center h-6 gap-3">
|
||||
<span className="text-foreground-light text-sm font-mono flex items-center gap-2">
|
||||
<span className="text-foreground font-semibold -mt-[2px]">
|
||||
<MotionNumber
|
||||
value={totalDiskUsage}
|
||||
style={{ lineHeight: 0.8 }}
|
||||
transition={{
|
||||
y: { type: 'spring', duration: 0.35, bounce: 0 },
|
||||
}}
|
||||
className="font-mono"
|
||||
/>
|
||||
</span>{' '}
|
||||
GB Disk provisioned
|
||||
</span>
|
||||
<InfoTooltip side="top">
|
||||
<p>{primaryDiskUsage} GB for Primary Database</p>
|
||||
{replicaDbs.length > 0 && (
|
||||
<>
|
||||
<p>
|
||||
{replicaDiskUsage} GB for {replicaDbs.length} Read{' '}
|
||||
{replicaDbs.length === 1 ? 'Replica' : 'Replicas'}
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
Read replicas have their own disk and use 25% more disk to account
|
||||
for WAL files.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<p className="text-sm">Data not available</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Switch to current billing cycle to see current disk size per project.
|
||||
</p>
|
||||
</div>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SectionContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiskUsage
|
||||
@@ -5,6 +5,7 @@ import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
import SectionHeader from '../SectionHeader'
|
||||
import { CategoryMetaKey, USAGE_CATEGORIES } from '../Usage.constants'
|
||||
import AttributeUsage from './AttributeUsage'
|
||||
import DiskUsage from './DiskUsage'
|
||||
|
||||
export interface ChartMeta {
|
||||
[key: string]: { data: DataPoint[]; margin: number; isLoading: boolean }
|
||||
@@ -48,23 +49,35 @@ const UsageSection = ({
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
{categoryMeta.attributes.map((attribute) => (
|
||||
<AttributeUsage
|
||||
key={attribute.name}
|
||||
slug={orgSlug}
|
||||
projectRef={projectRef}
|
||||
attribute={attribute}
|
||||
usage={usage}
|
||||
usageMeta={usage?.usages.find((x) => x.metric === attribute.key)}
|
||||
chartMeta={chartMeta}
|
||||
subscription={subscription}
|
||||
error={usageError}
|
||||
isLoading={isLoadingUsage}
|
||||
isError={isErrorUsage}
|
||||
isSuccess={isSuccessUsage}
|
||||
currentBillingCycleSelected={currentBillingCycleSelected}
|
||||
/>
|
||||
))}
|
||||
{categoryMeta.attributes.map((attribute) =>
|
||||
attribute.key === 'diskSize' ? (
|
||||
<DiskUsage
|
||||
key={attribute.name}
|
||||
slug={orgSlug}
|
||||
projectRef={projectRef}
|
||||
attribute={attribute}
|
||||
subscription={subscription}
|
||||
currentBillingCycleSelected={currentBillingCycleSelected}
|
||||
usage={usage}
|
||||
/>
|
||||
) : (
|
||||
<AttributeUsage
|
||||
key={attribute.name}
|
||||
slug={orgSlug}
|
||||
projectRef={projectRef}
|
||||
attribute={attribute}
|
||||
usage={usage}
|
||||
usageMeta={usage?.usages.find((x) => x.metric === attribute.key)}
|
||||
chartMeta={chartMeta}
|
||||
subscription={subscription}
|
||||
error={usageError}
|
||||
isLoading={isLoadingUsage}
|
||||
isError={isErrorUsage}
|
||||
isSuccess={isSuccessUsage}
|
||||
currentBillingCycleSelected={currentBillingCycleSelected}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@ export const projectKeys = {
|
||||
) => ['projects', 'transfer', projectRef, targetOrganizationSlug, 'preview'] as const,
|
||||
pauseStatus: (projectRef: string | undefined) =>
|
||||
['projects', projectRef, 'pause-status'] as const,
|
||||
|
||||
orgProjects: (slug: string | undefined) => ['projects', 'org', slug] as const,
|
||||
}
|
||||
|
||||
43
apps/studio/data/projects/org-projects.ts
Normal file
43
apps/studio/data/projects/org-projects.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import type { components } from 'data/api'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { projectKeys } from './keys'
|
||||
|
||||
export type OrgProjectsVariables = {
|
||||
orgSlug?: string
|
||||
}
|
||||
|
||||
export type OrgProjectsResponse = components['schemas']['OrganizationProjectsResponse']
|
||||
|
||||
export async function getOrgProjects(
|
||||
{ orgSlug }: OrgProjectsVariables,
|
||||
signal?: AbortSignal
|
||||
): Promise<OrgProjectsResponse> {
|
||||
if (!orgSlug) throw new Error('orgSlug is required')
|
||||
const { data, error } = await get(`/platform/organizations/{slug}/projects`, {
|
||||
params: {
|
||||
path: { slug: orgSlug },
|
||||
},
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type OrgProjectsData = Awaited<ReturnType<typeof getOrgProjects>>
|
||||
export type OrgProjectsError = ResponseError
|
||||
|
||||
export const useOrgProjectsQuery = <TData = OrgProjectsData>(
|
||||
{ orgSlug }: OrgProjectsVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<OrgProjectsData, OrgProjectsError, TData> = {}
|
||||
) =>
|
||||
useQuery<OrgProjectsData, OrgProjectsError, TData>(
|
||||
projectKeys.orgProjects(orgSlug),
|
||||
({ signal }) => getOrgProjects({ orgSlug }, signal),
|
||||
{
|
||||
enabled: enabled && typeof orgSlug !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
85
packages/api-types/types/api.d.ts
vendored
85
packages/api-types/types/api.d.ts
vendored
@@ -457,6 +457,10 @@ export interface paths {
|
||||
/** Sets up a payment method */
|
||||
post: operations['SetupIntentOrgController_setUpPaymentMethod']
|
||||
}
|
||||
'/platform/organizations/{slug}/projects': {
|
||||
/** Gets all projects for the given organization */
|
||||
get: operations['OrganizationProjectsController_getProjects']
|
||||
}
|
||||
'/platform/organizations/{slug}/roles': {
|
||||
/** Gets the given organization's roles */
|
||||
get: operations['OrganizationRolesController_getAllRolesV2']
|
||||
@@ -3116,6 +3120,21 @@ export interface components {
|
||||
| 'RESTARTING'
|
||||
| 'RESIZING'
|
||||
}
|
||||
/** @enum {string} */
|
||||
DatabaseStatus:
|
||||
| 'ACTIVE_HEALTHY'
|
||||
| 'ACTIVE_UNHEALTHY'
|
||||
| 'COMING_UP'
|
||||
| 'GOING_DOWN'
|
||||
| 'INIT_FAILED'
|
||||
| 'REMOVED'
|
||||
| 'RESTORING'
|
||||
| 'UNKNOWN'
|
||||
| 'UPGRADING'
|
||||
| 'INIT_READ_REPLICA'
|
||||
| 'INIT_READ_REPLICA_FAILED'
|
||||
| 'RESTARTING'
|
||||
| 'RESIZING'
|
||||
DatabaseStatusResponse: {
|
||||
identifier: string
|
||||
replicaInitializationStatus?: Record<string, never>
|
||||
@@ -3135,6 +3154,8 @@ export interface components {
|
||||
| 'RESTARTING'
|
||||
| 'RESIZING'
|
||||
}
|
||||
/** @enum {string} */
|
||||
DatabaseType: 'PRIMARY' | 'READ_REPLICA'
|
||||
DatabaseUpgradeStatus: {
|
||||
/** @enum {string} */
|
||||
error?:
|
||||
@@ -4169,6 +4190,9 @@ export interface components {
|
||||
*/
|
||||
supabase_org_id: string
|
||||
}
|
||||
OrganizationProjectsResponse: {
|
||||
projects: components['schemas']['ProjectWithDatabases'][]
|
||||
}
|
||||
OrganizationResponse: {
|
||||
billing_email: string | null
|
||||
id: number
|
||||
@@ -4669,6 +4693,19 @@ export interface components {
|
||||
release_channel: components['schemas']['ReleaseChannel']
|
||||
version: string
|
||||
}
|
||||
ProjectDatabase: {
|
||||
cloud_provider: string
|
||||
disk_last_modified_at?: string
|
||||
disk_throughput_mbps?: number
|
||||
/** @enum {string} */
|
||||
disk_type?: 'gp3' | 'io2'
|
||||
disk_volume_size_gb?: number
|
||||
identifier: string
|
||||
infra_compute_size?: components['schemas']['DbInstanceSize']
|
||||
region: string
|
||||
status: components['schemas']['DatabaseStatus']
|
||||
type: components['schemas']['DatabaseType']
|
||||
}
|
||||
ProjectDetailResponse: {
|
||||
cloud_provider: string
|
||||
connectionString: string
|
||||
@@ -4852,6 +4889,23 @@ export interface components {
|
||||
ssl_enforced: boolean
|
||||
status: string
|
||||
}
|
||||
/** @enum {string} */
|
||||
ProjectStatus:
|
||||
| 'ACTIVE_HEALTHY'
|
||||
| 'ACTIVE_UNHEALTHY'
|
||||
| 'COMING_UP'
|
||||
| 'GOING_DOWN'
|
||||
| 'INACTIVE'
|
||||
| 'INIT_FAILED'
|
||||
| 'REMOVED'
|
||||
| 'RESTARTING'
|
||||
| 'UNKNOWN'
|
||||
| 'UPGRADING'
|
||||
| 'PAUSING'
|
||||
| 'RESTORING'
|
||||
| 'RESTORE_FAILED'
|
||||
| 'PAUSE_FAILED'
|
||||
| 'RESIZING'
|
||||
ProjectUnpauseVersionInfo: {
|
||||
postgres_engine: components['schemas']['PostgresEngine']
|
||||
release_channel: components['schemas']['ReleaseChannel']
|
||||
@@ -4876,6 +4930,14 @@ export interface components {
|
||||
postgres_version: components['schemas']['PostgresEngine']
|
||||
release_channel: components['schemas']['ReleaseChannel']
|
||||
}
|
||||
ProjectWithDatabases: {
|
||||
databases: components['schemas']['ProjectDatabase'][]
|
||||
is_branch: boolean
|
||||
name: string
|
||||
ref: string
|
||||
region: string
|
||||
status: components['schemas']['ProjectStatus']
|
||||
}
|
||||
Provider: {
|
||||
created_at?: string
|
||||
domains?: components['schemas']['Domain'][]
|
||||
@@ -9195,6 +9257,29 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets all projects for the given organization */
|
||||
OrganizationProjectsController_getProjects: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Organization slug */
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['OrganizationProjectsResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to retrieve projects */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets the given organization's roles */
|
||||
OrganizationRolesController_getAllRolesV2: {
|
||||
parameters: {
|
||||
|
||||
Reference in New Issue
Block a user