CostControl.tsx208 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useFlag, useParams } from 'common' |
| 3 | import { ExternalLink } from 'lucide-react' |
| 4 | import { useTheme } from 'next-themes' |
| 5 | import Image from 'next/image' |
| 6 | import Link from 'next/link' |
| 7 | import { Alert, AlertTitle, Button } from 'ui' |
| 8 | import { Admonition } from 'ui-patterns/admonition' |
| 9 | import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' |
| 10 | |
| 11 | import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip' |
| 12 | import SpendCapSidePanel from './SpendCapSidePanel' |
| 13 | import { |
| 14 | ScaffoldSection, |
| 15 | ScaffoldSectionContent, |
| 16 | ScaffoldSectionDetail, |
| 17 | } from '@/components/layouts/Scaffold' |
| 18 | import AlertError from '@/components/ui/AlertError' |
| 19 | import { InlineLink } from '@/components/ui/InlineLink' |
| 20 | import NoPermission from '@/components/ui/NoPermission' |
| 21 | import PartnerIcon from '@/components/ui/PartnerIcon' |
| 22 | import { PARTNER_TO_NAME } from '@/components/ui/PartnerManagedResource' |
| 23 | import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' |
| 24 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 25 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 26 | import { BASE_PATH, DOCS_URL } from '@/lib/constants' |
| 27 | import { MANAGED_BY } from '@/lib/constants/infrastructure' |
| 28 | import { useOrgSettingsPageStateSnapshot } from '@/state/organization-settings' |
| 29 | |
| 30 | export interface CostControlProps {} |
| 31 | |
| 32 | const CostControl = ({}: CostControlProps) => { |
| 33 | const { slug } = useParams() |
| 34 | const { resolvedTheme } = useTheme() |
| 35 | const { data: selectedOrganization } = useSelectedOrganizationQuery() |
| 36 | |
| 37 | const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = useAsyncCheckPermissions( |
| 38 | PermissionAction.BILLING_READ, |
| 39 | 'stripe.subscriptions' |
| 40 | ) |
| 41 | |
| 42 | const snap = useOrgSettingsPageStateSnapshot() |
| 43 | const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') |
| 44 | const { |
| 45 | data: subscription, |
| 46 | error, |
| 47 | isPending: isLoading, |
| 48 | isSuccess, |
| 49 | isError, |
| 50 | } = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canReadSubscriptions }) |
| 51 | |
| 52 | const currentPlan = subscription?.plan |
| 53 | const isUsageBillingEnabled = subscription?.usage_billing_enabled ?? false |
| 54 | |
| 55 | const canChangeTier = |
| 56 | !projectUpdateDisabled && !['team', 'enterprise', 'platform'].includes(currentPlan?.id || '') |
| 57 | |
| 58 | const costControlDisabled = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE |
| 59 | |
| 60 | return ( |
| 61 | <> |
| 62 | <ScaffoldSection> |
| 63 | <ScaffoldSectionDetail> |
| 64 | <div className="sticky space-y-6 top-12"> |
| 65 | <div className="space-y-2"> |
| 66 | <p className="text-foreground text-base m-0">Cost Control</p> |
| 67 | <p className="text-sm text-foreground-light m-0"> |
| 68 | Allow scaling beyond your plan's{' '} |
| 69 | <InlineLink href={`/org/${slug}/usage`}>included quota</InlineLink>. |
| 70 | </p> |
| 71 | </div> |
| 72 | <div className="space-y-2"> |
| 73 | <p className="text-sm text-foreground-light m-0">More information</p> |
| 74 | <div> |
| 75 | <Link |
| 76 | href={`${DOCS_URL}/guides/platform/cost-control#spend-cap`} |
| 77 | target="_blank" |
| 78 | rel="noreferrer" |
| 79 | > |
| 80 | <div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition"> |
| 81 | <p className="text-sm m-0">Spend cap</p> |
| 82 | <ExternalLink size={16} strokeWidth={1.5} /> |
| 83 | </div> |
| 84 | </Link> |
| 85 | </div> |
| 86 | </div> |
| 87 | </div> |
| 88 | </ScaffoldSectionDetail> |
| 89 | <ScaffoldSectionContent> |
| 90 | {isPermissionsLoaded && !canReadSubscriptions ? ( |
| 91 | <NoPermission resourceText="update this organization's cost control" /> |
| 92 | ) : ( |
| 93 | <> |
| 94 | {isLoading && ( |
| 95 | <div className="space-y-2"> |
| 96 | <ShimmeringLoader /> |
| 97 | <ShimmeringLoader className="w-3/4" /> |
| 98 | <ShimmeringLoader className="w-1/2" /> |
| 99 | </div> |
| 100 | )} |
| 101 | |
| 102 | {isError && <AlertError subject="Failed to retrieve subscription" error={error} />} |
| 103 | |
| 104 | {isSuccess && costControlDisabled && ( |
| 105 | <Alert className="flex flex-col items-center gap-y-2 border-0 rounded-none"> |
| 106 | <PartnerIcon |
| 107 | organization={{ managed_by: selectedOrganization?.managed_by }} |
| 108 | showTooltip={false} |
| 109 | size="large" |
| 110 | /> |
| 111 | |
| 112 | <AlertTitle className="text-sm"> |
| 113 | The Spend Cap is not available for organizations managed by{' '} |
| 114 | {PARTNER_TO_NAME[selectedOrganization?.managed_by]}. |
| 115 | </AlertTitle> |
| 116 | </Alert> |
| 117 | )} |
| 118 | |
| 119 | {isSuccess && !costControlDisabled && ( |
| 120 | <div className="space-y-6"> |
| 121 | {['team', 'enterprise', 'platform'].includes(currentPlan?.id || '') ? ( |
| 122 | <> |
| 123 | <Admonition |
| 124 | type="default" |
| 125 | layout="horizontal" |
| 126 | title={`You will be charged for any additional usage on the ${ |
| 127 | currentPlan?.name || '' |
| 128 | } plan`} |
| 129 | description={ |
| 130 | <> |
| 131 | {currentPlan?.name || ''} plan requires you to have spend cap off at all |
| 132 | times. Your projects will never become unresponsive. Only when your{' '} |
| 133 | <Link |
| 134 | href={`/org/${slug}/usage`} |
| 135 | className="text-green-900 transition hover:text-green-1000" |
| 136 | > |
| 137 | included usage |
| 138 | </Link>{' '} |
| 139 | is exceeded will you be charged for any additional usage. |
| 140 | </> |
| 141 | } |
| 142 | /> |
| 143 | </> |
| 144 | ) : ( |
| 145 | <p className="text-sm text-foreground-light"> |
| 146 | If you need to go beyond the included quota, simply switch off your spend cap |
| 147 | to pay for additional usage. |
| 148 | </p> |
| 149 | )} |
| 150 | |
| 151 | <div className="flex flex-col md:flex-row gap-6"> |
| 152 | <div> |
| 153 | <div className="rounded-md bg-surface-200 w-[160px] h-[96px] shadow-sm"> |
| 154 | <Image |
| 155 | alt="Spend Cap" |
| 156 | width={160} |
| 157 | height={96} |
| 158 | src={ |
| 159 | isUsageBillingEnabled |
| 160 | ? `${BASE_PATH}/img/spend-cap-off${ |
| 161 | resolvedTheme?.includes('dark') ? '' : '--light' |
| 162 | }.png` |
| 163 | : `${BASE_PATH}/img/spend-cap-on${ |
| 164 | resolvedTheme?.includes('dark') ? '' : '--light' |
| 165 | }.png` |
| 166 | } |
| 167 | /> |
| 168 | </div> |
| 169 | </div> |
| 170 | <div> |
| 171 | <p className="mb-1"> |
| 172 | Spend cap is {isUsageBillingEnabled ? 'disabled' : 'enabled'} |
| 173 | </p> |
| 174 | <p className="text-sm text-foreground-light"> |
| 175 | {isUsageBillingEnabled ? ( |
| 176 | <span>You will be charged for usage beyond the included quota.</span> |
| 177 | ) : ( |
| 178 | <span> |
| 179 | You won't be charged any extra for usage. However, your projects could |
| 180 | become unresponsive or enter read only mode if you exceed the included |
| 181 | quota. |
| 182 | </span> |
| 183 | )} |
| 184 | </p> |
| 185 | <ProjectUpdateDisabledTooltip projectUpdateDisabled={projectUpdateDisabled}> |
| 186 | <Button |
| 187 | type="default" |
| 188 | className="mt-4 pointer-events-auto" |
| 189 | disabled={!canChangeTier} |
| 190 | onClick={() => snap.setPanelKey('costControl')} |
| 191 | > |
| 192 | Change spend cap |
| 193 | </Button> |
| 194 | </ProjectUpdateDisabledTooltip> |
| 195 | </div> |
| 196 | </div> |
| 197 | </div> |
| 198 | )} |
| 199 | </> |
| 200 | )} |
| 201 | </ScaffoldSectionContent> |
| 202 | </ScaffoldSection> |
| 203 | <SpendCapSidePanel /> |
| 204 | </> |
| 205 | ) |
| 206 | } |
| 207 | |
| 208 | export default CostControl |