1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use size 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 15import m from 'mithril'; 16 17import {Tree, TreeNode} from '../widgets/tree'; 18 19import {PopupMenuButton, PopupMenuItem} from './popup_menu'; 20 21// This file implements a component for rendering JSON-like values (with 22// customisation options like context menu and action buttons). 23// 24// It defines the common Value, StringValue, DictValue, ArrayValue types, 25// to be used as an interchangeable format between different components 26// and `renderValue` function to convert DictValue into vdom nodes. 27 28// Leaf (non-dict and non-array) value which can be displayed to the user 29// together with the rendering customisation parameters. 30type StringValue = { 31 kind: 'STRING'; 32 value: string; 33} & StringValueParams; 34 35// Helper function to create a StringValue from string together with optional 36// parameters. 37export function value(value: string, params?: StringValueParams): StringValue { 38 return { 39 kind: 'STRING', 40 value, 41 ...params, 42 }; 43} 44 45// Helper function to convert a potentially undefined value to StringValue or 46// null. 47export function maybeValue( 48 v?: string, 49 params?: StringValueParams, 50): StringValue | null { 51 if (!v) { 52 return null; 53 } 54 return value(v, params); 55} 56 57// A basic type for the JSON-like value, comprising a primitive type (string) 58// and composite types (arrays and dicts). 59export type Value = StringValue | Array | Dict; 60 61// Dictionary type. 62export type Dict = { 63 kind: 'DICT'; 64 items: {[name: string]: Value}; 65} & ValueParams; 66 67// Helper function to simplify creation of a dictionary. 68// This function accepts and filters out nulls as values in the passed 69// dictionary (useful for simplifying the code to render optional values). 70export function dict( 71 items: {[name: string]: Value | null}, 72 params?: ValueParams, 73): Dict { 74 const result: {[name: string]: Value} = {}; 75 for (const [name, value] of Object.entries(items)) { 76 if (value !== null) { 77 result[name] = value; 78 } 79 } 80 return { 81 kind: 'DICT', 82 items: result, 83 ...params, 84 }; 85} 86 87// Array type. 88export type Array = { 89 kind: 'ARRAY'; 90 items: Value[]; 91} & ValueParams; 92 93// Helper function to simplify creation of an array. 94// This function accepts and filters out nulls in the passed array (useful for 95// simplifying the code to render optional values). 96export function array(items: (Value | null)[], params?: ValueParams): Array { 97 return { 98 kind: 'ARRAY', 99 items: items.filter((item: Value | null) => item !== null) as Value[], 100 ...params, 101 }; 102} 103 104// Parameters for displaying a button next to a value to perform 105// the context-dependent action (i.e. go to the corresponding slice). 106type ButtonParams = { 107 action: () => void; 108 hoverText?: string; 109 icon?: string; 110}; 111 112// Customisation parameters which apply to any Value (e.g. context menu). 113interface ValueParams { 114 contextMenu?: PopupMenuItem[]; 115} 116 117// Customisation parameters which apply for a primitive value (e.g. showing 118// button next to a string, or making it clickable, or adding onhover effect). 119interface StringValueParams extends ValueParams { 120 leftButton?: ButtonParams; 121 rightButton?: ButtonParams; 122} 123 124export function isArray(value: Value): value is Array { 125 return value.kind === 'ARRAY'; 126} 127 128export function isDict(value: Value): value is Dict { 129 return value.kind === 'DICT'; 130} 131 132export function isStringValue(value: Value): value is StringValue { 133 return !isArray(value) && !isDict(value); 134} 135 136// Recursively render the given value and its children, returning a list of 137// vnodes corresponding to the nodes of the table. 138function renderValue(name: string, value: Value): m.Children { 139 const left = [ 140 name, 141 value.contextMenu 142 ? m(PopupMenuButton, { 143 icon: 'arrow_drop_down', 144 items: value.contextMenu, 145 }) 146 : null, 147 ]; 148 if (isArray(value)) { 149 const nodes = value.items.map((value: Value, index: number) => { 150 return renderValue(`[${index}]`, value); 151 }); 152 return m(TreeNode, {left, right: `array[${nodes.length}]`}, nodes); 153 } else if (isDict(value)) { 154 const nodes: m.Children[] = []; 155 for (const key of Object.keys(value.items)) { 156 nodes.push(renderValue(key, value.items[key])); 157 } 158 return m(TreeNode, {left, right: `dict`}, nodes); 159 } else { 160 const renderButton = (button?: ButtonParams) => { 161 if (!button) { 162 return null; 163 } 164 return m( 165 'i.material-icons.grey', 166 { 167 onclick: button.action, 168 title: button.hoverText, 169 }, 170 button.icon ? button.icon : 'call_made', 171 ); 172 }; 173 if (value.kind === 'STRING') { 174 const right = [ 175 renderButton(value.leftButton), 176 m('span', value.value), 177 renderButton(value.rightButton), 178 ]; 179 return m(TreeNode, {left, right}); 180 } else { 181 return null; 182 } 183 } 184} 185 186// Render a given dictionary to a tree. 187export function renderDict(dict: Dict): m.Child { 188 const rows: m.Children[] = []; 189 for (const key of Object.keys(dict.items)) { 190 rows.push(renderValue(key, dict.items[key])); 191 } 192 return m(Tree, rows); 193} 194