blob: 0dff8cd2c4bb446de14fb00c1ad7e763a514b711 [file] [log] [blame]
// 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};