mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
Merge pull request #17379 from supabase/feat/usage-banner-org
feat: over-usage banners for orgs
This commit is contained in:
4 files changed
+108
-11
No files matched your search
+12
-1
@@ -14,8 +14,10 @@ import { Alert, Button } from 'ui'
|
||||
import { BILLING_BREAKDOWN_METRICS } from './BillingBreakdown.constants'
|
||||
import BillingMetric from './BillingMetric'
|
||||
import UpcomingInvoice from './UpcomingInvoice'
|
||||
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
|
||||
|
||||
const BillingBreakdown = () => {
|
||||
const snap = useOrgSettingsPageStateSnapshot()
|
||||
const { slug: orgSlug } = useParams()
|
||||
const {
|
||||
data: usage,
|
||||
@@ -93,7 +95,16 @@ const BillingBreakdown = () => {
|
||||
variant="danger"
|
||||
title="Your organization's usage has exceeded its included quota"
|
||||
actions={[
|
||||
<Button key="upgrade-button" type="default" className="ml-8" onClick={() => {}}>
|
||||
<Button
|
||||
key="upgrade-button"
|
||||
type="default"
|
||||
className="ml-8"
|
||||
onClick={() =>
|
||||
snap.setPanelKey(
|
||||
currentPlan?.id === 'free' ? 'subscriptionPlan' : 'costControl'
|
||||
)
|
||||
}
|
||||
>
|
||||
{currentPlan?.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}
|
||||
</Button>,
|
||||
]}
|
||||
|
||||
@@ -11,12 +11,13 @@ import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
|
||||
import { Button, IconExternalLink, IconInfo, Listbox } from 'ui'
|
||||
import { Alert, Button, IconExternalLink, IconInfo, Listbox } from 'ui'
|
||||
import Activity from './Activity'
|
||||
import Bandwidth from './Bandwidth'
|
||||
import SizeAndCounts from './SizeAndCounts'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import Link from 'next/link'
|
||||
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
|
||||
const Usage = () => {
|
||||
const { slug, projectRef } = useParams()
|
||||
@@ -32,6 +33,9 @@ const Usage = () => {
|
||||
isError: isErrorSubscription,
|
||||
isSuccess: isSuccessSubscription,
|
||||
} = useOrgSubscriptionQuery({ orgSlug: slug })
|
||||
|
||||
const { data: usage } = useOrgUsageQuery({ orgSlug: slug })
|
||||
|
||||
const orgProjects = projects?.filter((project) => project.organization_id === organization?.id)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -88,6 +92,13 @@ const Usage = () => {
|
||||
? orgProjects?.find((it) => it.ref === selectedProjectRef)
|
||||
: undefined
|
||||
|
||||
const hasExceededAnyLimits = Boolean(
|
||||
usage?.usages.find(
|
||||
(metric) =>
|
||||
!metric.unlimited && metric.capped && metric.usage > (metric?.pricing_free_units ?? 0)
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer className="sticky top-0 border-b bg-scale-200 z-10 overflow-hidden">
|
||||
@@ -156,6 +167,33 @@ const Usage = () => {
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
|
||||
{!selectedProject && subscription && hasExceededAnyLimits && (
|
||||
<ScaffoldContainer className="mt-5">
|
||||
<Alert
|
||||
withIcon
|
||||
variant="danger"
|
||||
title="Your organization's usage has exceeded its included quota"
|
||||
actions={[
|
||||
<Link
|
||||
key="upgrade-button"
|
||||
href={`/org/${slug}/billing?panel=${
|
||||
subscription.plan.id === 'free' ? 'subscriptionPlan' : 'costControl'
|
||||
}`}
|
||||
>
|
||||
<Button asChild key="upgrade-button" type="default" className="ml-8">
|
||||
<a>{subscription.plan.id === 'free' ? 'Upgrade plan' : 'Change spend cap'}</a>
|
||||
</Button>
|
||||
</Link>,
|
||||
]}
|
||||
>
|
||||
Your projects can become unresponsive or enter read only mode.{' '}
|
||||
{subscription.plan.id === 'free'
|
||||
? 'Please upgrade to the Pro plan to ensure that your projects remain available.'
|
||||
: 'Please disable spend cap to ensure that your projects remain available.'}
|
||||
</Alert>
|
||||
</ScaffoldContainer>
|
||||
)}
|
||||
|
||||
{selectedProjectRef && (
|
||||
<ScaffoldContainer className="mt-5">
|
||||
<InformationBox
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { Badge } from 'ui'
|
||||
|
||||
import { getResourcesExceededLimits } from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import {
|
||||
getResourcesExceededLimits,
|
||||
getResourcesExceededLimitsOrg,
|
||||
} from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useProjectReadOnlyQuery } from 'data/config/project-read-only-query'
|
||||
import { useProjectUsageQuery } from 'data/usage/project-usage-query'
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
@@ -14,6 +17,8 @@ import NotificationsPopover from './NotificationsPopover'
|
||||
import OrgDropdown from './OrgDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
@@ -26,26 +31,48 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
})
|
||||
|
||||
// Skip with org-based-billing, as quota is for the entire org
|
||||
const { data: usage } = useProjectUsageQuery(
|
||||
const { data: projectUsage } = useProjectUsageQuery(
|
||||
{ projectRef },
|
||||
{ enabled: selectedOrganization && !selectedOrganization.subscription_id }
|
||||
{ enabled: Boolean(selectedOrganization && !selectedOrganization.subscription_id) }
|
||||
)
|
||||
const resourcesExceededLimits = getResourcesExceededLimits(usage)
|
||||
const { data: orgUsage } = useOrgUsageQuery(
|
||||
{ orgSlug: selectedOrganization?.slug },
|
||||
{ enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) }
|
||||
)
|
||||
|
||||
const exceedingLimits = useMemo(() => {
|
||||
if (orgUsage) {
|
||||
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
|
||||
} else if (projectUsage) {
|
||||
return getResourcesExceededLimits(projectUsage).length > 0
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}, [projectUsage, orgUsage])
|
||||
|
||||
// Skip with org-based-billing, as quota is for the entire org
|
||||
const { data: subscription } = useProjectSubscriptionV2Query(
|
||||
const { data: projectSubscription } = useProjectSubscriptionV2Query(
|
||||
{ projectRef },
|
||||
{ enabled: selectedOrganization && !selectedOrganization.subscription_id }
|
||||
)
|
||||
|
||||
const { data: orgSubscription } = useProjectSubscriptionV2Query(
|
||||
{ projectRef },
|
||||
{ enabled: Boolean(selectedOrganization && selectedOrganization.subscription_id) }
|
||||
)
|
||||
|
||||
const subscription = useMemo(() => {
|
||||
return projectSubscription || orgSubscription
|
||||
}, [projectSubscription, orgSubscription])
|
||||
|
||||
const projectHasNoLimits = subscription?.usage_billing_enabled === true
|
||||
|
||||
const showOverUsageBadge =
|
||||
useFlag('overusageBadge') &&
|
||||
subscription !== undefined &&
|
||||
(subscription.plan.id === 'free' || subscription?.plan.id === 'pro') &&
|
||||
(subscription.plan.id === 'free' || subscription.plan.id === 'pro') &&
|
||||
!projectHasNoLimits &&
|
||||
resourcesExceededLimits.length > 0
|
||||
exceedingLimits
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -95,7 +122,13 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
|
||||
{showOverUsageBadge && (
|
||||
<div className="ml-2">
|
||||
<Link href={`/project/${projectRef}/settings/billing/usage`}>
|
||||
<Link
|
||||
href={
|
||||
selectedOrganization?.subscription_id
|
||||
? `/org/${selectedOrganization.slug}/usage`
|
||||
: `/project/${projectRef}/settings/billing/usage`
|
||||
}
|
||||
>
|
||||
<a>
|
||||
<Badge color="red">Exceeding usage limits</Badge>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { compact } from 'lodash'
|
||||
import { USAGE_APPROACHING_THRESHOLD } from 'lib/constants'
|
||||
import { OrgUsageResponse, UsageMetric } from 'data/usage/org-usage-query'
|
||||
|
||||
export const getResourcesApproachingLimits = (usages: any) => {
|
||||
if (!usages) return []
|
||||
@@ -22,7 +23,21 @@ export const getResourcesExceededLimits = (usages: any) => {
|
||||
.filter((resourceName) => usages[resourceName] !== null)
|
||||
.map((resourceName) => {
|
||||
const resource = usages[resourceName]
|
||||
if (resource.limit > 0 && (resource.usage / resource.limit) > 1) return resourceName
|
||||
if (resource.limit > 0 && resource.usage / resource.limit > 1) return resourceName
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const getResourcesExceededLimitsOrg = (usageMetrics: UsageMetric[]): string[] => {
|
||||
if (!usageMetrics.length) return []
|
||||
|
||||
return usageMetrics
|
||||
.filter((usageMetric) => {
|
||||
if (!usageMetric.capped || !usageMetric.available_in_plan || usageMetric.unlimited) return false
|
||||
|
||||
const freeUnits = usageMetric.pricing_free_units || 0
|
||||
|
||||
return usageMetric.usage > freeUnits
|
||||
})
|
||||
.map((it) => it.metric)
|
||||
}
|
||||
Reference in new issue
Block a user