Files
supabase/apps/studio/components/interfaces/ConfigDrift/ConfigurationDriftPage.tsx
Danny White ec53175b8a refactor(ui): rename text-brand to text-primary (#50564)
## What kind of change does this PR introduce?

Refactor. Follow-up to #49871.

## What is the current behavior?

Branded (green) text still uses the `text-brand` classname while the
colour comes from `--primary`.

## What is the new behavior?

**Rename-only:** `text-brand` → `text-primary` across callsites and
docs. Leftover `bg-brand` / `border-brand` alias to `brand-default`. No
intentional colour changes in this PR.

This better matches how we treat our green in other components and
props, like `variant="primary"` for green buttons.

## To test

On light mode: smoke-test that branded text still looks like #49871
(readable green, not the bright fill).

-
[Homepage](https://zone-www-dot-com-git-dnywh-depr-316-text-brand-de2380-supabase.vercel.app/):
“Scale to millions” uses `text-primary`
- [Docs
homepage](https://docs-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/docs):
branded links like “More on self-hosting” are still readable
- [Typography
docs](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/typography):
documents `text-primary`
- [Colour
usage](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/color-usage):
`text-primary` is visibly darker than `bg-brand-default` _on light mode_
- [Studio auth
providers](https://studio-staging-git-dnywh-depr-316-text-brand-to-60fa6c-supabase.vercel.app/dashboard/project/_/auth/providers):
enabled provider badge text readable; status dot stays bright green

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Updated branded text, link hover states, icons, badges, indicators,
and highlighted content across the Design System, Docs, Studio, Learn,
UI Library, and marketing experiences to use the primary theme color.
* Updated syntax highlighting and table-of-contents styling for
consistent primary-color presentation.
  * Refined brand color fallback behavior for bright fills and borders.

* **Documentation**
* Updated color-usage and typography guidance to recommend the primary
text utility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 16:19:03 +10:00

331 lines
12 KiB
TypeScript

import { useParams } from 'common'
import { ArrowRight, CheckCircle2, FileWarning, Github, Plus } from 'lucide-react'
import Link from 'next/link'
import { Button, Card, Skeleton } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { CollapsibleCardSection } from 'ui-patterns/CollapsibleCardSection'
import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational'
import {
createConfigurationDriftRows,
groupMatchedConfigFields,
groupUnmanagedConfigFields,
type ConfigSectionGroup,
type ConfigurationDriftRow,
} from './ConfigurationDriftPage.utils'
import { AlertError } from '@/components/ui/AlertError'
import { useSelectedGitHubConfigDrift } from '@/hooks/misc/useGitHubConfigDrift'
export function ConfigurationDriftPageSkeleton() {
return (
<div className="space-y-6" aria-label="Loading configuration drift" aria-busy="true">
<div className="space-y-3 rounded-lg border border-border bg-surface-100 px-4 py-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-center gap-3">
<Skeleton className="h-8 w-8 shrink-0 rounded-md" />
<Skeleton className="h-4 w-44 max-w-[60vw]" />
</div>
<div className="flex w-full items-center gap-2 sm:w-auto">
<Skeleton className="h-7 flex-1 rounded-md sm:w-28 sm:flex-none" />
<Skeleton className="h-7 flex-1 rounded-md sm:w-20 sm:flex-none" />
</div>
</div>
<div className="grid gap-3 border-t border-border pt-3 sm:grid-cols-2 lg:grid-cols-3">
{['w-28', 'w-44', 'w-52'].map((width) => (
<div key={width} className="space-y-2">
<Skeleton className="h-3 w-20" />
<Skeleton className={`h-3 ${width}`} />
</div>
))}
</div>
<div className="border-t border-border pt-3">
<Skeleton className="h-3 w-40" />
<Skeleton className="mt-2 h-3 w-72 max-w-full" />
<div className="mt-3 overflow-hidden rounded-md border border-border">
<div className="flex items-center gap-2 bg-surface-100 px-3 py-2.5">
<Skeleton className="h-5 w-5 rounded-full" />
<Skeleton className="h-3 w-32" />
</div>
<div className="space-y-2 border-t border-border bg-[#0d0d0d] px-4 py-4">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-3 w-52 max-w-full" />
<Skeleton className="h-3 w-40 max-w-full" />
</div>
</div>
</div>
</div>
<Card className="overflow-hidden">
<div className="flex min-h-[77px] items-start gap-3 border-b border-border px-4 py-4 sm:px-5">
<Skeleton className="mt-0.5 h-5 w-5 shrink-0 rounded" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-48 max-w-full" />
<Skeleton className="h-3 w-[460px] max-w-full" />
</div>
</div>
<div className="border-b border-border bg-surface-100 px-4 py-3 sm:px-5">
<Skeleton className="h-3 w-32" />
</div>
<div className="px-4 py-4 sm:px-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-2">
<Skeleton className="h-4 w-36" />
<Skeleton className="h-3 w-52 max-w-[55vw]" />
</div>
<Skeleton className="h-7 w-24 shrink-0 rounded-md" />
</div>
<div className="mt-4 grid gap-3 md:grid-cols-2">
{[0, 1].map((panel) => (
<div
key={panel}
className="min-h-[84px] space-y-3 rounded-md border border-border p-3"
>
<Skeleton className="h-3 w-28" />
<Skeleton className="h-3 w-4/5" />
</div>
))}
</div>
</div>
<div className="flex flex-col gap-2 border-t border-border bg-surface-100 px-4 py-4 sm:flex-row sm:px-5">
<Skeleton className="h-[58px] flex-1 rounded-md" />
<Skeleton className="h-[58px] flex-1 rounded-md" />
</div>
</Card>
</div>
)
}
function ConfigurationDriftItem({ row }: { row: ConfigurationDriftRow }) {
const valueDiff = row.valueDiff
return (
<article className="border-t border-border px-4 py-4 first:border-t-0 sm:px-5">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1">
<h3 className="text-sm font-medium text-foreground">{row.settingLabel}</h3>
<code className="text-code-inline max-w-full truncate text-xs" title={row.configPath}>
{row.configPath}
</code>
<span className="text-xs text-warning">Drift</span>
</div>
<Button
asChild
type="button"
size="tiny"
className="w-full shrink-0 sm:w-auto"
icon={<ArrowRight className="h-3.5 w-3.5" />}
>
<Link href={row.settingHref}>Open setting</Link>
</Button>
</div>
<div className="mt-4 grid gap-3 md:grid-cols-2">
<div className="min-w-0 rounded-md border border-border bg-surface-100 p-3">
<p className="mb-2 text-xs font-medium text-foreground-light">
Current environment · active
</p>
<pre className="whitespace-pre-wrap break-all font-mono text-xs leading-5 text-foreground">
{valueDiff.dashboardValue}
</pre>
</div>
<div className="min-w-0 rounded-md border border-border bg-surface-100 p-3">
<p className="mb-2 text-xs font-medium text-foreground-light">config.toml · intended</p>
<pre className="whitespace-pre-wrap break-all font-mono text-xs leading-5 text-foreground">
{valueDiff.configValue}
</pre>
</div>
</div>
</article>
)
}
export function ConfigurationDriftResults({ rows }: { rows: ConfigurationDriftRow[] }) {
const driftCount = rows.length
const issueSummary = `${driftCount} ${driftCount === 1 ? 'setting differs' : 'settings differ'}`
return (
<Card className="overflow-hidden">
<div className="flex items-start gap-3 border-b border-border px-4 py-4 sm:px-5">
<div className="flex items-start gap-3">
<FileWarning className="mt-0.5 h-5 w-5 shrink-0 -translate-x-0.5 text-warning" />
<div className="min-w-0">
<h2 className="text-sm font-medium">{issueSummary}</h2>
<p className="mt-1 text-sm text-foreground-light">
Current environment values differ from config.toml. Update the setting to resolve.
</p>
</div>
</div>
</div>
<div>
{rows.map((row) => (
<ConfigurationDriftItem key={row.configPath} row={row} />
))}
</div>
</Card>
)
}
function ConfigFieldSection({
title,
description,
groups,
}: {
title: string
description: string
groups: ConfigSectionGroup[]
}) {
const fieldCount = groups.reduce((count, group) => count + group.rows.length, 0)
if (fieldCount === 0) return null
return (
<Card className="px-4 py-4 sm:px-5">
<CollapsibleCardSection title={`${title} (${fieldCount})`} description={description}>
<div className="space-y-4">
{groups.map((group) => (
<div key={group.section}>
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-foreground-muted">
{group.sectionLabel}
</h3>
<ul className="divide-y divide-border rounded-md border border-border">
{group.rows.map((row) => (
<li
key={row.configPath}
className="flex items-center justify-between gap-4 px-3 py-2 text-sm"
>
<span className="text-foreground-light">{row.label}</span>
<code
className="max-w-[60%] truncate text-xs text-foreground-muted"
title={row.value}
>
{row.value}
</code>
</li>
))}
</ul>
</div>
))}
</div>
</CollapsibleCardSection>
</Card>
)
}
export function ConfigurationDriftPage() {
const { ref: projectRef = '' } = useParams()
const { summary, isReady, isPending, isFetching, isError, error, errorSource, refetch } =
useSelectedGitHubConfigDrift()
const driftRows = createConfigurationDriftRows(summary.driftedFields, projectRef)
const comparableSettingCount = summary.matchedFields.length + driftRows.length
const matchedGroups = groupMatchedConfigFields(summary.matchedFields)
const unmanagedGroups = groupUnmanagedConfigFields(summary.unmanagedFields)
if (isPending) {
return <ConfigurationDriftPageSkeleton />
}
if (isError) {
const isConfigTomlError = errorSource === 'config-toml'
const subject = isConfigTomlError
? 'Could not read config.toml'
: 'Could not check configuration drift'
const description = isConfigTomlError
? 'Fix the listed values on the selected GitHub branch, then refresh.'
: 'Refresh to compare the supported settings with the selected GitHub branch again.'
return (
<AlertError
projectRef={projectRef}
subject={subject}
description={description}
error={error}
additionalActions={
<Button size="small" loading={isFetching} onClick={() => refetch()}>
Refresh
</Button>
}
/>
)
}
if (!isReady) {
return (
<EmptyStatePresentational
icon={Github}
title="Github integration required"
description={
<p className="mt-1 text-sm text-foreground-light">
This feature requires a GitHub integration to enable the project to be managed by a
config.toml file.
</p>
}
>
<Link href={`/project/${projectRef}/settings/integrations`}>
<Button size="tiny" variant="primary" icon={<Plus size={14} />}>
Connect a GitHub repo
</Button>
</Link>
</EmptyStatePresentational>
)
}
return (
<main className="space-y-6">
<Admonition
type="warning"
title="Config.toml will overwrite these settings on next deploy"
description={
<>
This project is connected to GitHub. The next commit redeploys{' '}
<code className="text-code-inline text-xs">config.toml</code> and overwrite changes made
here. To keep these changes, run{' '}
<code className="text-code-inline text-xs">supabase config pull</code> locally.
</>
}
/>
{comparableSettingCount === 0 ? (
<Card className="flex min-h-44 items-center justify-center px-6 text-center">
<div className="max-w-lg">
<Github className="mx-auto mb-3 h-6 w-6 text-foreground-muted" />
<h2 className="text-sm font-medium">No comparable settings found</h2>
<p className="mt-1 text-sm text-foreground-light">
This branch does not define any supported, non-secret settings that can be compared
with the current environment.
</p>
</div>
</Card>
) : driftRows.length === 0 ? (
<Card className="flex min-h-44 items-center justify-center px-6 text-center">
<div className="max-w-lg">
<CheckCircle2 className="mx-auto mb-3 h-6 w-6 text-primary" />
<h2 className="text-sm font-medium">All compared settings match</h2>
<p className="mt-1 text-sm text-foreground-light">
The current environment matches all {summary.matchedFields.length} comparable settings
in this branch.
</p>
</div>
</Card>
) : (
<ConfigurationDriftResults rows={driftRows} />
)}
<ConfigFieldSection
title="Matching config.toml"
description="These settings already match between the current environment and config.toml."
groups={matchedGroups}
/>
<ConfigFieldSection
title="Not tracked in config.toml"
description="These settings aren't declared in config.toml, so they can't drift — shown here for visibility only."
groups={unmanagedGroups}
/>
</main>
)
}