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'; 16 17import {isString} from '../../base/object_utils'; 18import {Icons} from '../../base/semantic_icons'; 19import {Engine} from '../../trace_processor/engine'; 20import {Row} from '../../trace_processor/query_result'; 21import {Anchor} from '../../widgets/anchor'; 22import {BasicTable} from '../../widgets/basic_table'; 23import {Button} from '../../widgets/button'; 24import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu'; 25import {Spinner} from '../../widgets/spinner'; 26 27import {ArgumentSelector} from './argument_selector'; 28import {argColumn, Column, columnFromSqlTableColumn} from './column'; 29import {renderCell} from './render_cell'; 30import {SqlTableState} from './state'; 31import {isArgSetIdColumn, SqlTableDescription} from './table_description'; 32import {Intent} from '../../widgets/common'; 33import {addHistogramTab} from '../charts/histogram/tab'; 34 35export interface SqlTableConfig { 36 readonly state: SqlTableState; 37} 38 39export class SqlTable implements m.ClassComponent<SqlTableConfig> { 40 private readonly table: SqlTableDescription; 41 private readonly engine: Engine; 42 43 private state: SqlTableState; 44 45 constructor(vnode: m.Vnode<SqlTableConfig>) { 46 this.state = vnode.attrs.state; 47 this.table = this.state.table; 48 this.engine = this.state.engine; 49 } 50 51 renderFilters(): m.Children { 52 const filters: m.Child[] = []; 53 for (const filter of this.state.getFilters()) { 54 const label = isString(filter) 55 ? filter 56 : `Arg(${filter.argName}) ${filter.op}`; 57 filters.push( 58 m(Button, { 59 label, 60 icon: 'close', 61 intent: Intent.Primary, 62 onclick: () => { 63 this.state.removeFilter(filter); 64 }, 65 }), 66 ); 67 } 68 return filters; 69 } 70 71 renderAddColumnOptions(addColumn: (column: Column) => void): m.Children { 72 // We do not want to add columns which already exist, so we track the 73 // columns which we are already showing here. 74 // TODO(altimin): Theoretically a single table can have two different 75 // arg_set_ids, so we should track (arg_set_id_column, arg_name) pairs here. 76 const existingColumns = new Set<string>(); 77 78 for (const column of this.state.getSelectedColumns()) { 79 existingColumns.add(column.alias); 80 } 81 82 const result = []; 83 for (const column of this.table.columns) { 84 if (existingColumns.has(column.name)) continue; 85 if (isArgSetIdColumn(column)) { 86 result.push( 87 m( 88 MenuItem, 89 { 90 label: column.name, 91 }, 92 m(ArgumentSelector, { 93 engine: this.engine, 94 argSetId: column, 95 tableName: this.table.name, 96 constraints: this.state.getQueryConstraints(), 97 alreadySelectedColumns: existingColumns, 98 onArgumentSelected: (argument: string) => { 99 addColumn(argColumn(this.table.name, column, argument)); 100 }, 101 }), 102 ), 103 ); 104 continue; 105 } 106 result.push( 107 m(MenuItem, { 108 label: column.name, 109 onclick: () => addColumn(columnFromSqlTableColumn(column)), 110 }), 111 ); 112 } 113 return result; 114 } 115 116 renderColumnHeader(column: Column, index: number) { 117 const sorted = this.state.isSortedBy(column); 118 const icon = 119 sorted === 'ASC' 120 ? Icons.SortedAsc 121 : sorted === 'DESC' 122 ? Icons.SortedDesc 123 : Icons.ContextMenu; 124 return m( 125 PopupMenu2, 126 { 127 trigger: m(Anchor, {icon}, column.title), 128 }, 129 sorted !== 'DESC' && 130 m(MenuItem, { 131 label: 'Sort: highest first', 132 icon: Icons.SortedDesc, 133 onclick: () => { 134 this.state.sortBy({column, direction: 'DESC'}); 135 }, 136 }), 137 sorted !== 'ASC' && 138 m(MenuItem, { 139 label: 'Sort: lowest first', 140 icon: Icons.SortedAsc, 141 onclick: () => { 142 this.state.sortBy({column, direction: 'ASC'}); 143 }, 144 }), 145 sorted !== undefined && 146 m(MenuItem, { 147 label: 'Unsort', 148 icon: Icons.Close, 149 onclick: () => this.state.unsort(), 150 }), 151 this.state.getSelectedColumns().length > 1 && 152 m(MenuItem, { 153 label: 'Hide', 154 icon: Icons.Hide, 155 onclick: () => this.state.hideColumnAtIndex(index), 156 }), 157 m(MenuItem, { 158 label: 'Create histogram', 159 icon: Icons.Chart, 160 onclick: () => { 161 addHistogramTab( 162 { 163 sqlColumn: column.alias, 164 columnTitle: column.title, 165 filters: this.state.getFilters(), 166 tableDisplay: this.table.displayName, 167 query: this.state.buildSqlSelectStatement().selectStatement, 168 }, 169 this.engine, 170 ); 171 }, 172 }), 173 // Menu items before divider apply to selected column 174 m(MenuDivider), 175 // Menu items after divider apply to entire table 176 m( 177 MenuItem, 178 {label: 'Add column', icon: Icons.AddColumn}, 179 this.renderAddColumnOptions((column) => { 180 this.state.addColumn(column, index); 181 }), 182 ), 183 ); 184 } 185 186 view() { 187 const rows = this.state.getDisplayedRows(); 188 189 return [ 190 m('div', this.renderFilters()), 191 m(BasicTable, { 192 data: rows, 193 columns: this.state.getSelectedColumns().map((column, i) => ({ 194 title: this.renderColumnHeader(column, i), 195 render: (row: Row) => renderCell(column, row, this.state), 196 })), 197 }), 198 this.state.isLoading() && m(Spinner), 199 this.state.getQueryError() !== undefined && 200 m('.query-error', this.state.getQueryError()), 201 ]; 202 } 203} 204 205export {SqlTableDescription}; 206