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