• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2021 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 * as m from 'mithril';
16
17import {Actions} from '../common/actions';
18import {
19  ColumnAttrs,
20  PivotTableQueryResponse,
21  RowAttrs,
22} from '../common/pivot_table_common';
23
24import {globals} from './globals';
25import {Panel} from './panel';
26import {
27  PivotTableHelper,
28} from './pivot_table_helper';
29import {PopupMenuButton} from './popup_menu';
30
31interface ExpandableCellAttrs {
32  pivotTableId: string;
33  row: RowAttrs;
34  column: ColumnAttrs;
35  rowIndices: number[];
36  expandedRowColumns: string[];
37}
38
39interface PivotTableRowAttrs {
40  pivotTableId: string;
41  row: RowAttrs;
42  columns: ColumnAttrs[];
43  rowIndices: number[];
44  expandedRowColumns: string[];
45}
46
47interface PivotTableBodyAttrs {
48  pivotTableId: string;
49  rows: RowAttrs[];
50  columns: ColumnAttrs[];
51  rowIndices: number[];
52  expandedRowColumns: string[];
53}
54
55interface PivotTableHeaderAttrs {
56  helper: PivotTableHelper;
57}
58
59interface PivotTableAttrs {
60  pivotTableId: string;
61  helper?: PivotTableHelper;
62}
63
64class PivotTableHeader implements m.ClassComponent<PivotTableHeaderAttrs> {
65  view(vnode: m.Vnode<PivotTableHeaderAttrs>) {
66    const {helper} = vnode.attrs;
67    const pivotTableId = helper.pivotTableId;
68    const pivotTable = globals.state.pivotTable[pivotTableId];
69    const resp =
70        globals.queryResults.get(pivotTableId) as PivotTableQueryResponse;
71
72    const cols = [];
73    for (const column of resp.columns) {
74      const isPivot = column.aggregation === undefined;
75      const cellContents = [m('span', column.name)];
76      if (!isPivot) {
77        const items = [{
78          text: column.order === 'DESC' ? 'Sort \u25B2' : 'Sort \u25BC',
79          callback: () => {
80            if (!pivotTable.isLoadingQuery) {
81              helper.togglePivotTableAggregationSorting(column.index);
82              helper.queryPivotTableChanges();
83            }
84          }
85        }];
86
87        for (const aggregation of helper.availableAggregations) {
88          if (aggregation === column.aggregation) {
89            continue;
90          }
91
92          items.push({
93            text: aggregation,
94            callback: () => {
95              helper.changeAggregation(column.index, aggregation);
96              helper.queryPivotTableChanges();
97            }
98          });
99        }
100        cellContents.push(m(PopupMenuButton, {icon: 'arrow_drop_down', items}));
101        if (resp.totalAggregations !== undefined) {
102          cellContents.push(
103              m('.total-aggregation',
104                `(${resp.totalAggregations[column.name]})`));
105        }
106      }
107      cols.push(
108          m('td',
109            {
110              class: pivotTable.isLoadingQuery ? 'disabled' : '',
111              draggable: !pivotTable.isLoadingQuery,
112              ondragstart: (e: DragEvent) => {
113                helper.selectedColumnOnDrag(e, isPivot, column.index);
114              },
115              ondrop: (e: DragEvent) => {
116                helper.removeHighlightFromDropLocation(e);
117                helper.selectedColumnOnDrop(e, isPivot, column.index);
118                helper.queryPivotTableChanges();
119              },
120              ondragenter: (e: DragEvent) => {
121                helper.highlightDropLocation(e, isPivot);
122              },
123              ondragleave: (e: DragEvent) => {
124                helper.removeHighlightFromDropLocation(e);
125              }
126            },
127            cellContents));
128    }
129    return m('tr', cols);
130  }
131}
132
133class ExpandableCell implements m.ClassComponent<ExpandableCellAttrs> {
134  view(vnode: m.Vnode<ExpandableCellAttrs>) {
135    const {pivotTableId, row, column, rowIndices, expandedRowColumns} =
136        vnode.attrs;
137    const pivotTable = globals.state.pivotTable[pivotTableId];
138    let expandIcon = 'expand_more';
139    if (row.expandedRows.has(column.name)) {
140      expandIcon = row.expandedRows.get(column.name)!.isExpanded ?
141          'expand_less' :
142          'expand_more';
143    }
144    let spinnerVisibility = 'hidden';
145    let animationState = 'paused';
146    if (row.loadingColumn === column.name) {
147      spinnerVisibility = 'visible';
148      animationState = 'running';
149    }
150    const padValue = new Array(row.depth * 2).join(' ');
151
152    return m(
153        'td.allow-white-space',
154        padValue,
155        m('i.material-icons',
156          {
157            class: pivotTable.isLoadingQuery ? 'disabled' : '',
158            onclick: () => {
159              if (pivotTable.isLoadingQuery) {
160                return;
161              }
162              const value = row.row[column.name]?.toString();
163              if (value === undefined) {
164                throw Error('Expanded row has undefined value.');
165              }
166              if (row.expandedRows.has(column.name) &&
167                  row.expandedRows.get(column.name)!.isExpanded) {
168                globals.dispatch(Actions.setPivotTableRequest({
169                  pivotTableId,
170                  action: 'UNEXPAND',
171                  attrs: {
172                    rowIndices,
173                    columnIdx: column.index,
174                    value,
175                    expandedRowColumns
176                  }
177                }));
178              } else {
179                globals.dispatch(Actions.setPivotTableRequest({
180                  pivotTableId,
181                  action: column.isStackColumn ? 'DESCENDANTS' : 'EXPAND',
182                  attrs: {
183                    rowIndices,
184                    columnIdx: column.index,
185                    value,
186                    expandedRowColumns
187                  }
188                }));
189              }
190            },
191          },
192          expandIcon),
193        ' ',
194        row.row[column.name],
195        ' ',
196        // Adds a loading spinner while querying the expanded column.
197        m('.pivot-table-spinner', {
198          style: {
199            visibility: spinnerVisibility,
200            animationPlayState: animationState
201          }
202        }));
203  }
204}
205
206class PivotTableRow implements m.ClassComponent<PivotTableRowAttrs> {
207  view(vnode: m.Vnode<PivotTableRowAttrs>) {
208    const cells = [];
209    const {pivotTableId, row, columns, rowIndices, expandedRowColumns} =
210        vnode.attrs;
211
212    for (const column of columns) {
213      if (row.row[column.name] === undefined &&
214          row.expandableColumns.has(column.name)) {
215        throw Error(
216            `Row data at expandable column "${column.name}" is undefined.`);
217      }
218      if (row.row[column.name] === undefined || row.row[column.name] === null) {
219        cells.push(m('td', ''));
220        continue;
221      }
222      if (row.expandableColumns.has(column.name)) {
223        cells.push(
224            m(ExpandableCell,
225              {pivotTableId, row, column, rowIndices, expandedRowColumns}));
226        continue;
227      }
228
229      let value = row.row[column.name]!.toString();
230      if (column.aggregation === undefined) {
231        // For each indentation level add 2 spaces, if we have an expansion
232        // button add 3 spaces to cover the icon size.
233        let padding = 2 * row.depth;
234        if (row.depth > 0 && column.isStackColumn) {
235          padding += 3;
236        }
237        value = value.padStart(padding + value.length, ' ');
238      }
239      cells.push(m('td.allow-white-space', value));
240    }
241    return m('tr', cells);
242  }
243}
244
245class PivotTableBody implements m.ClassComponent<PivotTableBodyAttrs> {
246  view(vnode: m.Vnode<PivotTableBodyAttrs>): m.Children {
247    const pivotTableRows = [];
248    const {pivotTableId, rows, columns, rowIndices, expandedRowColumns} =
249        vnode.attrs;
250    for (let i = 0; i < rows.length; ++i) {
251      pivotTableRows.push(m(PivotTableRow, {
252        pivotTableId,
253        row: rows[i],
254        columns,
255        rowIndices: rowIndices.concat(i),
256        expandedRowColumns
257      }));
258      for (const column of columns.slice().reverse()) {
259        const expandedRows = rows[i].expandedRows.get(column.name);
260        if (expandedRows !== undefined && expandedRows.isExpanded) {
261          pivotTableRows.push(m(PivotTableBody, {
262            pivotTableId,
263            rows: expandedRows.rows,
264            columns,
265            rowIndices: rowIndices.concat(i),
266            expandedRowColumns: expandedRowColumns.concat(column.name)
267          }));
268        }
269      }
270    }
271    return pivotTableRows;
272  }
273}
274
275export class PivotTable extends Panel<PivotTableAttrs> {
276  view(vnode: m.CVnode<PivotTableAttrs>) {
277    const {pivotTableId, helper} = vnode.attrs;
278    const pivotTable = globals.state.pivotTable[pivotTableId];
279    const resp =
280        globals.queryResults.get(pivotTableId) as PivotTableQueryResponse;
281
282    let body;
283    let header;
284    if (helper !== undefined && resp !== undefined) {
285      header = m(PivotTableHeader, {helper});
286      body = m(PivotTableBody, {
287        pivotTableId,
288        rows: resp.rows,
289        columns: resp.columns,
290        rowIndices: [],
291        expandedRowColumns: []
292      });
293    }
294
295    const startSec = pivotTable.traceTime ? pivotTable.traceTime.startSec :
296                                            globals.state.traceTime.startSec;
297    const endSec = pivotTable.traceTime ? pivotTable.traceTime.endSec :
298                                          globals.state.traceTime.endSec;
299
300    return m(
301        'div.pivot-table-tab',
302        m(
303            'header.overview',
304            m('span',
305              m('button',
306                {
307                  disabled: helper === undefined || pivotTable.isLoadingQuery,
308                  onclick: () => {
309                    if (helper !== undefined) {
310                      helper.toggleEditPivotTableModal();
311                      globals.rafScheduler.scheduleFullRedraw();
312                    }
313                  }
314                },
315                'Edit'),
316              ' ',
317              (pivotTable.isLoadingQuery ? m('.pivot-table-spinner') : null),
318              (resp !== undefined && !pivotTable.isLoadingQuery ?
319                   m('span.code',
320                     `Query took ${Math.round(resp.durationMs)} ms -`) :
321                   null),
322              m('span.code', `Selected range: ${endSec - startSec} s`)),
323            m('button',
324              {
325                disabled: helper === undefined || pivotTable.isLoadingQuery,
326                onclick: () => {
327                  globals.frontendLocalState.togglePivotTable();
328                  globals.queryResults.delete(pivotTableId);
329                  globals.pivotTableHelper.delete(pivotTableId);
330                  globals.dispatch(Actions.deletePivotTable({pivotTableId}));
331                }
332              },
333              'Close'),
334            ),
335        m('.query-table-container',
336          m('table.query-table.pivot-table',
337            m('thead', header),
338            m('tbody', body))));
339  }
340
341  renderCanvas() {}
342}
343