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