mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(studio): clean up free tier upgrade box (#49851)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Small update of the Projects overview UI to resolve the competing CTA's as well as alignment. Open to tweak a little more here. | Before | After | |--------|--------| | <img width="1287" height="346" alt="Screenshot 2026-09-01 at 15 08 33" src="https://github.com/user-attachments/assets/a0911d8c-7be9-4a70-ab37-8842093423c9" /> | <img width="1252" height="430" alt="Screenshot 2026-09-01 at 15 05 40" src="https://github.com/user-attachments/assets/cd75a8bd-9378-483b-96b8-2272a9990b4c" /> | <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Refined the plan usage card layout with larger metric values and cleaner labels. * Added dividers between usage metrics for improved readability. * Simplified card and loading-state styling by removing unnecessary borders, backgrounds, and padding. * Updated the upgrade button’s visual treatment. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
d088ec6259
commit
cecb328120
1 file changed
+20
-48
@@ -14,7 +14,6 @@ type MetricConfig = {
|
||||
key: PricingMetric
|
||||
label: string
|
||||
unit: MetricUnit
|
||||
/** Anchor id of the matching section on the org usage page. */
|
||||
anchor: string
|
||||
}
|
||||
|
||||
@@ -40,26 +39,15 @@ const METRICS: MetricConfig[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const formatGigabytes = (value: number) => {
|
||||
if (value === 0) return '0 GB'
|
||||
if (value < 1) return `${(value * 1000).toFixed(0)} MB`
|
||||
return `${value.toFixed(value < 10 ? 2 : 1)} GB`
|
||||
}
|
||||
|
||||
const formatGigabyteLimit = (limit: number) => {
|
||||
if (limit < 1) return `${(limit * 1000).toFixed(0)} MB`
|
||||
return `${limit} GB`
|
||||
}
|
||||
|
||||
// Show counts in full with thousands separators (e.g. `50,000`) rather than abbreviated
|
||||
// (`50k`), to match the pricing page and avoid ambiguity around plan limits.
|
||||
const formatCount = (value: number) => value.toLocaleString()
|
||||
|
||||
const formatValue = (value: number, unit: MetricUnit) =>
|
||||
unit === 'gigabytes' ? formatGigabytes(value) : formatCount(value)
|
||||
|
||||
const formatLimit = (limit: number, unit: MetricUnit) =>
|
||||
unit === 'gigabytes' ? formatGigabyteLimit(limit) : formatCount(limit)
|
||||
const formatUsagePair = (value: number, limit: number, unit: MetricUnit) => {
|
||||
if (unit === 'count') return { value: formatCount(value), limit: formatCount(limit) }
|
||||
if (limit < 1) {
|
||||
return { value: (value * 1000).toFixed(0), limit: `${(limit * 1000).toFixed(0)} MB` }
|
||||
}
|
||||
return { value: value === 0 ? '0' : value.toFixed(value < 10 ? 2 : 1), limit: `${limit} GB` }
|
||||
}
|
||||
|
||||
const RING_RADIUS = 7
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS
|
||||
@@ -113,8 +101,6 @@ const ProgressRing = ({
|
||||
)
|
||||
}
|
||||
|
||||
// The upgrade CTA surface this card represents. Used as the telemetry `source` +
|
||||
// `placement` value. Kept as a constant so the tracking stays explicit.
|
||||
const PLACEMENT = 'org_projects_list'
|
||||
|
||||
const CompactMetricRow = ({
|
||||
@@ -131,26 +117,25 @@ const CompactMetricRow = ({
|
||||
const ratio = limit > 0 ? current / limit : 0
|
||||
const isOver = limit > 0 && current >= limit
|
||||
const isApproaching = limit > 0 && ratio >= 0.8 && !isOver
|
||||
const formatted = formatUsagePair(current, limit, config.unit)
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/org/${orgSlug}/usage#${config.anchor}`}
|
||||
className="group/row block px-4 hover:bg-surface-200 transition-colors"
|
||||
className="group/row block hover:bg-surface-200 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 py-2 border-t border-dashed">
|
||||
<div className="flex items-center justify-between gap-2 py-2 border-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<ProgressRing ratio={ratio} isOver={isOver} isApproaching={isApproaching} />
|
||||
<span className="text-[11px] text-foreground-light uppercase font-mono truncate">
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="text-xs text-foreground-light truncate">{config.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center shrink-0">
|
||||
<span className="text-[11px] font-mono tabular-nums whitespace-nowrap">
|
||||
<span className="text-xs whitespace-nowrap">
|
||||
<span className={cn(isOver ? 'text-warning' : 'text-foreground')}>
|
||||
{formatValue(current, config.unit)}
|
||||
{formatted.value}
|
||||
</span>
|
||||
<span className="text-muted"> / </span>
|
||||
<span className="text-foreground-lighter"> {formatLimit(limit, config.unit)}</span>
|
||||
<span className="text-foreground-lighter">{formatted.limit}</span>
|
||||
</span>
|
||||
<ChevronRight
|
||||
size={12}
|
||||
@@ -164,8 +149,8 @@ const CompactMetricRow = ({
|
||||
}
|
||||
|
||||
const SkeletonMetricRow = ({ label }: { label: string }) => (
|
||||
<div className="px-4">
|
||||
<div className="flex items-center justify-between gap-2 py-2 border-t border-dashed">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2 py-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full border-2 border-foreground-muted/40 shrink-0"
|
||||
@@ -178,12 +163,6 @@ const SkeletonMetricRow = ({ label }: { label: string }) => (
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* Renders the upgrade CTA's plan-usage card in the org project list (the
|
||||
* `org_projects_list` CTA placement). The parent is responsible for gating on free plan —
|
||||
* this component only renders the visual sections once usage data is available. Shaped
|
||||
* like a `ProjectCard` so it reads as another tile.
|
||||
*/
|
||||
export const PlanUsageCard = () => {
|
||||
const track = useTrack()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
@@ -199,21 +178,13 @@ export const PlanUsageCard = () => {
|
||||
}).filter((row): row is { config: MetricConfig; usageItem: OrgMetricsUsage } => row !== null)
|
||||
: []
|
||||
|
||||
// Hide entirely on hard error or when the org has zero applicable metrics — both
|
||||
// are extreme edge cases. Otherwise always render the card shell so the layout is
|
||||
// reserved from first paint and the usage rows fade in once the query resolves.
|
||||
if (isError) return null
|
||||
if (isSuccess && visibleRows.length === 0) return null
|
||||
|
||||
return (
|
||||
<li className="list-none h-min">
|
||||
<div
|
||||
className={cn(
|
||||
'group relative bg-surface-100 border border-surface rounded-md',
|
||||
'flex flex-col gap-y-2'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4 p-4 pb-2">
|
||||
<div className="group relative flex flex-col gap-y-2">
|
||||
<div className="flex items-start justify-between gap-4 pb-2">
|
||||
<div className="flex flex-col min-w-0">
|
||||
<h5 className="text-sm text-foreground truncate">Free plan usage</h5>
|
||||
<p className="text-xs text-foreground-lighter truncate">Current billing cycle</p>
|
||||
@@ -222,11 +193,12 @@ export const PlanUsageCard = () => {
|
||||
<UpgradePlanButton
|
||||
source={PLACEMENT}
|
||||
plan="Pro"
|
||||
variant="default"
|
||||
onClick={() => track('upgrade_cta_clicked', { placement: PLACEMENT })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-end pb-2 [&>:first-child>*]:border-t-0">
|
||||
<div className="flex flex-col justify-end pb-2 [&>:first-child>*]:border-t-0 divide-y">
|
||||
{isSuccess
|
||||
? visibleRows.map(({ config, usageItem }) => (
|
||||
<CompactMetricRow
|
||||
|
||||
Reference in new issue
Block a user