mirror of
https://github.com/supabase/supabase.git
synced 2026-09-11 04:21:47 +08:00
Follow-up to #48344: collapses the two resolution paths for the Admonition module into one. `src/admonition.tsx` was a back-compat shim re-exporting `src/Admonition/`. Two ways to resolve one module is exactly what produced the macOS self-import bug fixed in #48344, and the local typecheck errors that #48374 worked around. This removes the shim and standardizes on the PascalCase subpath, matching every other export in the package. **Changed:** - Codemodded all 246 `ui-patterns/admonition` imports to `ui-patterns/Admonition` (240 `.tsx`, 5 `.mdx`, 1 `.ts` across studio, docs, www, design-system, and lite-studio) - Pointed the 5 internal `'../admonition'` imports back at the `'../Admonition'` directory **Removed:** - `packages/ui-patterns/src/admonition.tsx`, and its `./admonition` entry in the exports map (regenerated with `pnpm gen:exports`) ## To test - `grep -r "ui-patterns/admonition" --include='*.ts*'` → no hits - `pnpm test:case-hazards` → passes - `pnpm typecheck` → all 15 tasks green - `pnpm --filter studio run lint:ratchet` → passes - `pnpm --filter ui-patterns vitest run src/Admonition` → 11 tests pass <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Standardized Admonition component imports across the application and documentation. * Improved compatibility with case-sensitive environments by using the canonical component path. * Removed the legacy Admonition import entry point. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
281 lines
10 KiB
TypeScript
281 lines
10 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { ChevronRight, ExternalLink } from 'lucide-react'
|
|
import { useTheme } from 'next-themes'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { useEffect, useState } from 'react'
|
|
import { pricing } from 'shared-data/pricing'
|
|
import { toast } from 'sonner'
|
|
import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger, SidePanel } from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
|
|
import Table from '@/components/to-be-cleaned/Table'
|
|
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
|
import { useOrgSubscriptionUpdateMutation } from '@/data/subscriptions/org-subscription-update-mutation'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { BASE_PATH, DOCS_URL, PRICING_TIER_PRODUCT_IDS } from '@/lib/constants'
|
|
import { useOrgSettingsPageStateSnapshot } from '@/state/organization-settings'
|
|
|
|
const SPEND_CAP_OPTIONS: {
|
|
name: string
|
|
value: 'on' | 'off'
|
|
imageUrl: string
|
|
imageUrlLight: string
|
|
}[] = [
|
|
{
|
|
name: 'Spend cap enabled',
|
|
value: 'on',
|
|
imageUrl: `${BASE_PATH}/img/spend-cap-on.png`,
|
|
imageUrlLight: `${BASE_PATH}/img/spend-cap-on--light.png`,
|
|
},
|
|
{
|
|
name: 'Spend cap disabled',
|
|
value: 'off',
|
|
imageUrl: `${BASE_PATH}/img/spend-cap-off.png`,
|
|
imageUrlLight: `${BASE_PATH}/img/spend-cap-off--light.png`,
|
|
},
|
|
]
|
|
|
|
const SpendCapSidePanel = () => {
|
|
const { slug } = useParams()
|
|
const { resolvedTheme } = useTheme()
|
|
|
|
const [showUsageCosts, setShowUsageCosts] = useState(false)
|
|
const [selectedOption, setSelectedOption] = useState<'on' | 'off'>()
|
|
|
|
const { can: canUpdateSpendCap } = useAsyncCheckPermissions(
|
|
PermissionAction.BILLING_WRITE,
|
|
'stripe.subscriptions'
|
|
)
|
|
|
|
const snap = useOrgSettingsPageStateSnapshot()
|
|
const visible = snap.panelKey === 'costControl'
|
|
const onClose = () => snap.setPanelKey(undefined)
|
|
|
|
const { data: subscription, isPending: isLoading } = useOrgSubscriptionQuery({ orgSlug: slug })
|
|
const { mutate: updateOrgSubscription, isPending: isUpdating } = useOrgSubscriptionUpdateMutation(
|
|
{
|
|
onSuccess: () => {
|
|
toast.success(`Successfully ${isTurningOnCap ? 'enabled' : 'disabled'} spend cap`)
|
|
onClose()
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to toggle spend cap: ${error.message}`)
|
|
},
|
|
}
|
|
)
|
|
|
|
const isFreePlan = subscription?.plan?.id === 'free'
|
|
const isSpendCapOn = !subscription?.usage_billing_enabled
|
|
const isTurningOnCap = !isSpendCapOn && selectedOption === 'on'
|
|
const hasChanges = selectedOption !== (isSpendCapOn ? 'on' : 'off')
|
|
|
|
useEffect(() => {
|
|
if (visible && subscription !== undefined) {
|
|
setSelectedOption(isSpendCapOn ? 'on' : 'off')
|
|
}
|
|
}, [visible, isLoading, subscription, isSpendCapOn])
|
|
|
|
const onConfirm = async () => {
|
|
if (!slug) return console.error('Org slug is required')
|
|
|
|
const tier = (
|
|
selectedOption === 'on' ? PRICING_TIER_PRODUCT_IDS.PRO : PRICING_TIER_PRODUCT_IDS.PAYG
|
|
) as 'tier_pro' | 'tier_payg'
|
|
|
|
updateOrgSubscription({ slug, tier })
|
|
}
|
|
|
|
const billingMetricCategories: (keyof typeof pricing)[] = [
|
|
'database',
|
|
'auth',
|
|
'storage',
|
|
'realtime',
|
|
'edge_functions',
|
|
]
|
|
|
|
return (
|
|
<SidePanel
|
|
size="large"
|
|
loading={isLoading || isUpdating}
|
|
disabled={isFreePlan || isLoading || !hasChanges || isUpdating || !canUpdateSpendCap}
|
|
visible={visible}
|
|
onCancel={onClose}
|
|
onConfirm={onConfirm}
|
|
header={
|
|
<div className="flex items-center justify-between w-full">
|
|
<h4>Spend cap</h4>
|
|
<Button asChild variant="default" icon={<ExternalLink strokeWidth={1.5} />}>
|
|
<Link
|
|
href={`${DOCS_URL}/guides/platform/cost-control#spend-cap`}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
About spend cap
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
}
|
|
tooltip={!canUpdateSpendCap ? 'You do not have permission to update spend cap' : undefined}
|
|
>
|
|
<SidePanel.Content>
|
|
<div className="py-6 space-y-4">
|
|
<p className="text-sm">
|
|
Use the spend cap to manage project usage and costs, and control whether the project can
|
|
exceed the included quota allowance of any billed line item in a billing cycle
|
|
</p>
|
|
|
|
<Collapsible open={showUsageCosts} onOpenChange={setShowUsageCosts}>
|
|
<CollapsibleTrigger asChild>
|
|
<div className="flex items-center space-x-2 cursor-pointer">
|
|
<ChevronRight
|
|
strokeWidth={1.5}
|
|
size={16}
|
|
className={showUsageCosts ? 'rotate-90' : ''}
|
|
/>
|
|
<p className="text-sm text-foreground-light">
|
|
How are each resource charged after exceeding the included quota?
|
|
</p>
|
|
</div>
|
|
</CollapsibleTrigger>
|
|
<CollapsibleContent asChild>
|
|
<Table
|
|
className="mt-4"
|
|
head={
|
|
<>
|
|
<Table.th>
|
|
<p className="text-xs">Item</p>
|
|
</Table.th>
|
|
<Table.th>
|
|
<p className="text-xs">Rate</p>
|
|
</Table.th>
|
|
</>
|
|
}
|
|
body={billingMetricCategories.map((categoryId) => {
|
|
const category = pricing[categoryId]
|
|
const usageItems = category.features.filter((it: any) => it.usage_based)
|
|
|
|
return (
|
|
<>
|
|
<Table.tr key={categoryId}>
|
|
<Table.td>
|
|
<p className="text-xs text-foreground">{category.title}</p>
|
|
</Table.td>
|
|
<Table.td>{null}</Table.td>
|
|
</Table.tr>
|
|
{usageItems.map((item: any) => {
|
|
return (
|
|
<Table.tr key={item.title}>
|
|
<Table.td>
|
|
<p className="text-xs pl-4">{item.title}</p>
|
|
</Table.td>
|
|
<Table.td>
|
|
<p className="text-xs pl-4">
|
|
{Array.isArray(item.plans['pro'])
|
|
? item.plans['pro']?.join(', ')
|
|
: item.plans['pro']}
|
|
</p>
|
|
</Table.td>
|
|
</Table.tr>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
})}
|
|
/>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
|
|
{isFreePlan && (
|
|
<Admonition
|
|
type="note"
|
|
layout="horizontal"
|
|
title="Toggling of the spend cap is only available on the Pro Plan"
|
|
description="Upgrade your plan to disable the spend cap"
|
|
actions={
|
|
<Button variant="default" onClick={() => snap.setPanelKey('subscriptionPlan')}>
|
|
View available plans
|
|
</Button>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
<div className="mt-8! pb-4">
|
|
<div className="flex gap-3">
|
|
{SPEND_CAP_OPTIONS.map((option) => {
|
|
const isSelected = selectedOption === option.value
|
|
|
|
return (
|
|
<div
|
|
key={option.value}
|
|
className={cn('col-span-4 group space-y-1', isFreePlan && 'opacity-75')}
|
|
onClick={() => !isFreePlan && setSelectedOption(option.value)}
|
|
>
|
|
<Image
|
|
alt="Spend Cap"
|
|
className={cn(
|
|
'relative rounded-xl transition border bg-no-repeat bg-center bg-cover w-[160px] h-[96px]',
|
|
isSelected
|
|
? 'border-foreground'
|
|
: 'border-foreground-muted opacity-50 group-hover:border-foreground-lighter group-hover:opacity-100',
|
|
!isFreePlan && 'cursor-pointer',
|
|
!isFreePlan && !isSelected && 'group-hover:border-foreground-light'
|
|
)}
|
|
width={160}
|
|
height={96}
|
|
src={resolvedTheme?.includes('dark') ? option.imageUrl : option.imageUrlLight}
|
|
/>
|
|
|
|
<p
|
|
className={cn(
|
|
'text-sm transition',
|
|
!isFreePlan && 'group-hover:text-foreground',
|
|
isSelected ? 'text-foreground' : 'text-foreground-light'
|
|
)}
|
|
>
|
|
{option.name}
|
|
</p>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{selectedOption === 'on' ? (
|
|
<Admonition
|
|
type="warning"
|
|
title="Your projects could become unresponsive or enter read only mode"
|
|
description="Exceeding the included quota allowance with spend cap enabled can cause your projects
|
|
to become unresponsive or enter read only mode."
|
|
/>
|
|
) : (
|
|
<Admonition
|
|
type="note"
|
|
title="Charges apply for usage beyond included quota allowance"
|
|
description="Your projects will always remain responsive and active, and charges only apply when
|
|
exceeding the included quota limit."
|
|
/>
|
|
)}
|
|
|
|
{hasChanges && (
|
|
<>
|
|
<p className="text-sm">
|
|
{selectedOption === 'on'
|
|
? 'Upon clicking confirm, spend cap will be enabled for your organization and you will no longer be charged any extra for usage.'
|
|
: 'Upon clicking confirm, spend cap will be disabled for your organization and you will be charged for any usage beyond the included quota.'}
|
|
</p>
|
|
<p className="text-sm">
|
|
Toggling spend cap triggers an invoice and there might be prorated charges for any
|
|
usage beyond the Pro Plans quota during this billing cycle.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</SidePanel.Content>
|
|
</SidePanel>
|
|
)
|
|
}
|
|
|
|
export default SpendCapSidePanel
|