go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/property_viewer/property_viewer.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { EditorConfiguration, ModeSpec } from 'codemirror'; 16 import { useRef } from 'react'; 17 import { useLatest } from 'react-use'; 18 19 import { PropertyViewerConfigInstance } from '@/common/store/user_config/build_config'; 20 import { CodeMirrorEditor } from '@/generic_libs/components/code_mirror_editor'; 21 22 export interface PropertyViewerProps { 23 readonly properties: { readonly [key: string]: unknown }; 24 readonly config: PropertyViewerConfigInstance; 25 readonly onInit?: (editor: CodeMirror.Editor) => void; 26 } 27 28 export function PropertyViewer({ 29 properties, 30 config, 31 onInit, 32 }: PropertyViewerProps) { 33 const configRef = useLatest(config); 34 const formattedValue = JSON.stringify(properties, undefined, 2); 35 36 // Ensure the callbacks always refer to the latest values. 37 const formattedValueLines = useLatest(formattedValue.split('\n')); 38 const editorOptions = useRef<EditorConfiguration>({ 39 mode: { name: 'javascript', json: true } as ModeSpec<{ json: boolean }>, 40 readOnly: true, 41 matchBrackets: true, 42 lineWrapping: true, 43 foldGutter: true, 44 lineNumbers: true, 45 // Ensures all nodes are rendered therefore searchable. 46 viewportMargin: Infinity, 47 gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 48 }); 49 50 function setFolded(lineNum: number, folded: boolean) { 51 const line = formattedValueLines.current[lineNum]; 52 // Not a root-level key, ignore. 53 if (!line.startsWith(' "')) { 54 return; 55 } 56 configRef.current.setFolded(line, folded); 57 } 58 59 return ( 60 <CodeMirrorEditor 61 value={formattedValue} 62 initOptions={editorOptions.current} 63 onInit={(editor: CodeMirror.Editor) => { 64 editor.on('fold', (_, from) => setFolded(from.line, true)); 65 editor.on('unfold', (_, from) => setFolded(from.line, false)); 66 67 editor.on('changes', () => { 68 formattedValueLines.current.forEach((line, lineIndex) => { 69 if (configRef.current.isFolded(line)) { 70 // This also triggers the fold event listener above. This helps 71 // keeping the keys fresh. 72 editor.foldCode(lineIndex, undefined, 'fold'); 73 } 74 }); 75 }); 76 onInit?.(editor); 77 }} 78 css={{ 79 '& .CodeMirror-scroll': { 80 minWidth: '400px', 81 maxWidth: '1000px', 82 minHeight: '100px', 83 maxHeight: '600px', 84 }, 85 }} 86 /> 87 ); 88 }