// Copyright (C) 2023 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use size file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {Tree, TreeNode} from '../widgets/tree'; import {PopupMenu2} from '../widgets/menu'; import {Button} from '../widgets/button'; // This file implements a component for rendering JSON-like values (with // customisation options like context menu and action buttons). // // It defines the common Value, StringValue, DictValue, ArrayValue types, // to be used as an interchangeable format between different components // and `renderValue` function to convert DictValue into vdom nodes. // Leaf (non-dict and non-array) value which can be displayed to the user // together with the rendering customisation parameters. type StringValue = { kind: 'STRING'; value: string; } & StringValueParams; // Helper function to create a StringValue from string together with optional // parameters. export function value(value: string, params?: StringValueParams): StringValue { return { kind: 'STRING', value, ...params, }; } // Helper function to convert a potentially undefined value to StringValue or // null. export function maybeValue( v?: string, params?: StringValueParams, ): StringValue | null { if (!v) { return null; } return value(v, params); } // A basic type for the JSON-like value, comprising a primitive type (string) // and composite types (arrays and dicts). export type Value = StringValue | Array | Dict; // Dictionary type. export type Dict = { kind: 'DICT'; items: {[name: string]: Value}; } & ValueParams; // Helper function to simplify creation of a dictionary. // This function accepts and filters out nulls as values in the passed // dictionary (useful for simplifying the code to render optional values). export function dict( items: {[name: string]: Value | null}, params?: ValueParams, ): Dict { const result: {[name: string]: Value} = {}; for (const [name, value] of Object.entries(items)) { if (value !== null) { result[name] = value; } } return { kind: 'DICT', items: result, ...params, }; } // Array type. export type Array = { kind: 'ARRAY'; items: Value[]; } & ValueParams; // Helper function to simplify creation of an array. // This function accepts and filters out nulls in the passed array (useful for // simplifying the code to render optional values). export function array(items: (Value | null)[], params?: ValueParams): Array { return { kind: 'ARRAY', items: items.filter((item: Value | null) => item !== null) as Value[], ...params, }; } // Parameters for displaying a button next to a value to perform // the context-dependent action (i.e. go to the corresponding slice). type ButtonParams = { action: () => void; hoverText?: string; icon?: string; }; // Customisation parameters which apply to any Value (e.g. context menu). interface ValueParams { contextMenu?: m.Child[]; } // Customisation parameters which apply for a primitive value (e.g. showing // button next to a string, or making it clickable, or adding onhover effect). interface StringValueParams extends ValueParams { leftButton?: ButtonParams; rightButton?: ButtonParams; } export function isArray(value: Value): value is Array { return value.kind === 'ARRAY'; } export function isDict(value: Value): value is Dict { return value.kind === 'DICT'; } export function isStringValue(value: Value): value is StringValue { return !isArray(value) && !isDict(value); } // Recursively render the given value and its children, returning a list of // vnodes corresponding to the nodes of the table. function renderValue(name: string, value: Value): m.Children { const left = [ name, value.contextMenu ? m( PopupMenu2, { trigger: m(Button, { icon: 'arrow_drop_down', }), }, value.contextMenu, ) : null, ]; if (isArray(value)) { const nodes = value.items.map((value: Value, index: number) => { return renderValue(`[${index}]`, value); }); return m(TreeNode, {left, right: `array[${nodes.length}]`}, nodes); } else if (isDict(value)) { const nodes: m.Children[] = []; for (const key of Object.keys(value.items)) { nodes.push(renderValue(key, value.items[key])); } return m(TreeNode, {left, right: `dict`}, nodes); } else { const renderButton = (button?: ButtonParams) => { if (!button) { return null; } return m( 'i.material-icons.grey', { onclick: button.action, title: button.hoverText, }, button.icon ?? 'call_made', ); }; if (value.kind === 'STRING') { const right = [ renderButton(value.leftButton), m('span', value.value), renderButton(value.rightButton), ]; return m(TreeNode, {left, right}); } else { return null; } } } // Render a given dictionary to a tree. export function renderDict(dict: Dict): m.Child { const rows: m.Children[] = []; for (const key of Object.keys(dict.items)) { rows.push(renderValue(key, dict.items[key])); } return m(Tree, rows); }