Files
supabase/apps/studio/components/interfaces/Settings/Logs/LogsFormatters.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

254 lines
6.5 KiB
TypeScript

/*
* Response Code
*
* for http response codes
*/
import dayjs from 'dayjs'
import { AlertCircle, Info } from 'lucide-react'
import React from 'react'
import { cn } from 'ui'
import { isUnixMicro, unixMicroToIsoTimestamp } from './Logs.utils'
import CopyButton from '@/components/ui/CopyButton'
export const RowLayout: React.FC<React.PropsWithChildren> = ({ children }) => (
<div className="flex h-full w-full items-center gap-4">{children}</div>
)
// renders a timestamp (either unix microsecond or iso timestamp)
export const SelectionDetailedTimestampRow = ({ value }: { value: string | number }) => (
<SelectionDetailedRow
label="Timestamp"
value={isUnixMicro(value) ? unixMicroToIsoTimestamp(value) : String(value)}
/>
)
export const SelectionDetailedRow = ({
label,
value,
valueRender,
}: {
label: string
value: string
valueRender?: React.ReactNode
}) => {
return (
<div className="group flex items-center gap-2 flex-wrap">
<span className="text-foreground-lighter text-sm col-span-3 whitespace-pre-wrap">
{label}
</span>
<span
title={value}
className="truncate font-mono text-foreground text-sm whitespace-pre-wrap break-all"
>
{valueRender ?? value}
</span>
<CopyButton
iconOnly
text={value}
className="group-hover:opacity-100 opacity-0 p-0 h-6 w-6"
variant="text"
title="Copy to clipboard"
/>
</div>
)
}
// used for column renderers
export const TextFormatter: React.FC<{ className?: string; value: string }> = ({
value,
className,
}) => (
<span className={cn('font-mono text-xs truncate select-text cursor-text', className)}>
{value}
</span>
)
export const ResponseCodeFormatter = ({ value }: { value: string }) => {
if (!value) {
return (
<div>
<label className="text-xs text-border-stronger">No data</label>
</div>
)
}
const ResponseCodeItem = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => (
<div className="flex h-full items-center">
<div
className={cn(
`relative flex h-6 items-center rounded-md justify-center px-2 py-1 text-center`,
className
)}
>
<label className="block font-mono text-sm">{children}</label>
</div>
</div>
)
const split = value.toString().split('')[0]
switch (split) {
// 2XX || 1XX responses
case '1':
return <ResponseCodeItem>{value}</ResponseCodeItem>
case '2':
return <ResponseCodeItem className="bg-surface-100 text-primary">{value}</ResponseCodeItem>
// 5XX responses
case '5':
return <ResponseCodeItem className="bg-red-300 text-red-1100">{value}</ResponseCodeItem>
// 4XX || 3XX responses
case '4':
case '3':
return <ResponseCodeItem className="bg-warning/10 text-warning">{value}</ResponseCodeItem>
// All other responses
default:
return (
<ResponseCodeItem className="bg-surface-100 text-foreground-lighter">
{value}
</ResponseCodeItem>
)
}
}
/*
* Response Code
*
* for http response codes
*/
export const SeverityFormatter = ({
value,
uppercase = true,
}: {
value: string
uppercase?: boolean
}) => {
if (!value) {
return (
<div>
<label className="text-xs text-border-stronger">No data</label>
</div>
)
}
const uppercasedValue = value.toUpperCase()
const text = uppercase ? uppercasedValue : value
const Layout: React.FC<React.PropsWithChildren<{ className?: string }>> = ({
className,
children,
}) => <div className={`w-24 flex items-center h-full ${className}`}>{children}</div>
switch (uppercasedValue) {
case 'UNCAUGHTEXCEPTION':
case 'PANIC':
case 'FATAL':
case 'ERROR':
return (
<Layout className="gap-1">
<div className=" p-0.5 rounded-sm text-red-900!">
<AlertCircle size={14} strokeWidth={2} />
</div>
<span className="text-red-900! block! titlecase">{text}</span>
</Layout>
)
break
case 'INFO':
case 'DEBUG':
return (
<Layout className="gap-1">
<div className=" p-0.5 rounded-sm text-blue-900!">
<AlertCircle size={14} strokeWidth={2} />
</div>
<span className="text-blue-900! block! titlecase">{text}</span>
</Layout>
)
break
case 'LOG':
return (
<Layout className="gap-1">
<div className=" p-0.5 rounded-sm text-blue-900!">
<Info size={14} strokeWidth={2} />
</div>
<span className="text-blue-900! block! titlecase">{text}</span>
</Layout>
)
break
case 'WARN':
case 'WARNING':
return (
<Layout className="gap-1">
<div className=" p-0.5 rounded-sm text-amber-900!">
<AlertCircle size={14} strokeWidth={2} />
</div>
<span className="text-amber-900! block! titlecase">{text}</span>
</Layout>
)
break
// All other responses
default:
return (
<Layout>
<div className="relative rounded-sm px-2 py-1 text-center h-6 flex justify-center items-center bg-surface-100">
<label className="block font-mono text-sm text-foreground-lighter">{text}</label>
</div>
</Layout>
)
break
}
}
export const timestampLocalFormatter = (value: string | number) => {
const timestamp = isUnixMicro(value) ? unixMicroToIsoTimestamp(value) : value
return dayjs(timestamp).format('DD MMM HH:mm:ss')
}
/*
* JSON Syntax Highlighter
*
* for http response codes
*/
export function jsonSyntaxHighlight(input: Object) {
let json: string = JSON.stringify(input, null, 2)
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const newJson = json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
function (match) {
var cls = 'number text-tomato-900'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key text-foreground'
} else {
cls = 'string text-brand-600'
}
} else if (/true|false/.test(match)) {
cls = 'boolean text-blue-900'
} else if (/null/.test(match)) {
cls = 'null text-amber-1100'
}
return '<span class="' + cls + '">' + match + '</span>'
}
)
const jsonWithLineWraps = newJson.split(`\n`).map((x) => {
return `<span class="line text-xs">${x}</span>`
})
return jsonWithLineWraps.join('\n')
}