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  }