• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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