• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import * as m from 'mithril';
18
19import {GenericSet} from '../base/generic_set';
20import {sqliteString} from '../base/string_utils';
21import {Actions} from '../common/actions';
22import {ColumnType} from '../common/query_result';
23import {
24  Area,
25  PivotTableReduxQuery,
26  PivotTableReduxResult
27} from '../common/state';
28import {PivotTree} from '../controller/pivot_table_redux_controller';
29
30import {globals} from './globals';
31import {Panel} from './panel';
32import {
33  aggregationIndex,
34  areaFilter,
35  createColumnSet,
36  generateQuery,
37  QueryGeneratorError,
38  sliceAggregationColumns,
39  Table,
40  TableColumn,
41  tables,
42  threadSliceAggregationColumns
43} from './pivot_table_redux_query_generator';
44
45interface ColumnSetCheckboxAttrs {
46  set: GenericSet<TableColumn>;
47  setKey: TableColumn;
48}
49
50interface PathItem {
51  tree: PivotTree;
52  nextKey: ColumnType;
53}
54
55// Helper component that controls whether a particular key is present in a
56// ColumnSet.
57class ColumnSetCheckbox implements m.ClassComponent<ColumnSetCheckboxAttrs> {
58  view({attrs}: m.Vnode<ColumnSetCheckboxAttrs>) {
59    return m('input[type=checkbox]', {
60      onclick: (e: InputEvent) => {
61        const target = e.target as HTMLInputElement;
62        if (target.checked) {
63          attrs.set.add(attrs.setKey);
64        } else {
65          attrs.set.delete(attrs.setKey);
66        }
67        globals.rafScheduler.scheduleFullRedraw();
68      },
69      checked: attrs.set.has(attrs.setKey)
70    });
71  }
72}
73
74interface PivotTableReduxAttrs {
75  selectionArea: Area;
76}
77
78interface DrillFilter {
79  column: string;
80  value: ColumnType;
81}
82
83// Convert DrillFilter to SQL condition to be used in WHERE clause.
84function renderDrillFilter(filter: DrillFilter): string {
85  if (filter.value === null) {
86    return `${filter.column} IS NULL`;
87  } else if (typeof filter.value === 'number') {
88    return `${filter.column} = ${filter.value}`;
89  }
90  return `${filter.column} = ${sqliteString(filter.value)}`;
91}
92
93export class PivotTableRedux extends Panel<PivotTableReduxAttrs> {
94  selectedPivotsMap = createColumnSet();
95  selectedAggregations = createColumnSet();
96  constrainToArea = true;
97  editMode = true;
98
99  renderCanvas(): void {}
100
101  generateQuery(attrs: PivotTableReduxAttrs): PivotTableReduxQuery {
102    return generateQuery(
103        this.selectedPivotsMap,
104        this.selectedAggregations,
105        attrs.selectionArea,
106        this.constrainToArea);
107  }
108
109  runQuery(attrs: PivotTableReduxAttrs) {
110    try {
111      const query = this.generateQuery(attrs);
112      const lastPivotTableState = globals.state.pivotTableRedux;
113      globals.dispatch(Actions.setPivotStateReduxState({
114        pivotTableState: {
115          query,
116          queryId: lastPivotTableState.queryId + 1,
117          selectionArea: lastPivotTableState.selectionArea,
118          queryResult: null
119        }
120      }));
121    } catch (e) {
122      console.log(e);
123    }
124  }
125
126  renderTablePivotColumns(t: Table) {
127    return m(
128        'li',
129        t.name,
130        m('ul',
131          t.columns.map(
132              col =>
133                  m('li',
134                    m(ColumnSetCheckbox, {
135                      set: this.selectedPivotsMap,
136                      setKey: [t.name, col],
137                    }),
138                    col))));
139  }
140
141  renderResultsView(attrs: PivotTableReduxAttrs) {
142    return m(
143        '.pivot-table-redux',
144        m('button.mode-button',
145          {
146            onclick: () => {
147              this.editMode = true;
148              globals.rafScheduler.scheduleFullRedraw();
149            }
150          },
151          'Edit'),
152        this.renderResultsTable(attrs));
153  }
154
155  renderDrillDownCell(
156      area: Area, result: PivotTableReduxResult, filters: DrillFilter[]) {
157    return m(
158        'td',
159        m('button',
160          {
161            title: 'All corresponding slices',
162            onclick: () => {
163              const queryFilters = filters.map(renderDrillFilter);
164              if (this.constrainToArea) {
165                queryFilters.push(areaFilter(area));
166              }
167              const query = `
168                select * from ${result.metadata.tableName}
169                where ${queryFilters.join(' and \n')}
170              `;
171              // TODO(ddrone): the UI of running query as if it was a canned or
172              // custom query is a temporary one, replace with a proper UI.
173              globals.dispatch(Actions.executeQuery({
174                engineId: '0',
175                queryId: 'command',
176                query,
177              }));
178            }
179          },
180          m('i.material-icons', 'arrow_right')));
181  }
182
183  renderSectionRow(
184      area: Area, path: PathItem[], tree: PivotTree,
185      result: PivotTableReduxResult): m.Vnode {
186    const renderedCells = [];
187    for (let j = 0; j + 1 < path.length; j++) {
188      renderedCells.push(m('td', m('span.indent', ' '), `${path[j].nextKey}`));
189    }
190
191    const treeDepth = result.metadata.pivotColumns.length;
192    const colspan = treeDepth - path.length + 1;
193    const button =
194        m('button',
195          {
196            onclick: () => {
197              tree.isCollapsed = !tree.isCollapsed;
198              globals.rafScheduler.scheduleFullRedraw();
199            }
200          },
201          m('i.material-icons',
202            tree.isCollapsed ? 'expand_more' : 'expand_less'));
203
204    renderedCells.push(
205        m('td', {colspan}, button, `${path[path.length - 1].nextKey}`));
206
207    for (const value of tree.aggregates) {
208      renderedCells.push(m('td', `${value}`));
209    }
210
211    const drillFilters: DrillFilter[] = [];
212    for (let i = 0; i < path.length; i++) {
213      drillFilters.push({
214        value: `${path[i].nextKey}`,
215        column: result.metadata.pivotColumns[i]
216      });
217    }
218
219    renderedCells.push(this.renderDrillDownCell(area, result, drillFilters));
220    return m('tr', renderedCells);
221  }
222
223  renderTree(
224      area: Area, path: PathItem[], tree: PivotTree,
225      result: PivotTableReduxResult, sink: m.Vnode[]) {
226    if (tree.isCollapsed) {
227      sink.push(this.renderSectionRow(area, path, tree, result));
228      return;
229    }
230    if (tree.children.size > 0) {
231      // Avoid rendering the intermediate results row for the root of tree
232      // and in case there's only one child subtree.
233      if (!tree.isCollapsed && path.length > 0 && tree.children.size !== 1) {
234        sink.push(this.renderSectionRow(area, path, tree, result));
235      }
236      for (const [key, childTree] of tree.children.entries()) {
237        path.push({tree: childTree, nextKey: key});
238        this.renderTree(area, path, childTree, result, sink);
239        path.pop();
240      }
241      return;
242    }
243
244    // Avoid rendering the intermediate results row if it has only one leaf
245    // row.
246    if (!tree.isCollapsed && path.length > 0 && tree.rows.length > 1) {
247      sink.push(this.renderSectionRow(area, path, tree, result));
248    }
249    for (const row of tree.rows) {
250      const renderedCells = [];
251      const drillFilters: DrillFilter[] = [];
252      const treeDepth = result.metadata.pivotColumns.length;
253      for (let j = 0; j < treeDepth; j++) {
254        if (j < path.length) {
255          renderedCells.push(m('td', m('span.indent', ' '), `${row[j]}`));
256        } else {
257          renderedCells.push(m(`td`, `${row[j]}`));
258        }
259        drillFilters.push(
260            {column: result.metadata.pivotColumns[j], value: row[j]});
261      }
262      for (let j = 0; j < result.metadata.aggregationColumns.length; j++) {
263        const value = row[aggregationIndex(treeDepth, j, treeDepth)];
264        renderedCells.push(m('td', `${value}`));
265      }
266
267      renderedCells.push(this.renderDrillDownCell(area, result, drillFilters));
268      sink.push(m('tr', renderedCells));
269    }
270  }
271
272  renderTotalsRow(queryResult: PivotTableReduxResult) {
273    const overallValuesRow =
274        [m('td.total-values',
275           {'colspan': queryResult.metadata.pivotColumns.length},
276           m('strong', 'Total values:'))];
277    for (const aggValue of queryResult.tree.aggregates) {
278      overallValuesRow.push(m('td', `${aggValue}`));
279    }
280    overallValuesRow.push(m('td'));
281    return m('tr', overallValuesRow);
282  }
283
284  renderResultsTable(attrs: PivotTableReduxAttrs) {
285    const state = globals.state.pivotTableRedux;
286    if (state.query !== null || state.queryResult === null) {
287      return m('div', 'Loading...');
288    }
289
290    const renderedRows: m.Vnode[] = [];
291    const tree = state.queryResult.tree;
292
293    if (tree.children.size === 0 && tree.rows.length === 0) {
294      // Empty result, render a special message
295      return m('.empty-result', 'No slices in the current selection.');
296    }
297
298    this.renderTree(
299        attrs.selectionArea, [], tree, state.queryResult, renderedRows);
300
301    const allColumns = state.queryResult.metadata.pivotColumns.concat(
302        state.queryResult.metadata.aggregationColumns);
303    return m(
304        'table.query-table.pivot-table',
305        m('thead', m('tr', allColumns.map(column => m('td', column)), m('td'))),
306        m('tbody', this.renderTotalsRow(state.queryResult), renderedRows));
307  }
308
309  renderQuery(attrs: PivotTableReduxAttrs): m.Vnode {
310    // Prepare a button to switch to results mode.
311    let innerElement =
312        m('button.mode-button',
313          {
314            onclick: () => {
315              this.editMode = false;
316              this.runQuery(attrs);
317              globals.rafScheduler.scheduleFullRedraw();
318            }
319          },
320          'Execute');
321    try {
322      this.generateQuery(attrs);
323    } catch (e) {
324      if (e instanceof QueryGeneratorError) {
325        // If query generation fails, show an error message instead of a button.
326        innerElement = m('div.query-error', e.message);
327      } else {
328        throw e;
329      }
330    }
331
332    return m(
333        'div',
334        m('div',
335          m('input', {
336            type: 'checkbox',
337            id: 'constrain-to-selection',
338            checked: this.constrainToArea,
339            onclick: (e: InputEvent) => {
340              const checkbox = e.target as HTMLInputElement;
341              this.constrainToArea = checkbox.checked;
342            }
343          }),
344          m('label',
345            {
346              'for': 'constrain-to-selection',
347            },
348            'Constrain to current time range')),
349        innerElement);
350  }
351
352  view({attrs}: m.Vnode<PivotTableReduxAttrs>) {
353    return this.editMode ? this.renderEditView(attrs) :
354                           this.renderResultsView(attrs);
355  }
356
357  renderEditView(attrs: PivotTableReduxAttrs) {
358    return m(
359        '.pivot-table-redux.edit',
360        m('div',
361          m('h2', 'Pivots'),
362          m('ul',
363            tables.map(
364                t => this.renderTablePivotColumns(t),
365                ))),
366        m('div',
367          m('h2', 'Aggregations'),
368          m('ul',
369            ...sliceAggregationColumns.map(
370                t =>
371                    m('li',
372                      m(ColumnSetCheckbox, {
373                        set: this.selectedAggregations,
374                        setKey: ['slice', t],
375                      }),
376                      t)),
377            ...threadSliceAggregationColumns.map(
378                t =>
379                    m('li',
380                      m(ColumnSetCheckbox, {
381                        set: this.selectedAggregations,
382                        setKey: ['thread_slice', t],
383                      }),
384                      `thread_slice.${t}`)))),
385        this.renderQuery(attrs));
386  }
387}