xref: /aosp_15_r20/external/perfetto/ui/src/components/widgets/sql/table/render_cell_utils.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this 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 {TableManager, SqlColumn} from './column';
17import {MenuItem, PopupMenu2} from '../../../../widgets/menu';
18import {SqlValue} from '../../../../trace_processor/query_result';
19import {isString} from '../../../../base/object_utils';
20import {sqliteString} from '../../../../base/string_utils';
21import {Icons} from '../../../../base/semantic_icons';
22import {copyToClipboard} from '../../../../base/clipboard';
23import {sqlValueToReadableString} from '../../../../trace_processor/sql_utils';
24import {Anchor} from '../../../../widgets/anchor';
25
26interface FilterOp {
27  op: string;
28  requiresParam: boolean; // Denotes if the operator acts on an input value
29}
30
31export enum FilterOption {
32  GLOB = 'glob',
33  EQUALS_TO = 'equals to',
34  NOT_EQUALS_TO = 'not equals to',
35  GREATER_THAN = 'greater than',
36  GREATER_OR_EQUALS_THAN = 'greater or equals than',
37  LESS_THAN = 'less than',
38  LESS_OR_EQUALS_THAN = 'less or equals than',
39  IS_NULL = 'is null',
40  IS_NOT_NULL = 'is not null',
41}
42
43export const FILTER_OPTION_TO_OP: Record<FilterOption, FilterOp> = {
44  [FilterOption.GLOB]: {op: 'glob', requiresParam: true},
45  [FilterOption.EQUALS_TO]: {op: '=', requiresParam: true},
46  [FilterOption.NOT_EQUALS_TO]: {op: '!=', requiresParam: true},
47  [FilterOption.GREATER_THAN]: {op: '>', requiresParam: true},
48  [FilterOption.GREATER_OR_EQUALS_THAN]: {op: '>=', requiresParam: true},
49  [FilterOption.LESS_THAN]: {op: '<', requiresParam: true},
50  [FilterOption.LESS_OR_EQUALS_THAN]: {op: '<=', requiresParam: true},
51  [FilterOption.IS_NULL]: {op: 'IS NULL', requiresParam: false},
52  [FilterOption.IS_NOT_NULL]: {op: 'IS NOT NULL', requiresParam: false},
53};
54
55export const NUMERIC_FILTER_OPTIONS = [
56  FilterOption.EQUALS_TO,
57  FilterOption.NOT_EQUALS_TO,
58  FilterOption.GREATER_THAN,
59  FilterOption.GREATER_OR_EQUALS_THAN,
60  FilterOption.LESS_THAN,
61  FilterOption.LESS_OR_EQUALS_THAN,
62];
63
64export const STRING_FILTER_OPTIONS = [
65  FilterOption.EQUALS_TO,
66  FilterOption.NOT_EQUALS_TO,
67];
68
69export const NULL_FILTER_OPTIONS = [
70  FilterOption.IS_NULL,
71  FilterOption.IS_NOT_NULL,
72];
73
74function filterOptionMenuItem(
75  label: string,
76  column: SqlColumn,
77  filterOp: (cols: string[]) => string,
78  tableManager: TableManager,
79): m.Child {
80  return m(MenuItem, {
81    label,
82    onclick: () => {
83      tableManager.addFilter({op: filterOp, columns: [column]});
84    },
85  });
86}
87
88// Return a list of "standard" menu items, adding corresponding filters to the given cell.
89export function getStandardFilters(
90  value: SqlValue,
91  c: SqlColumn,
92  tableManager: TableManager,
93): m.Child[] {
94  if (value === null) {
95    return NULL_FILTER_OPTIONS.map((option) =>
96      filterOptionMenuItem(
97        option,
98        c,
99        (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option].op}`,
100        tableManager,
101      ),
102    );
103  }
104  if (isString(value)) {
105    return STRING_FILTER_OPTIONS.map((option) =>
106      filterOptionMenuItem(
107        option,
108        c,
109        (cols) =>
110          `${cols[0]} ${FILTER_OPTION_TO_OP[option].op} ${sqliteString(value)}`,
111        tableManager,
112      ),
113    );
114  }
115  if (typeof value === 'bigint' || typeof value === 'number') {
116    return NUMERIC_FILTER_OPTIONS.map((option) =>
117      filterOptionMenuItem(
118        option,
119        c,
120        (cols) => `${cols[0]} ${FILTER_OPTION_TO_OP[option].op} ${value}`,
121        tableManager,
122      ),
123    );
124  }
125  return [];
126}
127
128function copyMenuItem(label: string, value: string): m.Child {
129  return m(MenuItem, {
130    icon: Icons.Copy,
131    label,
132    onclick: () => {
133      copyToClipboard(value);
134    },
135  });
136}
137
138// Return a list of "standard" menu items for the given cell.
139export function getStandardContextMenuItems(
140  value: SqlValue,
141  column: SqlColumn,
142  tableManager: TableManager,
143): m.Child[] {
144  const result: m.Child[] = [];
145
146  if (isString(value)) {
147    result.push(copyMenuItem('Copy', value));
148  }
149
150  const filters = getStandardFilters(value, column, tableManager);
151  if (filters.length > 0) {
152    result.push(
153      m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters),
154    );
155  }
156
157  return result;
158}
159
160export function displayValue(value: SqlValue): m.Child {
161  if (value === null) {
162    return m('i', 'NULL');
163  }
164  return sqlValueToReadableString(value);
165}
166
167export function renderStandardCell(
168  value: SqlValue,
169  column: SqlColumn,
170  tableManager: TableManager,
171): m.Children {
172  const contextMenuItems: m.Child[] = getStandardContextMenuItems(
173    value,
174    column,
175    tableManager,
176  );
177  return m(
178    PopupMenu2,
179    {
180      trigger: m(Anchor, displayValue(value)),
181    },
182    ...contextMenuItems,
183  );
184}
185