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