1- import { ActionIcon , Badge , Box , Code , CopyButton , Group , Loader , Space , Text , Tooltip } from '@mantine/core'
2- import { IconCopy } from '@tabler/icons-react'
3- import React from 'react'
4- import type { CatalogFlag } from './flagCatalog'
5- import type { FlagCatalogState } from './useFlagCatalog'
1+ import { ActionIcon , Box , Button , Code , CopyButton , Group , Loader , Space , Text , Tooltip } from '@mantine/core'
2+ import { IconArrowBackUp , IconCopy } from '@tabler/icons-react'
3+ import React , { type ReactNode } from 'react'
4+ import type { CatalogFlag } from './flagsRequests'
5+ import { useFlagsContext } from './flagsContext'
6+ import { validateOverrideValue } from './flagTypes'
7+ import { getOverride , type FlagOverride } from './inspectedPageFlags'
8+
9+ export function FlagCatalogBody ( ) {
10+ const { catalog, bottomFlags } = useFlagsContext ( )
611
7- export function FlagCatalogBody ( {
8- catalog,
9- flags,
10- total,
11- } : {
12- catalog : FlagCatalogState
13- flags : CatalogFlag [ ]
14- total : number
15- } ) {
1612 if ( catalog . loading ) {
1713 return (
1814 < Group justify = "center" py = "xl" >
@@ -28,31 +24,102 @@ export function FlagCatalogBody({
2824 return (
2925 < >
3026 < Text c = "dimmed" size = "xs" >
31- { total } { total === 1 ? 'flag' : 'flags' }
27+ { catalog . total } { catalog . total === 1 ? 'flag' : 'flags' }
3228 </ Text >
3329 < Space h = "xs" />
34- < Box style = { { border : '1px solid var(--mantine-color-gray-2)' , borderRadius : 'var(--mantine-radius-sm)' } } >
35- { flags . length === 0 ? (
36- < Text c = "dimmed" p = "md" >
37- No flags match.
38- </ Text >
39- ) : (
40- flags . map ( ( flag ) => < FlagRow key = { flag . key } flag = { flag } /> )
41- ) }
42- </ Box >
30+ < FlagList
31+ flags = { bottomFlags }
32+ borderColor = "var(--mantine-color-gray-2)"
33+ // `bottomFlags` is the page minus overridden flags (those are pinned above). Only call it "no
34+ // match" when the server total is 0; otherwise this page's flags are all overridden.
35+ emptyMessage = {
36+ catalog . total === 0 ? 'No flags match.' : 'All flags on this page are overridden — see Local overrides above.'
37+ }
38+ />
39+ </ >
40+ )
41+ }
42+
43+ /**
44+ * The always-visible "Local overrides" section shown above the paginated catalog. Lists every
45+ * overridden flag (regardless of which catalog page it's on), so overrides are never buried by
46+ * pagination. The overridden flags' catalog data comes from the context (see useOverriddenFlags).
47+ */
48+ export function OverridesSection ( ) {
49+ const { overriddenFlags } = useFlagsContext ( )
50+
51+ if ( overriddenFlags . length === 0 ) {
52+ return null
53+ }
54+ return (
55+ < >
56+ < Text fw = { 600 } size = "sm" >
57+ Local overrides ({ overriddenFlags . length } )
58+ </ Text >
59+ < Space h = "xs" />
60+ < FlagList flags = { overriddenFlags } borderColor = "var(--mantine-color-violet-2)" />
4361 </ >
4462 )
4563}
4664
47- function FlagRow ( { flag } : { flag : CatalogFlag } ) {
65+ // Renders a bordered list of flag rows, or `emptyMessage` when there are none. Shared by the catalog
66+ // body and the "Local overrides" section — they differ only in border color and empty copy. Reads
67+ // the override state + actions from context so each row's wiring stays identical.
68+ function FlagList ( {
69+ flags,
70+ borderColor,
71+ emptyMessage,
72+ } : {
73+ flags : CatalogFlag [ ]
74+ borderColor : string
75+ emptyMessage ?: ReactNode
76+ } ) {
77+ const { overrides, applyOverride, removeOverride } = useFlagsContext ( )
78+ return (
79+ < Box style = { { border : `1px solid ${ borderColor } ` , borderRadius : 'var(--mantine-radius-sm)' } } >
80+ { flags . length === 0 ? (
81+ < Text c = "dimmed" p = "md" >
82+ { emptyMessage }
83+ </ Text >
84+ ) : (
85+ flags . map ( ( flag ) => (
86+ < FlagRow
87+ key = { flag . key }
88+ flag = { flag }
89+ override = { getOverride ( overrides , flag . key ) }
90+ onSelectVariant = { applyOverride }
91+ onRevert = { removeOverride }
92+ />
93+ ) )
94+ ) }
95+ </ Box >
96+ )
97+ }
98+
99+ function FlagRow ( {
100+ flag,
101+ override,
102+ onSelectVariant,
103+ onRevert,
104+ } : {
105+ flag : CatalogFlag
106+ override : FlagOverride | undefined
107+ onSelectVariant : ( flagKey : string , override : FlagOverride ) => void
108+ onRevert : ( flagKey : string ) => void
109+ } ) {
110+ const overridden = override !== undefined
111+
48112 return (
49113 < Group
50114 justify = "space-between"
51115 wrap = "nowrap"
52116 align = "center"
53117 px = "sm"
54118 py = "xs"
55- style = { { borderBottom : '1px solid var(--mantine-color-gray-1)' } }
119+ style = { {
120+ borderBottom : '1px solid var(--mantine-color-gray-1)' ,
121+ backgroundColor : overridden ? 'var(--mantine-color-violet-0)' : undefined ,
122+ } }
56123 >
57124 < Box style = { { minWidth : 0 , flex : 1 } } >
58125 < Text size = "sm" fw = { 600 } truncate >
@@ -61,16 +128,41 @@ function FlagRow({ flag }: { flag: CatalogFlag }) {
61128 < FlagKey value = { flag . key } />
62129 </ Box >
63130 < Group gap = "xs" wrap = "wrap" justify = "flex-end" style = { { flexShrink : 0 , maxWidth : '55%' } } >
131+ { overridden && (
132+ < Tooltip label = "Revert override" >
133+ < ActionIcon variant = "subtle" color = "gray" size = "sm" onClick = { ( ) => onRevert ( flag . key ) } >
134+ < IconArrowBackUp size = { 16 } />
135+ </ ActionIcon >
136+ </ Tooltip >
137+ ) }
64138 { flag . variants . length === 0 ? (
65139 < Text c = "dimmed" size = "xs" >
66140 no variants
67141 </ Text >
68142 ) : (
69- flag . variants . map ( ( variant ) => (
70- < Badge key = { variant . name } variant = "light" color = "gray" title = { formatValue ( variant . value ) } >
71- { variant . name }
72- </ Badge >
73- ) )
143+ flag . variants . map ( ( variant ) => {
144+ const isActive = overridden && valuesEqual ( override . value , variant . value )
145+ // The catalog falls back to the raw string when a variant doesn't parse as its
146+ // declared type (see parseVariantValue) — writing that through would violate the
147+ // same contract validateOverrideValue enforces for manual overrides. `allowNull` keeps
148+ // a legitimate JSON `null` variant applyable (a raw-string type mismatch still fails).
149+ const validationError = validateOverrideValue ( flag . type , variant . value , { allowNull : true } )
150+ return (
151+ < Button
152+ key = { variant . name }
153+ size = "compact-xs"
154+ variant = { isActive ? 'filled' : 'default' }
155+ color = { isActive ? 'violet' : 'gray' }
156+ disabled = { ! ! validationError }
157+ onClick = { ( ) =>
158+ onSelectVariant ( flag . key , { type : flag . type , value : variant . value as FlagOverride [ 'value' ] } )
159+ }
160+ title = { validationError ?? formatValue ( variant . value ) }
161+ >
162+ { variant . name }
163+ </ Button >
164+ )
165+ } )
74166 ) }
75167 </ Group >
76168 </ Group >
@@ -104,6 +196,10 @@ function FlagKey({ value }: { value: string }) {
104196 )
105197}
106198
199+ function valuesEqual ( a : unknown , b : unknown ) : boolean {
200+ return JSON . stringify ( a ) === JSON . stringify ( b )
201+ }
202+
107203function formatValue ( value : unknown ) : string {
108204 return typeof value === 'string' ? value : JSON . stringify ( value )
109205}
0 commit comments