xref: /aosp_15_r20/external/perfetto/ui/src/frontend/value.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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