blob: 9b866b0e30b6387cbbec0fb8552e00bae9d79d1d [file] [log] [blame]
// 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 {PopupMenuButton, PopupMenuItem} from './popup_menu';
// 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?: PopupMenuItem[];
}
// 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, depth: number): Generator<m.Child> {
const row = [
m('th',
{
style: `padding-left: ${15 * depth}px`,
},
name,
value.contextMenu ? m(PopupMenuButton, {
icon: 'arrow_drop_down',
items: value.contextMenu,
}) :
null),
];
if (isArray(value)) {
yield m('tr', row);
for (let i = 0; i < value.items.length; ++i) {
yield* renderValue(`[${i}]`, value.items[i], depth + 1);
}
return;
} else if (isDict(value)) {
yield m('tr', row);
for (const key of Object.keys(value.items)) {
yield* renderValue(key, value.items[key], depth + 1);
}
return;
}
const renderButton = (button?: ButtonParams) => {
if (!button) {
return null;
}
return m(
'i.material-icons.grey',
{
onclick: button.action,
title: button.hoverText,
},
button.icon ? button.icon : 'call_made');
};
if (value.kind === 'STRING') {
row.push(
m('td',
renderButton(value.leftButton),
m('span', value.value),
renderButton(value.rightButton)));
}
yield m('tr', row);
}
// Render a given dictionary into a vnode.
export function renderDict(dict: Dict): m.Child {
const rows: m.Child[] = [];
for (const key of Object.keys(dict.items)) {
for (const vnode of renderValue(key, dict.items[key], 0)) {
rows.push(vnode);
}
}
return m('table.auto-layout', rows);
}