| // 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); |
| } |