| // Copyright (C) 2023 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 {EngineProxy} 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'; |
| |
| export interface SqlTableConfig { |
| readonly state: SqlTableState; |
| } |
| |
| export class SqlTable implements m.ClassComponent<SqlTableConfig> { |
| private readonly table: SqlTableDescription; |
| private readonly engine: EngineProxy; |
| |
| private state: SqlTableState; |
| |
| constructor(vnode: m.Vnode<SqlTableConfig>) { |
| 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', |
| 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<string>(); |
| |
| 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(MenuDivider), |
| 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}; |