Files
Danny WhiteandJoshen Lim 817a2710da feat(studio): page titles for account and org surfaces (#43536)
Stacked PR 4/5 for page title improvements. Includes account and
organization-level title updates plus organization list/selector page
titles.

_Base:
[dnywh/feat/page-titles](https://github.com/supabase/supabase/pull/43538)_

---

## What kind of change does this PR introduce?

- Resolves FE-1960
- Resolves FE-1983
- Resolves DEPR-207

## What is the current behavior?

Page titles between surfaces are inconsistent and vague. Sometimes they
say the product name:

```
My Project | My Org | Supabase
```

...even when on a specific surface like Database > Tables.

Other times they show the entity name but skip over the project or org
name :

```
Edge Functions | Supabase
```

## What is the new behavior?

Account and organization-level title updates plus organization
list/selector page titles, adopting the layout title format introduced
in https://github.com/supabase/supabase/pull/43538:

```
users | Table Editor | My Project | My Org | Supabase
hello-world | Logs | Edge Functions | My Project | My Org | Supabase
Backups | Database | My Project | My Org | Supabase
Authentication | My Project | My Org | Supabase
```

That format is:

entity, section, surface, project, org, brand

## Additional context

Related stacked PRs also based on the the original
[dnywh/feat/page-titles](https://github.com/supabase/supabase/pull/43538)
branch:

- https://github.com/supabase/supabase/pull/43534
- https://github.com/supabase/supabase/pull/43535
- https://github.com/supabase/supabase/pull/43537

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-03-13 12:18:13 +11:00

203 lines
8.4 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import {
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import PartnerIcon from 'components/ui/PartnerIcon'
import { PARTNER_TO_NAME } from 'components/ui/PartnerManagedResource'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { ExternalLink } from 'lucide-react'
import { useTheme } from 'next-themes'
import Image from 'next/image'
import Link from 'next/link'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Alert, Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
import SpendCapSidePanel from './SpendCapSidePanel'
import { InlineLink } from '@/components/ui/InlineLink'
export interface CostControlProps {}
const CostControl = ({}: CostControlProps) => {
const { slug } = useParams()
const { resolvedTheme } = useTheme()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = useAsyncCheckPermissions(
PermissionAction.BILLING_READ,
'stripe.subscriptions'
)
const snap = useOrgSettingsPageStateSnapshot()
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const {
data: subscription,
error,
isPending: isLoading,
isSuccess,
isError,
} = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canReadSubscriptions })
const currentPlan = subscription?.plan
const isUsageBillingEnabled = subscription?.usage_billing_enabled ?? false
const canChangeTier =
!projectUpdateDisabled && !['team', 'enterprise', 'platform'].includes(currentPlan?.id || '')
const costControlDisabled = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
return (
<>
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="sticky space-y-6 top-12">
<div className="space-y-2">
<p className="text-foreground text-base m-0">Cost Control</p>
<p className="text-sm text-foreground-light m-0">
Allow scaling beyond your plan's{' '}
<InlineLink href={`/org/${slug}/usage`}>included quota</InlineLink>.
</p>
</div>
<div className="space-y-2">
<p className="text-sm text-foreground-light m-0">More information</p>
<div>
<Link
href={`${DOCS_URL}/guides/platform/cost-control#spend-cap`}
target="_blank"
rel="noreferrer"
>
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm m-0">Spend cap</p>
<ExternalLink size={16} strokeWidth={1.5} />
</div>
</Link>
</div>
</div>
</div>
</ScaffoldSectionDetail>
<ScaffoldSectionContent>
{isPermissionsLoaded && !canReadSubscriptions ? (
<NoPermission resourceText="update this organization's cost control" />
) : (
<>
{isLoading && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
{isError && <AlertError subject="Failed to retrieve subscription" error={error} />}
{isSuccess && costControlDisabled && (
<Alert_Shadcn_ className="flex flex-col items-center gap-y-2 border-0 rounded-none">
<PartnerIcon
organization={{ managed_by: selectedOrganization?.managed_by }}
showTooltip={false}
size="large"
/>
<AlertTitle_Shadcn_ className="text-sm">
The Spend Cap is not available for organizations managed by{' '}
{PARTNER_TO_NAME[selectedOrganization?.managed_by]}.
</AlertTitle_Shadcn_>
</Alert_Shadcn_>
)}
{isSuccess && !costControlDisabled && (
<div className="space-y-6">
{['team', 'enterprise', 'platform'].includes(currentPlan?.id || '') ? (
<Alert
withIcon
variant="info"
title={`You will be charged for any additional usage on the ${
currentPlan?.name || ''
} plan`}
>
{currentPlan?.name || ''} plan requires you to have spend cap off at all
times. Your projects will never become unresponsive. Only when your{' '}
<Link
href={`/org/${slug}/usage`}
className="text-green-900 transition hover:text-green-1000"
>
included usage
</Link>{' '}
is exceeded will you be charged for any additional usage.
</Alert>
) : (
<p className="text-sm text-foreground-light">
If you need to go beyond the included quota, simply switch off your spend cap
to pay for additional usage.
</p>
)}
<div className="flex flex-col md:flex-row gap-6">
<div>
<div className="rounded-md bg-surface-200 w-[160px] h-[96px] shadow">
<Image
alt="Spend Cap"
width={160}
height={96}
src={
isUsageBillingEnabled
? `${BASE_PATH}/img/spend-cap-off${
resolvedTheme?.includes('dark') ? '' : '--light'
}.png`
: `${BASE_PATH}/img/spend-cap-on${
resolvedTheme?.includes('dark') ? '' : '--light'
}.png`
}
/>
</div>
</div>
<div>
<p className="mb-1">
Spend cap is {isUsageBillingEnabled ? 'disabled' : 'enabled'}
</p>
<p className="text-sm text-foreground-light">
{isUsageBillingEnabled ? (
<span>You will be charged for usage beyond the included quota.</span>
) : (
<span>
You won't be charged any extra for usage. However, your projects could
become unresponsive or enter read only mode if you exceed the included
quota.
</span>
)}
</p>
<ProjectUpdateDisabledTooltip projectUpdateDisabled={projectUpdateDisabled}>
<Button
type="default"
className="mt-4 pointer-events-auto"
disabled={!canChangeTier}
onClick={() => snap.setPanelKey('costControl')}
>
Change spend cap
</Button>
</ProjectUpdateDisabledTooltip>
</div>
</div>
</div>
)}
</>
)}
</ScaffoldSectionContent>
</ScaffoldSection>
<SpendCapSidePanel />
</>
)
}
export default CostControl