mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 01:40:13 -04:00
b554f58106
## Problem We want to reduce the code we ship and maintain. ## Solution - Migrate old `Input` usage to the new Shadcn `input` This PR focuses on: - DevToolbar - design-system examples - `www` usages - `docs` usages ## Screenshots ### Docs: OAuth Apple Before: <img width="613" height="508" alt="image" src="https://github.com/user-attachments/assets/1d2d7726-cc5e-471f-a2c2-995b9d7f70ee" /> After: <img width="606" height="530" alt="image" src="https://github.com/user-attachments/assets/ca4f522f-de9c-4edf-966b-70cad5015d0c" /> NOTE: Also used the `DataInput` for the secret once the inputs are filled. ### Docs: Extensions Before: <img width="596" height="161" alt="image" src="https://github.com/user-attachments/assets/16d2f548-90dc-4987-9954-7c47ac58e76e" /> After: <img width="604" height="227" alt="image" src="https://github.com/user-attachments/assets/62c74102-98c6-47a6-b19b-cbf67dfad68f" /> ### WWW: Blog search Before: <img width="971" height="417" alt="image" src="https://github.com/user-attachments/assets/efb0307e-60b5-4d8f-9823-c8b8996cdf32" /> After: <img width="964" height="403" alt="image" src="https://github.com/user-attachments/assets/2dc0decd-b773-4bc6-9a72-c43f352f8cbf" /> ### WWW: Blog author search Before: <img width="953" height="337" alt="image" src="https://github.com/user-attachments/assets/1f629704-ab7d-4e4b-878e-1838ab16037f" /> After: <img width="1028" height="341" alt="image" src="https://github.com/user-attachments/assets/d8d54dcb-3c00-46ea-b97f-55c16cda917f" /> ### WWW: Assistant demo Before: <img width="421" height="715" alt="image" src="https://github.com/user-attachments/assets/bcc4a591-d53c-4202-acf8-2b3d6cfd52d2" /> After: <img width="435" height="731" alt="image" src="https://github.com/user-attachments/assets/8a57c5da-5c9e-474d-a89e-2835d3498aef" /> ### WWW: Integrations Before: <img width="740" height="599" alt="image" src="https://github.com/user-attachments/assets/cf3d3d8a-b247-4e20-b47d-11976ca49c57" /> After: <img width="911" height="492" alt="image" src="https://github.com/user-attachments/assets/dcb5b6e8-f4e2-4801-b390-352390a0b486" /> ### WWW: features Before: <img width="1098" height="491" alt="image" src="https://github.com/user-attachments/assets/ea3645c5-df03-4eb9-b28c-41018e01c41e" /> After: <img width="976" height="479" alt="image" src="https://github.com/user-attachments/assets/4439a38e-6342-42cd-a859-1e599a8cf0f4" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Adjusted input widths and spacing for more consistent search and form layouts. * **Refactor** * Standardized input components across apps and docs, and reimplemented search controls as composed input groups with dedicated icon/action areas. * **Chores** * Removed a deprecated legacy input variant along with its legacy styles and tests. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
580 lines
21 KiB
TypeScript
580 lines
21 KiB
TypeScript
'use client'
|
|
|
|
import { useFeatureFlags } from 'common'
|
|
import { Copy, EyeOff, Search, X } from 'lucide-react'
|
|
import Image from 'next/image'
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useState,
|
|
type ChangeEvent,
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
} from 'react'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
cn,
|
|
Input_Shadcn_ as Input,
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupInput,
|
|
Sheet,
|
|
SheetClose,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
Switch,
|
|
Tabs_Shadcn_ as Tabs,
|
|
TabsList_Shadcn_ as TabsList,
|
|
TabsTrigger_Shadcn_ as TabsTrigger,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
|
|
import { useDevToolbar } from './DevToolbarContext'
|
|
import type { DevTelemetryEvent, ExtraTab } from './types'
|
|
import {
|
|
CC_ORIGINALS_KEY,
|
|
deleteCookie,
|
|
getCookie,
|
|
parseOverrideValue,
|
|
PH_ORIGINALS_KEY,
|
|
readOriginals,
|
|
safeJsonParse,
|
|
setCookie,
|
|
valuesAreEqual,
|
|
writeOriginals,
|
|
} from './utils'
|
|
|
|
// Duplicated for tree-shaking — bundler must see literal process.env reference.
|
|
// Keep in sync: index.ts, DevToolbarContext.tsx, DevToolbarTrigger.tsx, feature-flags.tsx
|
|
const env = process.env.NEXT_PUBLIC_ENVIRONMENT
|
|
const IS_TOOLBAR_ENABLED = env === 'local' || env === 'staging'
|
|
const IS_LOCAL_DEV = env === 'local'
|
|
|
|
function EventRow({ event }: { event: DevTelemetryEvent }) {
|
|
const [isExpanded, setIsExpanded] = useState(false)
|
|
|
|
const time = new Date(event.timestamp).toLocaleTimeString('en-US', {
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
})
|
|
|
|
const handleCopy = (e: React.MouseEvent) => {
|
|
e.stopPropagation()
|
|
navigator.clipboard
|
|
.writeText(JSON.stringify({ name: event.eventName, properties: event.properties }, null, 2))
|
|
.catch((error) => console.warn('Copy failed', error))
|
|
}
|
|
|
|
return (
|
|
<div className="group last:border-b-0">
|
|
<div className="relative flex items-center h-9 hover:bg-surface-100">
|
|
<button
|
|
type="button"
|
|
aria-expanded={isExpanded}
|
|
onClick={() => setIsExpanded((prev) => !prev)}
|
|
className="flex items-center flex-1 min-w-0 h-full px-6 gap-5 cursor-pointer"
|
|
>
|
|
<span className="flex items-center gap-2 shrink-0 w-16">
|
|
<span
|
|
className={cn(
|
|
'w-1.5 h-1.5 rounded-[2px] shrink-0',
|
|
event.source === 'client' ? 'bg-brand' : 'bg-foreground-lighter'
|
|
)}
|
|
/>
|
|
<span
|
|
className={cn(
|
|
'font-mono text-xs uppercase',
|
|
event.source === 'client' ? 'text-brand' : 'text-foreground-light'
|
|
)}
|
|
>
|
|
{event.source}
|
|
</span>
|
|
</span>
|
|
|
|
<span className="flex items-center gap-2 flex-1 min-w-0 overflow-hidden">
|
|
<span className="font-mono text-xs text-foreground-lighter shrink-0 w-20">{time}</span>
|
|
<span className="font-mono text-xs text-foreground-lighter shrink-0 w-28 truncate uppercase text-left">
|
|
{event.eventType}
|
|
</span>
|
|
<span className="font-mono text-xs text-foreground truncate shrink-0">
|
|
{event.eventName}
|
|
</span>
|
|
{event.distinctId && (
|
|
<span className="font-mono text-xs text-foreground-muted truncate">
|
|
{event.distinctId}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</button>
|
|
<div className="shrink-0 pr-6">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
aria-label="Copy JSON"
|
|
className="p-1 rounded-sm hover:bg-surface-200 text-foreground-muted hover:text-foreground-light"
|
|
>
|
|
<Copy className="w-3 h-3" />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="left">Copy JSON</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
{isExpanded && (
|
|
<pre className="px-8 py-2 bg-surface-100 border-b text-xs font-mono overflow-x-auto max-h-[200px] overflow-y-auto text-foreground-light">
|
|
{JSON.stringify(event.properties, null, 2)}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function FlagRow({
|
|
flagName,
|
|
currentValue,
|
|
originalValue,
|
|
isOverridden,
|
|
onToggle,
|
|
}: {
|
|
flagName: string
|
|
currentValue: unknown
|
|
originalValue: unknown
|
|
isOverridden: boolean
|
|
onToggle: (value: unknown) => void
|
|
}) {
|
|
const valueType = typeof originalValue
|
|
const isNull = originalValue === null
|
|
const inputProps = {
|
|
size: 'tiny' as const,
|
|
value: String(currentValue),
|
|
onChange: (event: ChangeEvent<HTMLInputElement>) => onToggle(event.target.value),
|
|
className: 'w-32',
|
|
}
|
|
|
|
return (
|
|
<div className={cn('px-4 py-3 flex flex-col gap-0.5', isOverridden && 'bg-warning/5')}>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex flex-col items-start gap-1">
|
|
<div className="flex gap-2 min-w-0 h-4">
|
|
<span
|
|
className={cn(
|
|
'font-mono text-xs truncate',
|
|
isOverridden ? 'text-warning' : 'text-foreground'
|
|
)}
|
|
>
|
|
{flagName}
|
|
</span>
|
|
{isOverridden && (
|
|
<Badge variant="warning" className="shrink-0">
|
|
Overridden
|
|
</Badge>
|
|
)}
|
|
{isNull && (
|
|
<Badge variant="secondary" className="shrink-0">
|
|
null
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-foreground-muted uppercase font-mono">
|
|
Original:{' '}
|
|
<code className="text-foreground-lighter">{JSON.stringify(originalValue)}</code>
|
|
</div>
|
|
</div>
|
|
|
|
{isNull ? (
|
|
<Input size="tiny" value="null" disabled className="w-32 opacity-50" />
|
|
) : valueType === 'boolean' ? (
|
|
<Switch
|
|
checked={currentValue as boolean}
|
|
onCheckedChange={(checked) => onToggle(checked)}
|
|
/>
|
|
) : valueType === 'number' ? (
|
|
<Input type="number" {...inputProps} />
|
|
) : (
|
|
<Input {...inputProps} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function DevToolbar({ extraTabs = [] }: { extraTabs?: ExtraTab[] }) {
|
|
const { isEnabled, isOpen, setIsOpen, events, setEvents, dismissToolbar } = useDevToolbar()
|
|
const [activeTab, setActiveTab] = useState<string>('events')
|
|
const [flagsSubTab, setFlagsSubTab] = useState<'posthog' | 'configcat'>('posthog')
|
|
const [eventFilter, setEventFilter] = useState<string>('')
|
|
const { posthog: posthogFlags, configcat: configcatFlags } = useFeatureFlags()
|
|
const [phFlagOverrides, setPhFlagOverrides] = useState<Record<string, unknown>>({})
|
|
const [ccFlagOverrides, setCcFlagOverrides] = useState<Record<string, unknown>>({})
|
|
const [phFlagOriginals, setPhFlagOriginals] = useState<Record<string, unknown>>({})
|
|
const [ccFlagOriginals, setCcFlagOriginals] = useState<Record<string, unknown>>({})
|
|
|
|
const loadOverrides = useCallback(
|
|
(cookieName: string, label: string, setter: (value: Record<string, unknown>) => void) => {
|
|
const parsed = safeJsonParse<Record<string, unknown>>(getCookie(cookieName), {}, label)
|
|
if (Object.keys(parsed).length > 0) {
|
|
setter(parsed)
|
|
}
|
|
},
|
|
[]
|
|
)
|
|
|
|
const saveOverrides = useCallback(
|
|
(
|
|
cookieName: string,
|
|
overrides: Record<string, unknown>,
|
|
setter: (value: Record<string, unknown>) => void
|
|
) => {
|
|
setter(overrides)
|
|
if (Object.keys(overrides).length > 0) {
|
|
setCookie(cookieName, JSON.stringify(overrides), '/')
|
|
} else {
|
|
deleteCookie(cookieName)
|
|
}
|
|
},
|
|
[]
|
|
)
|
|
|
|
const updateOriginals = useCallback(
|
|
(
|
|
storageKey: typeof PH_ORIGINALS_KEY | typeof CC_ORIGINALS_KEY,
|
|
setter: Dispatch<SetStateAction<Record<string, unknown>>>
|
|
) =>
|
|
(updater: (prev: Record<string, unknown>) => Record<string, unknown>) => {
|
|
setter((prev) => {
|
|
const next = updater(prev)
|
|
writeOriginals(storageKey, next)
|
|
return next
|
|
})
|
|
},
|
|
[]
|
|
)
|
|
|
|
useEffect(() => {
|
|
loadOverrides('x-ph-flag-overrides', 'PostHog flag overrides', setPhFlagOverrides)
|
|
loadOverrides('x-cc-flag-overrides', 'ConfigCat flag overrides', setCcFlagOverrides)
|
|
}, [loadOverrides])
|
|
|
|
useEffect(() => {
|
|
setPhFlagOriginals(readOriginals(PH_ORIGINALS_KEY))
|
|
setCcFlagOriginals(readOriginals(CC_ORIGINALS_KEY))
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const STYLE_ID = 'dev-toolbar-hide-native-devtools'
|
|
const existing = document.getElementById(STYLE_ID)
|
|
|
|
if (isOpen) {
|
|
if (!existing) {
|
|
const style = document.createElement('style')
|
|
style.id = STYLE_ID
|
|
style.textContent = `
|
|
.tsqd-open-btn, .tsqd-open-btn-container { display: none !important; }
|
|
`
|
|
document.head.appendChild(style)
|
|
}
|
|
} else {
|
|
existing?.remove()
|
|
}
|
|
}, [isOpen])
|
|
|
|
const updatePhOriginals = updateOriginals(PH_ORIGINALS_KEY, setPhFlagOriginals)
|
|
const updateCcOriginals = updateOriginals(CC_ORIGINALS_KEY, setCcFlagOriginals)
|
|
|
|
const togglePhFlagOverride = (flagName: string, value: unknown) => {
|
|
const originalValue = phFlagOriginals[flagName] ?? posthogFlags[flagName]
|
|
const parsedValue = parseOverrideValue(value, originalValue)
|
|
if (valuesAreEqual(parsedValue, originalValue)) {
|
|
const newOverrides = { ...phFlagOverrides }
|
|
delete newOverrides[flagName]
|
|
saveOverrides('x-ph-flag-overrides', newOverrides, setPhFlagOverrides)
|
|
return
|
|
}
|
|
|
|
updatePhOriginals((prev) =>
|
|
flagName in prev ? prev : { ...prev, [flagName]: posthogFlags[flagName] }
|
|
)
|
|
const newOverrides = { ...phFlagOverrides, [flagName]: parsedValue }
|
|
saveOverrides('x-ph-flag-overrides', newOverrides, setPhFlagOverrides)
|
|
}
|
|
|
|
const toggleCcFlagOverride = (flagName: string, value: unknown) => {
|
|
const originalValue = ccFlagOriginals[flagName] ?? configcatFlags[flagName]
|
|
const parsedValue = parseOverrideValue(value, originalValue)
|
|
if (valuesAreEqual(parsedValue, originalValue)) {
|
|
const newOverrides = { ...ccFlagOverrides }
|
|
delete newOverrides[flagName]
|
|
saveOverrides('x-cc-flag-overrides', newOverrides, setCcFlagOverrides)
|
|
return
|
|
}
|
|
|
|
updateCcOriginals((prev) =>
|
|
flagName in prev ? prev : { ...prev, [flagName]: configcatFlags[flagName] }
|
|
)
|
|
const newOverrides = { ...ccFlagOverrides, [flagName]: parsedValue }
|
|
saveOverrides('x-cc-flag-overrides', newOverrides, setCcFlagOverrides)
|
|
}
|
|
|
|
const clearAllOverrides = () => {
|
|
setPhFlagOverrides({})
|
|
setCcFlagOverrides({})
|
|
setPhFlagOriginals({})
|
|
setCcFlagOriginals({})
|
|
deleteCookie('x-ph-flag-overrides')
|
|
deleteCookie('x-cc-flag-overrides')
|
|
writeOriginals(PH_ORIGINALS_KEY, {})
|
|
writeOriginals(CC_ORIGINALS_KEY, {})
|
|
window.location.reload()
|
|
}
|
|
|
|
const normalizedFilter = eventFilter.trim().toLowerCase()
|
|
const filteredEvents = (
|
|
normalizedFilter
|
|
? events.filter(
|
|
(e) =>
|
|
e.eventName.toLowerCase().includes(normalizedFilter) ||
|
|
e.eventType.toLowerCase().includes(normalizedFilter)
|
|
)
|
|
: events
|
|
)
|
|
.slice()
|
|
.sort((a, b) => b.timestamp - a.timestamp)
|
|
|
|
const phOverrideCount = Object.keys(phFlagOverrides).length
|
|
const ccOverrideCount = Object.keys(ccFlagOverrides).length
|
|
const totalOverrideCount = phOverrideCount + ccOverrideCount
|
|
|
|
if (!IS_TOOLBAR_ENABLED || !isEnabled) return null
|
|
|
|
return (
|
|
<Sheet open={isOpen} onOpenChange={setIsOpen}>
|
|
<SheetContent
|
|
side="bottom"
|
|
size="lg"
|
|
className="flex flex-col p-0 gap-0 overflow-hidden"
|
|
showClose={false}
|
|
hasOverlay={false}
|
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
|
>
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={setActiveTab}
|
|
className="flex flex-col flex-1 min-h-0 overflow-hidden"
|
|
>
|
|
<SheetHeader className="border-b shrink-0 space-y-0 p-0">
|
|
<SheetTitle className="sr-only">Dev Toolbar</SheetTitle>
|
|
<SheetDescription className="sr-only">
|
|
View telemetry events and feature flags for local development
|
|
</SheetDescription>
|
|
<div className="flex items-center px-6">
|
|
<Image
|
|
src="/img/logo-pixel-small-light.png"
|
|
alt="Dev Toolbar"
|
|
width={16}
|
|
height={16}
|
|
style={{
|
|
filter:
|
|
'brightness(0) saturate(100%) invert(72%) sepia(57%) saturate(431%) hue-rotate(108deg) brightness(95%) contrast(91%)',
|
|
}}
|
|
aria-hidden="true"
|
|
className="shrink-0 mr-4"
|
|
/>
|
|
<TabsList className="flex gap-x-4 rounded-none border-none! h-auto">
|
|
<TabsTrigger value="events" className="text-xs py-3 border-b font-mono uppercase">
|
|
Events ({filteredEvents.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger value="flags" className="text-xs py-3 border-b font-mono uppercase">
|
|
Flags {totalOverrideCount > 0 && `(${totalOverrideCount})`}
|
|
</TabsTrigger>
|
|
{extraTabs.map((tab) => (
|
|
<TabsTrigger
|
|
key={tab.id}
|
|
value={tab.id}
|
|
className="text-xs py-3 border-b font-mono uppercase"
|
|
>
|
|
{tab.label}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="text"
|
|
icon={<EyeOff className="w-4 h-4" />}
|
|
onClick={dismissToolbar}
|
|
className="text-foreground-light hover:text-foreground p-1"
|
|
/>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Hide Dev Toolbar</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<SheetClose asChild>
|
|
<Button
|
|
type="text"
|
|
icon={<X className="w-4 h-4" />}
|
|
className="text-foreground-light hover:text-foreground p-1"
|
|
/>
|
|
</SheetClose>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Close</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</SheetHeader>
|
|
|
|
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
{activeTab === 'events' && (
|
|
<div className="flex-1 overflow-hidden flex flex-col">
|
|
<div className="flex items-center justify-between border-b shrink-0 px-6 py-2 gap-2">
|
|
<InputGroup className="w-full max-w-sm">
|
|
<InputGroupInput
|
|
size="tiny"
|
|
placeholder="Filter events..."
|
|
value={eventFilter}
|
|
onChange={(e) => setEventFilter(e.target.value)}
|
|
/>
|
|
<InputGroupAddon>
|
|
<Search size={14} className="text-foreground-lighter" />
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
<Button
|
|
type="default"
|
|
onClick={() => setEvents([])}
|
|
className="text-foreground-lighter hover:text-foreground"
|
|
>
|
|
Clear all
|
|
</Button>
|
|
</div>
|
|
|
|
{!IS_LOCAL_DEV && (
|
|
<div className="px-6 py-2 text-xs text-foreground-muted border-b bg-surface-100">
|
|
Server-side events are only visible when using the toolbar in local development
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 min-h-0 overflow-y-auto pb-4">
|
|
{filteredEvents.length === 0 ? (
|
|
<div className="text-center text-foreground-lighter py-8 text-sm">
|
|
No events yet. Interact with the app to see telemetry events.
|
|
</div>
|
|
) : (
|
|
<div className="overflow-hidden">
|
|
{filteredEvents.map((event) => (
|
|
<EventRow key={`${event.source}-${event.id}`} event={event} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'flags' && (
|
|
<div className="flex-1 min-h-0 overflow-hidden flex">
|
|
{/* Sidebar */}
|
|
<div className="w-44 border-r shrink-0 flex flex-col">
|
|
<nav className="flex flex-col p-3 gap-0.5">
|
|
{(
|
|
[
|
|
{ id: 'posthog', label: 'PostHog', count: phOverrideCount },
|
|
{ id: 'configcat', label: 'ConfigCat', count: ccOverrideCount },
|
|
] as const
|
|
).map(({ id, label, count }) => (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => setFlagsSubTab(id)}
|
|
className={cn(
|
|
'flex items-center justify-between px-3 py-1.5 rounded-sm text-sm text-left uppercase font-mono tracking-wide',
|
|
flagsSubTab === id
|
|
? 'bg-surface-300 text-foreground'
|
|
: 'text-foreground-light hover:bg-surface-200'
|
|
)}
|
|
>
|
|
<span className="font-mono text-xs">{label}</span>
|
|
{count > 0 && (
|
|
<span className="text-xs text-foreground-lighter">{count}</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
{totalOverrideCount > 0 && (
|
|
<div className="mt-auto p-2 border-t">
|
|
<Button type="outline" size="tiny" block onClick={clearAllOverrides}>
|
|
Reset & Reload
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Flag list */}
|
|
<div className="flex-1 min-h-0 overflow-y-auto pb-6">
|
|
<div className="divide-y">
|
|
{flagsSubTab === 'posthog' &&
|
|
(Object.keys(posthogFlags).length === 0 ? (
|
|
<div className="text-center text-foreground-lighter py-8 text-sm">
|
|
No PostHog feature flags loaded yet.
|
|
</div>
|
|
) : (
|
|
Object.entries(posthogFlags).map(([flagName, flagValue]) => (
|
|
<FlagRow
|
|
key={flagName}
|
|
flagName={flagName}
|
|
currentValue={
|
|
phFlagOverrides[flagName] ?? phFlagOriginals[flagName] ?? flagValue
|
|
}
|
|
originalValue={phFlagOriginals[flagName] ?? flagValue}
|
|
isOverridden={flagName in phFlagOverrides}
|
|
onToggle={(value) => togglePhFlagOverride(flagName, value)}
|
|
/>
|
|
))
|
|
))}
|
|
{flagsSubTab === 'configcat' &&
|
|
(Object.keys(configcatFlags).length === 0 ? (
|
|
<div className="text-center text-foreground-lighter py-8 text-sm">
|
|
No ConfigCat feature flags loaded yet.
|
|
</div>
|
|
) : (
|
|
Object.entries(configcatFlags).map(([flagName, flagValue]) => (
|
|
<FlagRow
|
|
key={flagName}
|
|
flagName={flagName}
|
|
currentValue={
|
|
ccFlagOverrides[flagName] ?? ccFlagOriginals[flagName] ?? flagValue
|
|
}
|
|
originalValue={ccFlagOriginals[flagName] ?? flagValue}
|
|
isOverridden={flagName in ccFlagOverrides}
|
|
onToggle={(value) => toggleCcFlagOverride(flagName, value)}
|
|
/>
|
|
))
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{extraTabs.map((tab) => (
|
|
<div
|
|
key={tab.id}
|
|
className={cn('flex-1 min-h-0 overflow-y-auto', activeTab !== tab.id && 'hidden')}
|
|
>
|
|
{tab.content}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Tabs>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|