// Copyright (C) 2024 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import m from 'mithril'; import {isString} from '../../base/object_utils'; import {Icons} from '../../base/semantic_icons'; import {Engine} from '../../trace_processor/engine'; import {Row} from '../../trace_processor/query_result'; import {Anchor} from '../../widgets/anchor'; import {BasicTable} from '../../widgets/basic_table'; import {Button} from '../../widgets/button'; import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu'; import {Spinner} from '../../widgets/spinner'; import {ArgumentSelector} from './argument_selector'; import {argColumn, Column, columnFromSqlTableColumn} from './column'; import {renderCell} from './render_cell'; import {SqlTableState} from './state'; import {isArgSetIdColumn, SqlTableDescription} from './table_description'; import {Intent} from '../../widgets/common'; import {addHistogramTab} from '../charts/histogram/tab'; export interface SqlTableConfig { readonly state: SqlTableState; } export class SqlTable implements m.ClassComponent { private readonly table: SqlTableDescription; private readonly engine: Engine; private state: SqlTableState; constructor(vnode: m.Vnode) { this.state = vnode.attrs.state; this.table = this.state.table; this.engine = this.state.engine; } renderFilters(): m.Children { const filters: m.Child[] = []; for (const filter of this.state.getFilters()) { const label = isString(filter) ? filter : `Arg(${filter.argName}) ${filter.op}`; filters.push( m(Button, { label, icon: 'close', intent: Intent.Primary, onclick: () => { this.state.removeFilter(filter); }, }), ); } return filters; } renderAddColumnOptions(addColumn: (column: Column) => void): m.Children { // We do not want to add columns which already exist, so we track the // columns which we are already showing here. // TODO(altimin): Theoretically a single table can have two different // arg_set_ids, so we should track (arg_set_id_column, arg_name) pairs here. const existingColumns = new Set(); for (const column of this.state.getSelectedColumns()) { existingColumns.add(column.alias); } const result = []; for (const column of this.table.columns) { if (existingColumns.has(column.name)) continue; if (isArgSetIdColumn(column)) { result.push( m( MenuItem, { label: column.name, }, m(ArgumentSelector, { engine: this.engine, argSetId: column, tableName: this.table.name, constraints: this.state.getQueryConstraints(), alreadySelectedColumns: existingColumns, onArgumentSelected: (argument: string) => { addColumn(argColumn(this.table.name, column, argument)); }, }), ), ); continue; } result.push( m(MenuItem, { label: column.name, onclick: () => addColumn(columnFromSqlTableColumn(column)), }), ); } return result; } renderColumnHeader(column: Column, index: number) { const sorted = this.state.isSortedBy(column); const icon = sorted === 'ASC' ? Icons.SortedAsc : sorted === 'DESC' ? Icons.SortedDesc : Icons.ContextMenu; return m( PopupMenu2, { trigger: m(Anchor, {icon}, column.title), }, sorted !== 'DESC' && m(MenuItem, { label: 'Sort: highest first', icon: Icons.SortedDesc, onclick: () => { this.state.sortBy({column, direction: 'DESC'}); }, }), sorted !== 'ASC' && m(MenuItem, { label: 'Sort: lowest first', icon: Icons.SortedAsc, onclick: () => { this.state.sortBy({column, direction: 'ASC'}); }, }), sorted !== undefined && m(MenuItem, { label: 'Unsort', icon: Icons.Close, onclick: () => this.state.unsort(), }), this.state.getSelectedColumns().length > 1 && m(MenuItem, { label: 'Hide', icon: Icons.Hide, onclick: () => this.state.hideColumnAtIndex(index), }), m(MenuItem, { label: 'Create histogram', icon: Icons.Chart, onclick: () => { addHistogramTab( { sqlColumn: column.alias, columnTitle: column.title, filters: this.state.getFilters(), tableDisplay: this.table.displayName, query: this.state.buildSqlSelectStatement().selectStatement, }, this.engine, ); }, }), // Menu items before divider apply to selected column m(MenuDivider), // Menu items after divider apply to entire table m( MenuItem, {label: 'Add column', icon: Icons.AddColumn}, this.renderAddColumnOptions((column) => { this.state.addColumn(column, index); }), ), ); } view() { const rows = this.state.getDisplayedRows(); return [ m('div', this.renderFilters()), m(BasicTable, { data: rows, columns: this.state.getSelectedColumns().map((column, i) => ({ title: this.renderColumnHeader(column, i), render: (row: Row) => renderCell(column, row, this.state), })), }), this.state.isLoading() && m(Spinner), this.state.getQueryError() !== undefined && m('.query-error', this.state.getQueryError()), ]; } } export {SqlTableDescription};