Files
Danny White 805aee289e fix(studio): color regressions after theme update (#47794)
## What kind of change does this PR introduce?

Bug fix + small shared component

## What is the current behavior?

After the recent colour system changes, several UI elements lost
contrast in light mode:

- `InfoIcon` with a background appeared as a flat grey circle (glyph
fill matched the background)
- Several buttons forced `text-white`, which no longer contrasts
correctly against the updated brand fills
- Selected / completed check badges were inconsistent between call sites

## What is the new behavior?

- `InfoIcon` uses `text-background` for the glyph fill so the "i" is
visible against the grey circle
- www primary buttons drop hardcoded `text-white` and use standard
`Button` colours
- New shared `SuccessCheck` in `ui` for selected state and completion
progress (green circle; white check in light mode, black check in dark
mode)
- Documented in the design system with selected + progress examples

Note on `InfoIcon`:
[#47933](https://github.com/supabase/supabase/pull/47933) landed a
related fix using `text-background-200` (`--card`). This PR keeps
`text-background` instead, to match `CheckIcon` / `EyeOffIcon` in the
same file and avoid the legacy alias.

| Before | After |
| --- | --- |
| <img width="688" height="268" alt="CleanShot 2026-07-10 at 11 30
25@2x"
src="https://github.com/user-attachments/assets/c0276b0c-1023-46c8-805c-35a22def1353"
/> | <img width="664" height="278" alt="CleanShot 2026-07-10 at 11 29
43@2x"
src="https://github.com/user-attachments/assets/3508cf37-1b48-4fb7-a939-83522feb44f1"
/> |
| <img width="468" height="550" alt="CleanShot 2026-07-10 at 11 31
04@2x"
src="https://github.com/user-attachments/assets/acd42273-15ce-4fb2-9d0c-5a43ac23073c"
/> | <img width="460" height="540" alt="CleanShot 2026-07-10 at 11 32
50@2x"
src="https://github.com/user-attachments/assets/f66fb4bc-81f7-4df1-95f5-63980c4e8537"
/> |

## To test

Use the staging preview link from this PR and check the following in
**light mode** (and spot-check dark mode):

**www**
- Visit `/404` — "Head back" button should have readable text (not
white-on-green)
- Visit `/company` — "Join the team" button in the Team section
- Visit a product page with a hero CTA (e.g. `/database`, `/realtime`) —
primary "Start for free" button

**studio**
- Database → Replication → deploy a read replica — footer `InfoIcon`
next to the pricing line should show a visible "i" inside the grey
circle
- `/redeem` — select an org; green `SuccessCheck` should match
light/dark contrast (white check / black check)
- Settings → API → service role key row — red "secret" tag text should
be readable

**design system**
- `/docs/components/success-check` — demo, selected-state, and progress
examples
2026-07-15 14:09:08 +00:00

249 lines
9.2 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { useFlag, useParams } from 'common'
import { AlertCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useMemo, useRef } from 'react'
import { toast } from 'sonner'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
import { getLastUsedAPIKeys } from './DisplayApiSettings.utils'
import Panel from '@/components/ui/Panel'
import { useApiKeysLastUsedQuery } from '@/data/analytics/api-keys-last-used-query'
import { useJwtSecretUpdatingStatusQuery } from '@/data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
export const DisplayApiSettings = ({
showTitle = true,
showNotice = true,
showLegacyText = true,
}: {
showTitle?: boolean
showNotice?: boolean
showLegacyText?: boolean
}) => {
const { ref: projectRef } = useParams()
const {
data: settings,
isError: isProjectSettingsError,
isPending: isProjectSettingsLoading,
} = useProjectSettingsV2Query({ projectRef })
const {
data,
isError: isJwtSecretUpdateStatusError,
isPending: isJwtSecretUpdateStatusLoading,
} = useJwtSecretUpdatingStatusQuery({ projectRef })
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const isLoading = isProjectSettingsLoading || isLoadingPermissions
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
const apiKeys = useMemo(() => settings?.service_api_keys ?? [], [settings])
// api keys should not be empty. However it can be populated with a delay on project creation
const isApiKeysEmpty = apiKeys.length === 0
const now = useRef(new Date()).current
const showApiKeyLastUsed = useFlag('showApiKeysLastUsed')
const {
isLoading: isLoadingLastUsed,
isError: isLastUsedError,
data: lastUsedLogData,
} = useApiKeysLastUsedQuery(
{
projectRef,
isoTimestampStart: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
isoTimestampEnd: now.toISOString(),
},
{ enabled: showApiKeyLastUsed }
)
const lastUsedAPIKeys = useMemo(() => {
if (
apiKeys.length < 1 ||
!lastUsedLogData ||
lastUsedLogData.length < 1 ||
!showApiKeyLastUsed
) {
return {}
}
try {
return getLastUsedAPIKeys(apiKeys, lastUsedLogData)
} catch (e: any) {
toast.error('Failed to identify when the anon and service_role keys were last used')
console.error(e)
return {}
}
}, [lastUsedLogData, apiKeys, showApiKeyLastUsed])
return (
<Panel
noMargin
title={
showTitle && (
<div className="space-y-3">
<h5 className="text-base">Project API Keys</h5>
<p className="text-sm text-foreground-light">
Your API is secured behind an API gateway which requires an API Key for every request.
<br />
You can use the keys below in the Supabase client libraries.
<br />
</p>
</div>
)
}
>
{isLoading ? (
<div className="flex items-center justify-center py-8 space-x-2">
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">Retrieving API keys</p>
</div>
) : !canReadAPIKeys ? (
<div className="flex items-center py-8 px-8 space-x-2">
<AlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
You don't have permission to view API keys. These keys restricted to users with higher
access levels.
</p>
</div>
) : isProjectSettingsError || isJwtSecretUpdateStatusError ? (
<div className="flex items-center justify-center py-8 space-x-2">
<AlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
{isProjectSettingsError ? 'Failed to retrieve API keys' : 'Failed to update JWT secret'}
</p>
</div>
) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
<div className="flex items-center justify-center py-8 space-x-2">
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
{isProjectSettingsLoading || isApiKeysEmpty
? 'Retrieving API keys'
: 'JWT secret is being updated'}
</p>
</div>
) : (
apiKeys.map((x, i: number) => (
<Panel.Content
key={x.api_key}
className={
i >= 1 &&
'border-t border-panel-border-interior-light in-data-[theme*=dark]:border-panel-border-interior-dark'
}
>
<FormLayout
layout="horizontal"
label={
<div className="flex items-center space-x-1">
{x.tags?.split(',').map((x, i: number) => (
<code key={`${x}${i}`} className="text-code-inline">
{x}
</code>
))}
{x.tags === 'service_role' && (
<>
<code className="text-code-inline bg-destructive! text-destructive-foreground! border-destructive!">
secret
</code>
</>
)}
{x.tags === 'anon' && <code className="text-code-inline">public</code>}
</div>
}
description={
x.tags === 'service_role' ? (
<>
This key has the ability to bypass Row Level Security. Never share it publicly.
If leaked, generate a new JWT secret immediately.{' '}
{showLegacyText && (
<span>
Prefer using{' '}
<Link
href={`/project/${projectRef}/settings/api-keys/new`}
className="text-link underline"
>
Secret API keys
</Link>{' '}
instead.
</span>
)}
</>
) : (
<>
This key is safe to use in a browser if you have enabled Row Level Security for
your tables and configured policies.{' '}
{showLegacyText && (
<span>
Prefer using{' '}
<Link
href={`/project/${projectRef}/settings/api-keys/new`}
className="text-link underline"
>
Publishable API keys
</Link>{' '}
instead.
</span>
)}
</>
)
}
>
<Input
readOnly
className="font-mono"
copy={canReadAPIKeys && isNotUpdatingJwtSecret}
reveal={x.tags !== 'anon' && canReadAPIKeys && isNotUpdatingJwtSecret}
value={
!canReadAPIKeys
? 'You need additional permissions to view API keys'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed, new API key may have issues'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: (x?.api_key ?? 'You need additional permissions to view API keys')
}
onChange={() => {}}
/>
</FormLayout>
{showApiKeyLastUsed && (
<div
className="pt-2 text-foreground-lighter w-full text-sm data-[invisible=true]:invisible"
data-invisible={isLoadingLastUsed}
>
{isLastUsedError
? 'Unable to load requests from the past 24 hours.'
: lastUsedAPIKeys[x.api_key]
? `Last request was ${lastUsedAPIKeys[x.api_key]} ago.`
: 'No requests in the past 24 hours.'}
</div>
)}
</Panel.Content>
))
)}
{showNotice ? (
<Panel.Notice
className="border-t"
title="API keys have moved"
badgeLabel="Changelog"
description={`
\`anon\` and \`service_role\` API keys can now be replaced with \`publishable\` and \`secret\` API keys.
`}
href="https://github.com/orgs/supabase/discussions/29260"
buttonText="Read the announcement"
/>
) : null}
</Panel>
)
}