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 {SqlColumn} from './sql_column'; 17import {MenuItem, PopupMenu} 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'; 25import {TableManager} from './table_column'; 26 27export interface LegacySqlTableFilterOp { 28 op: string; // string representation of the operation (to be injected to SQL) 29 label: LegacySqlTableFilterLabel; // human readable name for operation 30 requiresParam?: boolean; // Denotes if the operator acts on an input value 31} 32 33export type LegacySqlTableFilterLabel = 34 | 'glob' 35 | 'equals to' 36 | 'not equals to' 37 | 'greater than' 38 | 'greater or equals than' 39 | 'less than' 40 | 'less or equals than' 41 | 'is null' 42 | 'is not null'; 43 44export const LegacySqlTableFilterOptions: Record< 45 LegacySqlTableFilterLabel, 46 LegacySqlTableFilterOp 47> = { 48 'glob': {op: 'glob', label: 'glob', requiresParam: true}, 49 'equals to': {op: '=', label: 'equals to', requiresParam: true}, 50 'not equals to': {op: '!=', label: 'not equals to', requiresParam: true}, 51 'greater than': {op: '>', label: 'greater than', requiresParam: true}, 52 'greater or equals than': { 53 op: '>=', 54 label: 'greater or equals than', 55 requiresParam: true, 56 }, 57 'less than': {op: '<', label: 'less than', requiresParam: true}, 58 'less or equals than': { 59 op: '<=', 60 label: 'less or equals than', 61 requiresParam: true, 62 }, 63 'is null': {op: 'IS NULL', label: 'is null', requiresParam: false}, 64 'is not null': { 65 op: 'IS NOT NULL', 66 label: 'is not null', 67 requiresParam: false, 68 }, 69}; 70 71export const NUMERIC_FILTER_OPTIONS: LegacySqlTableFilterLabel[] = [ 72 'equals to', 73 'not equals to', 74 'greater than', 75 'greater or equals than', 76 'less than', 77 'less or equals than', 78]; 79 80export const STRING_FILTER_OPTIONS: LegacySqlTableFilterLabel[] = [ 81 'equals to', 82 'not equals to', 83]; 84 85export const NULL_FILTER_OPTIONS: LegacySqlTableFilterLabel[] = [ 86 'is null', 87 'is not null', 88]; 89 90function filterOptionMenuItem( 91 label: string, 92 column: SqlColumn, 93 filterOp: (cols: string[]) => string, 94 tableManager: TableManager, 95): m.Child { 96 return m(MenuItem, { 97 label, 98 onclick: () => { 99 tableManager.filters.addFilter({op: filterOp, columns: [column]}); 100 }, 101 }); 102} 103 104// Return a list of "standard" menu items, adding corresponding filters to the given cell. 105export function getStandardFilters( 106 value: SqlValue, 107 c: SqlColumn, 108 tableManager: TableManager, 109): m.Child[] { 110 if (value === null) { 111 return NULL_FILTER_OPTIONS.map((label) => 112 filterOptionMenuItem( 113 label, 114 c, 115 (cols) => `${cols[0]} ${LegacySqlTableFilterOptions[label].op}`, 116 tableManager, 117 ), 118 ); 119 } 120 if (isString(value)) { 121 return STRING_FILTER_OPTIONS.map((label) => 122 filterOptionMenuItem( 123 label, 124 c, 125 (cols) => 126 `${cols[0]} ${LegacySqlTableFilterOptions[label].op} ${sqliteString(value)}`, 127 tableManager, 128 ), 129 ); 130 } 131 if (typeof value === 'bigint' || typeof value === 'number') { 132 return NUMERIC_FILTER_OPTIONS.map((label) => 133 filterOptionMenuItem( 134 label, 135 c, 136 (cols) => 137 `${cols[0]} ${LegacySqlTableFilterOptions[label].op} ${value}`, 138 tableManager, 139 ), 140 ); 141 } 142 return []; 143} 144 145function copyMenuItem(label: string, value: string): m.Child { 146 return m(MenuItem, { 147 icon: Icons.Copy, 148 label, 149 onclick: () => { 150 copyToClipboard(value); 151 }, 152 }); 153} 154 155// Return a list of "standard" menu items for the given cell. 156export function getStandardContextMenuItems( 157 value: SqlValue, 158 column: SqlColumn, 159 tableManager: TableManager, 160): m.Child[] { 161 const result: m.Child[] = []; 162 163 if (isString(value)) { 164 result.push(copyMenuItem('Copy', value)); 165 } 166 167 const filters = getStandardFilters(value, column, tableManager); 168 if (filters.length > 0) { 169 result.push( 170 m(MenuItem, {label: 'Add filter', icon: Icons.Filter}, ...filters), 171 ); 172 } 173 174 return result; 175} 176 177export function displayValue(value: SqlValue): m.Child { 178 if (value === null) { 179 return m('i', 'NULL'); 180 } 181 return sqlValueToReadableString(value); 182} 183 184export function renderStandardCell( 185 value: SqlValue, 186 column: SqlColumn, 187 tableManager: TableManager | undefined, 188): m.Children { 189 if (tableManager === undefined) { 190 return displayValue(value); 191 } 192 const contextMenuItems: m.Child[] = getStandardContextMenuItems( 193 value, 194 column, 195 tableManager, 196 ); 197 return m( 198 PopupMenu, 199 { 200 trigger: m(Anchor, displayValue(value)), 201 }, 202 ...contextMenuItems, 203 ); 204} 205