blob: a57f2eac13e59d6e00acae11d9aea525a576c12b [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 {Tree, TreeNode} from '../widgets/tree';
import {PopupMenu2} from '../widgets/menu';
import {Button} from '../widgets/button';
// 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?: m.Child[];
}
// 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): m.Children {
const left = [
name,
value.contextMenu
? m(
PopupMenu2,
{
trigger: m(Button, {
icon: 'arrow_drop_down',
}),
},
value.contextMenu,
)
: null,
];
if (isArray(value)) {
const nodes = value.items.map((value: Value, index: number) => {
return renderValue(`[${index}]`, value);
});
return m(TreeNode, {left, right: `array[${nodes.length}]`}, nodes);
} else if (isDict(value)) {
const nodes: m.Children[] = [];
for (const key of Object.keys(value.items)) {
nodes.push(renderValue(key, value.items[key]));
}
return m(TreeNode, {left, right: `dict`}, nodes);
} else {
const renderButton = (button?: ButtonParams) => {
if (!button) {
return null;
}
return m(
'i.material-icons.grey',
{
onclick: button.action,
title: button.hoverText,
},
button.icon ?? 'call_made',
);
};
if (value.kind === 'STRING') {
const right = [
renderButton(value.leftButton),
m('span', value.value),
renderButton(value.rightButton),
];
return m(TreeNode, {left, right});
} else {
return null;
}
}
}
// Render a given dictionary to a tree.
export function renderDict(dict: Dict): m.Child {
const rows: m.Children[] = [];
for (const key of Object.keys(dict.items)) {
rows.push(renderValue(key, dict.items[key]));
}
return m(Tree, rows);
}