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