44 * Uses react-simple-code-editor + prism-react-renderer for minimal bundle size (~18KB)
55 */
66
7- import { useState , useCallback , useMemo } from 'react' ;
7+ import { useState , useCallback , useMemo , useEffect , useRef } from 'react' ;
88import Editor from 'react-simple-code-editor' ;
99import { Highlight , themes } from 'prism-react-renderer' ;
1010import { useTheme } from '@/hooks/use-theme' ;
1111import { cn } from '@/lib/utils' ;
12- import { AlertCircle , CheckCircle2 } from 'lucide-react' ;
12+ import { isSensitiveKey } from '@/lib/sensitive-keys' ;
13+ import { AlertCircle , CheckCircle2 , Eye , EyeOff } from 'lucide-react' ;
14+ import { Button } from '@/components/ui/button' ;
1315
1416interface CodeEditorProps {
1517 value : string ;
@@ -71,6 +73,19 @@ export function CodeEditor({
7173} : CodeEditorProps ) {
7274 const { isDark } = useTheme ( ) ;
7375 const [ isFocused , setIsFocused ] = useState ( false ) ;
76+ const [ isMasked , setIsMasked ] = useState ( true ) ;
77+ // Force Editor remount when theme changes (works around react-simple-code-editor caching)
78+ const [ editorKey , setEditorKey ] = useState ( 0 ) ;
79+ const isFirstRender = useRef ( true ) ;
80+
81+ useEffect ( ( ) => {
82+ // Skip first render, only trigger on theme changes
83+ if ( isFirstRender . current ) {
84+ isFirstRender . current = false ;
85+ return ;
86+ }
87+ setEditorKey ( ( k ) => k + 1 ) ;
88+ } , [ isDark ] ) ;
7489
7590 // Validate on every change for JSON
7691 const validation = useMemo ( ( ) => {
@@ -81,32 +96,76 @@ export function CodeEditor({
8196 } , [ value , language ] ) ;
8297
8398 // Highlight function using prism-react-renderer
99+ // Note: Line numbers removed - they break textarea/pre alignment in react-simple-code-editor
84100 const highlightCode = useCallback (
85101 ( code : string ) => (
86102 < Highlight theme = { isDark ? themes . nightOwl : themes . github } code = { code } language = { language } >
87- { ( { tokens, getLineProps, getTokenProps } ) => (
88- < >
89- { tokens . map ( ( line , i ) => (
90- < div
91- key = { i }
92- { ...getLineProps ( { line } ) }
93- className = { cn ( 'table-row' , validation . line === i + 1 && 'bg-destructive/20' ) }
94- >
95- < span className = "table-cell pr-4 text-right text-muted-foreground select-none opacity-50 text-xs w-8" >
96- { i + 1 }
97- </ span >
98- < span className = "table-cell" >
99- { line . map ( ( token , key ) => (
100- < span key = { key } { ...getTokenProps ( { token } ) } />
101- ) ) }
102- </ span >
103- </ div >
104- ) ) }
105- </ >
106- ) }
103+ { ( { tokens, getLineProps, getTokenProps } ) => {
104+ let nextValueIsSensitive = false ;
105+
106+ return (
107+ < >
108+ { tokens . map ( ( line , i ) => (
109+ < div
110+ key = { i }
111+ { ...getLineProps ( { line } ) }
112+ className = { cn ( validation . line === i + 1 && 'bg-destructive/20' ) }
113+ >
114+ { line . map ( ( token , key ) => {
115+ let isSensitive = false ;
116+
117+ // Check for sensitive keys
118+ if ( token . types . includes ( 'property' ) ) {
119+ const content = token . content . replace ( / [ ' " ] / g, '' ) ;
120+ // Use shared sensitive key detection utility
121+ if ( isSensitiveKey ( content ) ) {
122+ nextValueIsSensitive = true ;
123+ } else {
124+ nextValueIsSensitive = false ;
125+ }
126+ }
127+ // Apply masking to values following sensitive keys
128+ else if (
129+ ( token . types . includes ( 'string' ) ||
130+ token . types . includes ( 'number' ) ||
131+ token . types . includes ( 'boolean' ) ) &&
132+ nextValueIsSensitive
133+ ) {
134+ isSensitive = true ;
135+ // Consumes the flag for this value
136+ nextValueIsSensitive = false ;
137+ }
138+ // Reset flag on commas or new keys (handled by property check),
139+ // but persist through colons and whitespace
140+ else if ( token . types . includes ( 'punctuation' ) ) {
141+ if (
142+ token . content !== ':' &&
143+ token . content !== '[' &&
144+ token . content !== '{'
145+ ) {
146+ nextValueIsSensitive = false ;
147+ }
148+ }
149+
150+ const tokenProps = getTokenProps ( { token } ) ;
151+
152+ if ( isSensitive && isMasked ) {
153+ tokenProps . className = cn (
154+ tokenProps . className ,
155+ 'blur-[3px] select-none opacity-70 transition-all duration-200'
156+ ) ;
157+ }
158+
159+ return < span key = { key } { ...tokenProps } /> ;
160+ } ) }
161+ </ div >
162+ ) ) }
163+ </ >
164+ ) ;
165+ } }
107166 </ Highlight >
108167 ) ,
109- [ isDark , language , validation . line ]
168+ [ isDark , language , validation . line , isMasked ]
110169 ) ;
111170
112171 return (
@@ -126,7 +185,7 @@ export function CodeEditor({
126185 value = { value }
127186 onValueChange = { readonly ? ( ) => { } : onChange }
128187 highlight = { highlightCode }
129- key = { isDark ? 'dark' : 'light' }
188+ key = { editorKey }
130189 padding = { 12 }
131190 disabled = { readonly }
132191 onFocus = { ( ) => setIsFocused ( true ) }
@@ -142,6 +201,19 @@ export function CodeEditor({
142201 minHeight,
143202 } }
144203 />
204+
205+ { /* Secrets Toggle Overlay */ }
206+ < div className = "absolute top-2 right-2 z-10 opacity-50 hover:opacity-100 transition-opacity" >
207+ < Button
208+ variant = "ghost"
209+ size = "icon"
210+ className = "h-6 w-6 bg-background/50 hover:bg-background border shadow-sm rounded-full"
211+ onClick = { ( ) => setIsMasked ( ! isMasked ) }
212+ title = { isMasked ? 'Reveal sensitive values' : 'Mask sensitive values' }
213+ >
214+ { isMasked ? < Eye className = "h-3 w-3" /> : < EyeOff className = "h-3 w-3" /> }
215+ </ Button >
216+ </ div >
145217 </ div >
146218
147219 { /* Validation status */ }
0 commit comments