1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2022 The Android Open Source Project 2*6dbdd20aSAndroid Build Coastguard Worker// 3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); 4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License. 5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at 6*6dbdd20aSAndroid Build Coastguard Worker// 7*6dbdd20aSAndroid Build Coastguard Worker// http://www.apache.org/licenses/LICENSE-2.0 8*6dbdd20aSAndroid Build Coastguard Worker// 9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, 11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and 13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License. 14*6dbdd20aSAndroid Build Coastguard Worker 15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril'; 16*6dbdd20aSAndroid Build Coastguard Workerimport {SortDirection} from '../base/comparison_utils'; 17*6dbdd20aSAndroid Build Coastguard Workerimport {sqliteString} from '../base/string_utils'; 18*6dbdd20aSAndroid Build Coastguard Workerimport {DropDirection} from '../core/pivot_table_manager'; 19*6dbdd20aSAndroid Build Coastguard Workerimport { 20*6dbdd20aSAndroid Build Coastguard Worker PivotTableResult, 21*6dbdd20aSAndroid Build Coastguard Worker Aggregation, 22*6dbdd20aSAndroid Build Coastguard Worker AggregationFunction, 23*6dbdd20aSAndroid Build Coastguard Worker columnKey, 24*6dbdd20aSAndroid Build Coastguard Worker PivotTree, 25*6dbdd20aSAndroid Build Coastguard Worker TableColumn, 26*6dbdd20aSAndroid Build Coastguard Worker COUNT_AGGREGATION, 27*6dbdd20aSAndroid Build Coastguard Worker} from '../core/pivot_table_types'; 28*6dbdd20aSAndroid Build Coastguard Workerimport {AreaSelection} from '../public/selection'; 29*6dbdd20aSAndroid Build Coastguard Workerimport {raf} from '../core/raf_scheduler'; 30*6dbdd20aSAndroid Build Coastguard Workerimport {ColumnType} from '../trace_processor/query_result'; 31*6dbdd20aSAndroid Build Coastguard Workerimport { 32*6dbdd20aSAndroid Build Coastguard Worker aggregationIndex, 33*6dbdd20aSAndroid Build Coastguard Worker areaFilters, 34*6dbdd20aSAndroid Build Coastguard Worker sliceAggregationColumns, 35*6dbdd20aSAndroid Build Coastguard Worker tables, 36*6dbdd20aSAndroid Build Coastguard Worker} from '../core/pivot_table_query_generator'; 37*6dbdd20aSAndroid Build Coastguard Workerimport {ReorderableCell, ReorderableCellGroup} from './reorderable_cells'; 38*6dbdd20aSAndroid Build Coastguard Workerimport {AttributeModalHolder} from './tables/attribute_modal_holder'; 39*6dbdd20aSAndroid Build Coastguard Workerimport {DurationWidget} from '../components/widgets/duration'; 40*6dbdd20aSAndroid Build Coastguard Workerimport {getSqlTableDescription} from '../components/widgets/sql/table/sql_table_registry'; 41*6dbdd20aSAndroid Build Coastguard Workerimport {assertExists, assertFalse} from '../base/logging'; 42*6dbdd20aSAndroid Build Coastguard Workerimport {Filter, SqlColumn} from '../components/widgets/sql/table/column'; 43*6dbdd20aSAndroid Build Coastguard Workerimport {argSqlColumn} from '../components/widgets/sql/table/well_known_columns'; 44*6dbdd20aSAndroid Build Coastguard Workerimport {TraceImpl} from '../core/trace_impl'; 45*6dbdd20aSAndroid Build Coastguard Workerimport {PivotTableManager} from '../core/pivot_table_manager'; 46*6dbdd20aSAndroid Build Coastguard Workerimport {extensions} from '../components/extensions'; 47*6dbdd20aSAndroid Build Coastguard Workerimport {MenuItem, PopupMenu2} from '../widgets/menu'; 48*6dbdd20aSAndroid Build Coastguard Workerimport {Button} from '../widgets/button'; 49*6dbdd20aSAndroid Build Coastguard Workerimport {popupMenuIcon} from '../widgets/table'; 50*6dbdd20aSAndroid Build Coastguard Worker 51*6dbdd20aSAndroid Build Coastguard Workerinterface PathItem { 52*6dbdd20aSAndroid Build Coastguard Worker tree: PivotTree; 53*6dbdd20aSAndroid Build Coastguard Worker nextKey: ColumnType; 54*6dbdd20aSAndroid Build Coastguard Worker} 55*6dbdd20aSAndroid Build Coastguard Worker 56*6dbdd20aSAndroid Build Coastguard Workerinterface PivotTableAttrs { 57*6dbdd20aSAndroid Build Coastguard Worker trace: TraceImpl; 58*6dbdd20aSAndroid Build Coastguard Worker selectionArea: AreaSelection; 59*6dbdd20aSAndroid Build Coastguard Worker} 60*6dbdd20aSAndroid Build Coastguard Worker 61*6dbdd20aSAndroid Build Coastguard Workerinterface DrillFilter { 62*6dbdd20aSAndroid Build Coastguard Worker column: TableColumn; 63*6dbdd20aSAndroid Build Coastguard Worker value: ColumnType; 64*6dbdd20aSAndroid Build Coastguard Worker} 65*6dbdd20aSAndroid Build Coastguard Worker 66*6dbdd20aSAndroid Build Coastguard Workerfunction drillFilterColumnName(column: TableColumn): SqlColumn { 67*6dbdd20aSAndroid Build Coastguard Worker switch (column.kind) { 68*6dbdd20aSAndroid Build Coastguard Worker case 'argument': 69*6dbdd20aSAndroid Build Coastguard Worker return argSqlColumn('arg_set_id', column.argument); 70*6dbdd20aSAndroid Build Coastguard Worker case 'regular': 71*6dbdd20aSAndroid Build Coastguard Worker return `${column.column}`; 72*6dbdd20aSAndroid Build Coastguard Worker } 73*6dbdd20aSAndroid Build Coastguard Worker} 74*6dbdd20aSAndroid Build Coastguard Worker 75*6dbdd20aSAndroid Build Coastguard Worker// Convert DrillFilter to SQL condition to be used in WHERE clause. 76*6dbdd20aSAndroid Build Coastguard Workerfunction renderDrillFilter(filter: DrillFilter): Filter { 77*6dbdd20aSAndroid Build Coastguard Worker const column = drillFilterColumnName(filter.column); 78*6dbdd20aSAndroid Build Coastguard Worker const value = filter.value; 79*6dbdd20aSAndroid Build Coastguard Worker if (value === null) { 80*6dbdd20aSAndroid Build Coastguard Worker return {op: (cols) => `${cols[0]} IS NULL`, columns: [column]}; 81*6dbdd20aSAndroid Build Coastguard Worker } else if (typeof value === 'number' || typeof value === 'bigint') { 82*6dbdd20aSAndroid Build Coastguard Worker return {op: (cols) => `${cols[0]} = ${filter.value}`, columns: [column]}; 83*6dbdd20aSAndroid Build Coastguard Worker } else if (value instanceof Uint8Array) { 84*6dbdd20aSAndroid Build Coastguard Worker throw new Error(`BLOB as DrillFilter not implemented`); 85*6dbdd20aSAndroid Build Coastguard Worker } 86*6dbdd20aSAndroid Build Coastguard Worker return { 87*6dbdd20aSAndroid Build Coastguard Worker op: (cols) => `${cols[0]} = ${sqliteString(value)}`, 88*6dbdd20aSAndroid Build Coastguard Worker columns: [column], 89*6dbdd20aSAndroid Build Coastguard Worker }; 90*6dbdd20aSAndroid Build Coastguard Worker} 91*6dbdd20aSAndroid Build Coastguard Worker 92*6dbdd20aSAndroid Build Coastguard Workerfunction readableColumnName(column: TableColumn) { 93*6dbdd20aSAndroid Build Coastguard Worker switch (column.kind) { 94*6dbdd20aSAndroid Build Coastguard Worker case 'argument': 95*6dbdd20aSAndroid Build Coastguard Worker return `Argument ${column.argument}`; 96*6dbdd20aSAndroid Build Coastguard Worker case 'regular': 97*6dbdd20aSAndroid Build Coastguard Worker return `${column.column}`; 98*6dbdd20aSAndroid Build Coastguard Worker } 99*6dbdd20aSAndroid Build Coastguard Worker} 100*6dbdd20aSAndroid Build Coastguard Worker 101*6dbdd20aSAndroid Build Coastguard Workerexport function markFirst(index: number) { 102*6dbdd20aSAndroid Build Coastguard Worker if (index === 0) { 103*6dbdd20aSAndroid Build Coastguard Worker return '.first'; 104*6dbdd20aSAndroid Build Coastguard Worker } 105*6dbdd20aSAndroid Build Coastguard Worker return ''; 106*6dbdd20aSAndroid Build Coastguard Worker} 107*6dbdd20aSAndroid Build Coastguard Worker 108*6dbdd20aSAndroid Build Coastguard Workerexport class PivotTable implements m.ClassComponent<PivotTableAttrs> { 109*6dbdd20aSAndroid Build Coastguard Worker private pivotMgr: PivotTableManager; 110*6dbdd20aSAndroid Build Coastguard Worker 111*6dbdd20aSAndroid Build Coastguard Worker constructor({attrs}: m.CVnode<PivotTableAttrs>) { 112*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr = attrs.trace.pivotTable; 113*6dbdd20aSAndroid Build Coastguard Worker this.attributeModalHolder = new AttributeModalHolder((arg) => 114*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr.setPivotSelected({ 115*6dbdd20aSAndroid Build Coastguard Worker column: {kind: 'argument', argument: arg}, 116*6dbdd20aSAndroid Build Coastguard Worker selected: true, 117*6dbdd20aSAndroid Build Coastguard Worker }), 118*6dbdd20aSAndroid Build Coastguard Worker ); 119*6dbdd20aSAndroid Build Coastguard Worker } 120*6dbdd20aSAndroid Build Coastguard Worker 121*6dbdd20aSAndroid Build Coastguard Worker get pivotState() { 122*6dbdd20aSAndroid Build Coastguard Worker return this.pivotMgr.state; 123*6dbdd20aSAndroid Build Coastguard Worker } 124*6dbdd20aSAndroid Build Coastguard Worker 125*6dbdd20aSAndroid Build Coastguard Worker renderDrillDownCell(attrs: PivotTableAttrs, filters: DrillFilter[]) { 126*6dbdd20aSAndroid Build Coastguard Worker return m( 127*6dbdd20aSAndroid Build Coastguard Worker 'td', 128*6dbdd20aSAndroid Build Coastguard Worker m( 129*6dbdd20aSAndroid Build Coastguard Worker 'button', 130*6dbdd20aSAndroid Build Coastguard Worker { 131*6dbdd20aSAndroid Build Coastguard Worker title: 'All corresponding slices', 132*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 133*6dbdd20aSAndroid Build Coastguard Worker const queryFilters = filters.map(renderDrillFilter); 134*6dbdd20aSAndroid Build Coastguard Worker if (this.pivotState.constrainToArea) { 135*6dbdd20aSAndroid Build Coastguard Worker queryFilters.push(...areaFilters(attrs.selectionArea)); 136*6dbdd20aSAndroid Build Coastguard Worker } 137*6dbdd20aSAndroid Build Coastguard Worker extensions.addSqlTableTab(attrs.trace, { 138*6dbdd20aSAndroid Build Coastguard Worker table: assertExists(getSqlTableDescription('slice')), 139*6dbdd20aSAndroid Build Coastguard Worker // TODO(altimin): this should properly reference the required columns, but it works for now (until the pivot table is going to be rewritten to be more flexible). 140*6dbdd20aSAndroid Build Coastguard Worker filters: queryFilters, 141*6dbdd20aSAndroid Build Coastguard Worker }); 142*6dbdd20aSAndroid Build Coastguard Worker }, 143*6dbdd20aSAndroid Build Coastguard Worker }, 144*6dbdd20aSAndroid Build Coastguard Worker m('i.material-icons', 'arrow_right'), 145*6dbdd20aSAndroid Build Coastguard Worker ), 146*6dbdd20aSAndroid Build Coastguard Worker ); 147*6dbdd20aSAndroid Build Coastguard Worker } 148*6dbdd20aSAndroid Build Coastguard Worker 149*6dbdd20aSAndroid Build Coastguard Worker renderSectionRow( 150*6dbdd20aSAndroid Build Coastguard Worker attrs: PivotTableAttrs, 151*6dbdd20aSAndroid Build Coastguard Worker path: PathItem[], 152*6dbdd20aSAndroid Build Coastguard Worker tree: PivotTree, 153*6dbdd20aSAndroid Build Coastguard Worker result: PivotTableResult, 154*6dbdd20aSAndroid Build Coastguard Worker ): m.Vnode { 155*6dbdd20aSAndroid Build Coastguard Worker const renderedCells = []; 156*6dbdd20aSAndroid Build Coastguard Worker for (let j = 0; j + 1 < path.length; j++) { 157*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(m('td', m('span.indent', ' '), `${path[j].nextKey}`)); 158*6dbdd20aSAndroid Build Coastguard Worker } 159*6dbdd20aSAndroid Build Coastguard Worker 160*6dbdd20aSAndroid Build Coastguard Worker const treeDepth = result.metadata.pivotColumns.length; 161*6dbdd20aSAndroid Build Coastguard Worker const colspan = treeDepth - path.length + 1; 162*6dbdd20aSAndroid Build Coastguard Worker const button = m( 163*6dbdd20aSAndroid Build Coastguard Worker 'button', 164*6dbdd20aSAndroid Build Coastguard Worker { 165*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 166*6dbdd20aSAndroid Build Coastguard Worker tree.isCollapsed = !tree.isCollapsed; 167*6dbdd20aSAndroid Build Coastguard Worker raf.scheduleFullRedraw(); 168*6dbdd20aSAndroid Build Coastguard Worker }, 169*6dbdd20aSAndroid Build Coastguard Worker }, 170*6dbdd20aSAndroid Build Coastguard Worker m('i.material-icons', tree.isCollapsed ? 'expand_more' : 'expand_less'), 171*6dbdd20aSAndroid Build Coastguard Worker ); 172*6dbdd20aSAndroid Build Coastguard Worker 173*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push( 174*6dbdd20aSAndroid Build Coastguard Worker m('td', {colspan}, button, `${path[path.length - 1].nextKey}`), 175*6dbdd20aSAndroid Build Coastguard Worker ); 176*6dbdd20aSAndroid Build Coastguard Worker 177*6dbdd20aSAndroid Build Coastguard Worker for (let i = 0; i < result.metadata.aggregationColumns.length; i++) { 178*6dbdd20aSAndroid Build Coastguard Worker const renderedValue = this.renderCell( 179*6dbdd20aSAndroid Build Coastguard Worker result.metadata.aggregationColumns[i].column, 180*6dbdd20aSAndroid Build Coastguard Worker tree.aggregates[i], 181*6dbdd20aSAndroid Build Coastguard Worker ); 182*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(m('td' + markFirst(i), renderedValue)); 183*6dbdd20aSAndroid Build Coastguard Worker } 184*6dbdd20aSAndroid Build Coastguard Worker 185*6dbdd20aSAndroid Build Coastguard Worker const drillFilters: DrillFilter[] = []; 186*6dbdd20aSAndroid Build Coastguard Worker for (let i = 0; i < path.length; i++) { 187*6dbdd20aSAndroid Build Coastguard Worker drillFilters.push({ 188*6dbdd20aSAndroid Build Coastguard Worker value: `${path[i].nextKey}`, 189*6dbdd20aSAndroid Build Coastguard Worker column: result.metadata.pivotColumns[i], 190*6dbdd20aSAndroid Build Coastguard Worker }); 191*6dbdd20aSAndroid Build Coastguard Worker } 192*6dbdd20aSAndroid Build Coastguard Worker 193*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(this.renderDrillDownCell(attrs, drillFilters)); 194*6dbdd20aSAndroid Build Coastguard Worker return m('tr', renderedCells); 195*6dbdd20aSAndroid Build Coastguard Worker } 196*6dbdd20aSAndroid Build Coastguard Worker 197*6dbdd20aSAndroid Build Coastguard Worker renderCell(column: TableColumn, value: ColumnType): m.Children { 198*6dbdd20aSAndroid Build Coastguard Worker if ( 199*6dbdd20aSAndroid Build Coastguard Worker column.kind === 'regular' && 200*6dbdd20aSAndroid Build Coastguard Worker (column.column === 'dur' || column.column === 'thread_dur') 201*6dbdd20aSAndroid Build Coastguard Worker ) { 202*6dbdd20aSAndroid Build Coastguard Worker if (typeof value === 'bigint') { 203*6dbdd20aSAndroid Build Coastguard Worker return m(DurationWidget, {dur: value}); 204*6dbdd20aSAndroid Build Coastguard Worker } else if (typeof value === 'number') { 205*6dbdd20aSAndroid Build Coastguard Worker return m(DurationWidget, {dur: BigInt(Math.round(value))}); 206*6dbdd20aSAndroid Build Coastguard Worker } 207*6dbdd20aSAndroid Build Coastguard Worker } 208*6dbdd20aSAndroid Build Coastguard Worker return `${value}`; 209*6dbdd20aSAndroid Build Coastguard Worker } 210*6dbdd20aSAndroid Build Coastguard Worker 211*6dbdd20aSAndroid Build Coastguard Worker renderTree( 212*6dbdd20aSAndroid Build Coastguard Worker attrs: PivotTableAttrs, 213*6dbdd20aSAndroid Build Coastguard Worker path: PathItem[], 214*6dbdd20aSAndroid Build Coastguard Worker tree: PivotTree, 215*6dbdd20aSAndroid Build Coastguard Worker result: PivotTableResult, 216*6dbdd20aSAndroid Build Coastguard Worker sink: m.Vnode[], 217*6dbdd20aSAndroid Build Coastguard Worker ) { 218*6dbdd20aSAndroid Build Coastguard Worker if (tree.isCollapsed) { 219*6dbdd20aSAndroid Build Coastguard Worker sink.push(this.renderSectionRow(attrs, path, tree, result)); 220*6dbdd20aSAndroid Build Coastguard Worker return; 221*6dbdd20aSAndroid Build Coastguard Worker } 222*6dbdd20aSAndroid Build Coastguard Worker if (tree.children.size > 0) { 223*6dbdd20aSAndroid Build Coastguard Worker // Avoid rendering the intermediate results row for the root of tree 224*6dbdd20aSAndroid Build Coastguard Worker // and in case there's only one child subtree. 225*6dbdd20aSAndroid Build Coastguard Worker if (!tree.isCollapsed && path.length > 0 && tree.children.size !== 1) { 226*6dbdd20aSAndroid Build Coastguard Worker sink.push(this.renderSectionRow(attrs, path, tree, result)); 227*6dbdd20aSAndroid Build Coastguard Worker } 228*6dbdd20aSAndroid Build Coastguard Worker for (const [key, childTree] of tree.children.entries()) { 229*6dbdd20aSAndroid Build Coastguard Worker path.push({tree: childTree, nextKey: key}); 230*6dbdd20aSAndroid Build Coastguard Worker this.renderTree(attrs, path, childTree, result, sink); 231*6dbdd20aSAndroid Build Coastguard Worker path.pop(); 232*6dbdd20aSAndroid Build Coastguard Worker } 233*6dbdd20aSAndroid Build Coastguard Worker return; 234*6dbdd20aSAndroid Build Coastguard Worker } 235*6dbdd20aSAndroid Build Coastguard Worker 236*6dbdd20aSAndroid Build Coastguard Worker // Avoid rendering the intermediate results row if it has only one leaf 237*6dbdd20aSAndroid Build Coastguard Worker // row. 238*6dbdd20aSAndroid Build Coastguard Worker if (!tree.isCollapsed && path.length > 0 && tree.rows.length > 1) { 239*6dbdd20aSAndroid Build Coastguard Worker sink.push(this.renderSectionRow(attrs, path, tree, result)); 240*6dbdd20aSAndroid Build Coastguard Worker } 241*6dbdd20aSAndroid Build Coastguard Worker for (const row of tree.rows) { 242*6dbdd20aSAndroid Build Coastguard Worker const renderedCells = []; 243*6dbdd20aSAndroid Build Coastguard Worker const drillFilters: DrillFilter[] = []; 244*6dbdd20aSAndroid Build Coastguard Worker const treeDepth = result.metadata.pivotColumns.length; 245*6dbdd20aSAndroid Build Coastguard Worker for (let j = 0; j < treeDepth; j++) { 246*6dbdd20aSAndroid Build Coastguard Worker const value = this.renderCell(result.metadata.pivotColumns[j], row[j]); 247*6dbdd20aSAndroid Build Coastguard Worker if (j < path.length) { 248*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(m('td', m('span.indent', ' '), value)); 249*6dbdd20aSAndroid Build Coastguard Worker } else { 250*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(m(`td`, value)); 251*6dbdd20aSAndroid Build Coastguard Worker } 252*6dbdd20aSAndroid Build Coastguard Worker drillFilters.push({ 253*6dbdd20aSAndroid Build Coastguard Worker column: result.metadata.pivotColumns[j], 254*6dbdd20aSAndroid Build Coastguard Worker value: row[j], 255*6dbdd20aSAndroid Build Coastguard Worker }); 256*6dbdd20aSAndroid Build Coastguard Worker } 257*6dbdd20aSAndroid Build Coastguard Worker for (let j = 0; j < result.metadata.aggregationColumns.length; j++) { 258*6dbdd20aSAndroid Build Coastguard Worker const value = row[aggregationIndex(treeDepth, j)]; 259*6dbdd20aSAndroid Build Coastguard Worker const renderedValue = this.renderCell( 260*6dbdd20aSAndroid Build Coastguard Worker result.metadata.aggregationColumns[j].column, 261*6dbdd20aSAndroid Build Coastguard Worker value, 262*6dbdd20aSAndroid Build Coastguard Worker ); 263*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(m('td.aggregation' + markFirst(j), renderedValue)); 264*6dbdd20aSAndroid Build Coastguard Worker } 265*6dbdd20aSAndroid Build Coastguard Worker 266*6dbdd20aSAndroid Build Coastguard Worker renderedCells.push(this.renderDrillDownCell(attrs, drillFilters)); 267*6dbdd20aSAndroid Build Coastguard Worker sink.push(m('tr', renderedCells)); 268*6dbdd20aSAndroid Build Coastguard Worker } 269*6dbdd20aSAndroid Build Coastguard Worker } 270*6dbdd20aSAndroid Build Coastguard Worker 271*6dbdd20aSAndroid Build Coastguard Worker renderTotalsRow(queryResult: PivotTableResult) { 272*6dbdd20aSAndroid Build Coastguard Worker const overallValuesRow = [ 273*6dbdd20aSAndroid Build Coastguard Worker m( 274*6dbdd20aSAndroid Build Coastguard Worker 'td.total-values', 275*6dbdd20aSAndroid Build Coastguard Worker {colspan: queryResult.metadata.pivotColumns.length}, 276*6dbdd20aSAndroid Build Coastguard Worker m('strong', 'Total values:'), 277*6dbdd20aSAndroid Build Coastguard Worker ), 278*6dbdd20aSAndroid Build Coastguard Worker ]; 279*6dbdd20aSAndroid Build Coastguard Worker for (let i = 0; i < queryResult.metadata.aggregationColumns.length; i++) { 280*6dbdd20aSAndroid Build Coastguard Worker overallValuesRow.push( 281*6dbdd20aSAndroid Build Coastguard Worker m( 282*6dbdd20aSAndroid Build Coastguard Worker 'td' + markFirst(i), 283*6dbdd20aSAndroid Build Coastguard Worker this.renderCell( 284*6dbdd20aSAndroid Build Coastguard Worker queryResult.metadata.aggregationColumns[i].column, 285*6dbdd20aSAndroid Build Coastguard Worker queryResult.tree.aggregates[i], 286*6dbdd20aSAndroid Build Coastguard Worker ), 287*6dbdd20aSAndroid Build Coastguard Worker ), 288*6dbdd20aSAndroid Build Coastguard Worker ); 289*6dbdd20aSAndroid Build Coastguard Worker } 290*6dbdd20aSAndroid Build Coastguard Worker overallValuesRow.push(m('td')); 291*6dbdd20aSAndroid Build Coastguard Worker return m('tr', overallValuesRow); 292*6dbdd20aSAndroid Build Coastguard Worker } 293*6dbdd20aSAndroid Build Coastguard Worker 294*6dbdd20aSAndroid Build Coastguard Worker sortingItem(aggregationIndex: number, order: SortDirection): m.Child { 295*6dbdd20aSAndroid Build Coastguard Worker const pivotMgr = this.pivotMgr; 296*6dbdd20aSAndroid Build Coastguard Worker return m(MenuItem, { 297*6dbdd20aSAndroid Build Coastguard Worker label: order === 'DESC' ? 'Highest first' : 'Lowest first', 298*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 299*6dbdd20aSAndroid Build Coastguard Worker pivotMgr.setSortColumn(aggregationIndex, order); 300*6dbdd20aSAndroid Build Coastguard Worker }, 301*6dbdd20aSAndroid Build Coastguard Worker }); 302*6dbdd20aSAndroid Build Coastguard Worker } 303*6dbdd20aSAndroid Build Coastguard Worker 304*6dbdd20aSAndroid Build Coastguard Worker readableAggregationName(aggregation: Aggregation) { 305*6dbdd20aSAndroid Build Coastguard Worker if (aggregation.aggregationFunction === 'COUNT') { 306*6dbdd20aSAndroid Build Coastguard Worker return 'Count'; 307*6dbdd20aSAndroid Build Coastguard Worker } 308*6dbdd20aSAndroid Build Coastguard Worker return `${aggregation.aggregationFunction}(${readableColumnName( 309*6dbdd20aSAndroid Build Coastguard Worker aggregation.column, 310*6dbdd20aSAndroid Build Coastguard Worker )})`; 311*6dbdd20aSAndroid Build Coastguard Worker } 312*6dbdd20aSAndroid Build Coastguard Worker 313*6dbdd20aSAndroid Build Coastguard Worker aggregationPopupItem( 314*6dbdd20aSAndroid Build Coastguard Worker aggregation: Aggregation, 315*6dbdd20aSAndroid Build Coastguard Worker index: number, 316*6dbdd20aSAndroid Build Coastguard Worker nameOverride?: string, 317*6dbdd20aSAndroid Build Coastguard Worker ): m.Child { 318*6dbdd20aSAndroid Build Coastguard Worker return m(MenuItem, { 319*6dbdd20aSAndroid Build Coastguard Worker label: nameOverride ?? readableColumnName(aggregation.column), 320*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 321*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr.addAggregation(aggregation, index); 322*6dbdd20aSAndroid Build Coastguard Worker }, 323*6dbdd20aSAndroid Build Coastguard Worker }); 324*6dbdd20aSAndroid Build Coastguard Worker } 325*6dbdd20aSAndroid Build Coastguard Worker 326*6dbdd20aSAndroid Build Coastguard Worker aggregationPopupTableGroup( 327*6dbdd20aSAndroid Build Coastguard Worker table: string, 328*6dbdd20aSAndroid Build Coastguard Worker columns: string[], 329*6dbdd20aSAndroid Build Coastguard Worker index: number, 330*6dbdd20aSAndroid Build Coastguard Worker ): m.Child | undefined { 331*6dbdd20aSAndroid Build Coastguard Worker const items: m.Child[] = []; 332*6dbdd20aSAndroid Build Coastguard Worker for (const column of columns) { 333*6dbdd20aSAndroid Build Coastguard Worker const tableColumn: TableColumn = {kind: 'regular', table, column}; 334*6dbdd20aSAndroid Build Coastguard Worker items.push( 335*6dbdd20aSAndroid Build Coastguard Worker this.aggregationPopupItem( 336*6dbdd20aSAndroid Build Coastguard Worker {aggregationFunction: 'SUM', column: tableColumn}, 337*6dbdd20aSAndroid Build Coastguard Worker index, 338*6dbdd20aSAndroid Build Coastguard Worker ), 339*6dbdd20aSAndroid Build Coastguard Worker ); 340*6dbdd20aSAndroid Build Coastguard Worker } 341*6dbdd20aSAndroid Build Coastguard Worker 342*6dbdd20aSAndroid Build Coastguard Worker if (items.length === 0) { 343*6dbdd20aSAndroid Build Coastguard Worker return undefined; 344*6dbdd20aSAndroid Build Coastguard Worker } 345*6dbdd20aSAndroid Build Coastguard Worker 346*6dbdd20aSAndroid Build Coastguard Worker return m(MenuItem, {label: `Add ${table} aggregation`}, items); 347*6dbdd20aSAndroid Build Coastguard Worker } 348*6dbdd20aSAndroid Build Coastguard Worker 349*6dbdd20aSAndroid Build Coastguard Worker renderAggregationHeaderCell( 350*6dbdd20aSAndroid Build Coastguard Worker aggregation: Aggregation, 351*6dbdd20aSAndroid Build Coastguard Worker index: number, 352*6dbdd20aSAndroid Build Coastguard Worker removeItem: boolean, 353*6dbdd20aSAndroid Build Coastguard Worker ): ReorderableCell { 354*6dbdd20aSAndroid Build Coastguard Worker const popupItems: m.Child[] = []; 355*6dbdd20aSAndroid Build Coastguard Worker if (aggregation.sortDirection === undefined) { 356*6dbdd20aSAndroid Build Coastguard Worker popupItems.push( 357*6dbdd20aSAndroid Build Coastguard Worker this.sortingItem(index, 'DESC'), 358*6dbdd20aSAndroid Build Coastguard Worker this.sortingItem(index, 'ASC'), 359*6dbdd20aSAndroid Build Coastguard Worker ); 360*6dbdd20aSAndroid Build Coastguard Worker } else { 361*6dbdd20aSAndroid Build Coastguard Worker // Table is already sorted by the same column, return one item with 362*6dbdd20aSAndroid Build Coastguard Worker // opposite direction. 363*6dbdd20aSAndroid Build Coastguard Worker popupItems.push( 364*6dbdd20aSAndroid Build Coastguard Worker this.sortingItem( 365*6dbdd20aSAndroid Build Coastguard Worker index, 366*6dbdd20aSAndroid Build Coastguard Worker aggregation.sortDirection === 'DESC' ? 'ASC' : 'DESC', 367*6dbdd20aSAndroid Build Coastguard Worker ), 368*6dbdd20aSAndroid Build Coastguard Worker ); 369*6dbdd20aSAndroid Build Coastguard Worker } 370*6dbdd20aSAndroid Build Coastguard Worker const otherAggs: AggregationFunction[] = ['SUM', 'MAX', 'MIN', 'AVG']; 371*6dbdd20aSAndroid Build Coastguard Worker if (aggregation.aggregationFunction !== 'COUNT') { 372*6dbdd20aSAndroid Build Coastguard Worker for (const otherAgg of otherAggs) { 373*6dbdd20aSAndroid Build Coastguard Worker if (aggregation.aggregationFunction === otherAgg) { 374*6dbdd20aSAndroid Build Coastguard Worker continue; 375*6dbdd20aSAndroid Build Coastguard Worker } 376*6dbdd20aSAndroid Build Coastguard Worker const pivotMgr = this.pivotMgr; 377*6dbdd20aSAndroid Build Coastguard Worker popupItems.push( 378*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 379*6dbdd20aSAndroid Build Coastguard Worker label: otherAgg, 380*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 381*6dbdd20aSAndroid Build Coastguard Worker pivotMgr.setAggregationFunction(index, otherAgg); 382*6dbdd20aSAndroid Build Coastguard Worker }, 383*6dbdd20aSAndroid Build Coastguard Worker }), 384*6dbdd20aSAndroid Build Coastguard Worker ); 385*6dbdd20aSAndroid Build Coastguard Worker } 386*6dbdd20aSAndroid Build Coastguard Worker } 387*6dbdd20aSAndroid Build Coastguard Worker 388*6dbdd20aSAndroid Build Coastguard Worker if (removeItem) { 389*6dbdd20aSAndroid Build Coastguard Worker popupItems.push( 390*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 391*6dbdd20aSAndroid Build Coastguard Worker label: 'Remove', 392*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 393*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr.removeAggregation(index); 394*6dbdd20aSAndroid Build Coastguard Worker }, 395*6dbdd20aSAndroid Build Coastguard Worker }), 396*6dbdd20aSAndroid Build Coastguard Worker ); 397*6dbdd20aSAndroid Build Coastguard Worker } 398*6dbdd20aSAndroid Build Coastguard Worker 399*6dbdd20aSAndroid Build Coastguard Worker let hasCount = false; 400*6dbdd20aSAndroid Build Coastguard Worker for (const agg of this.pivotState.selectedAggregations.values()) { 401*6dbdd20aSAndroid Build Coastguard Worker if (agg.aggregationFunction === 'COUNT') { 402*6dbdd20aSAndroid Build Coastguard Worker hasCount = true; 403*6dbdd20aSAndroid Build Coastguard Worker } 404*6dbdd20aSAndroid Build Coastguard Worker } 405*6dbdd20aSAndroid Build Coastguard Worker 406*6dbdd20aSAndroid Build Coastguard Worker if (!hasCount) { 407*6dbdd20aSAndroid Build Coastguard Worker popupItems.push( 408*6dbdd20aSAndroid Build Coastguard Worker this.aggregationPopupItem( 409*6dbdd20aSAndroid Build Coastguard Worker COUNT_AGGREGATION, 410*6dbdd20aSAndroid Build Coastguard Worker index, 411*6dbdd20aSAndroid Build Coastguard Worker 'Add count aggregation', 412*6dbdd20aSAndroid Build Coastguard Worker ), 413*6dbdd20aSAndroid Build Coastguard Worker ); 414*6dbdd20aSAndroid Build Coastguard Worker } 415*6dbdd20aSAndroid Build Coastguard Worker 416*6dbdd20aSAndroid Build Coastguard Worker const sliceAggregationsItem = this.aggregationPopupTableGroup( 417*6dbdd20aSAndroid Build Coastguard Worker assertExists(getSqlTableDescription('slice')).name, 418*6dbdd20aSAndroid Build Coastguard Worker sliceAggregationColumns, 419*6dbdd20aSAndroid Build Coastguard Worker index, 420*6dbdd20aSAndroid Build Coastguard Worker ); 421*6dbdd20aSAndroid Build Coastguard Worker if (sliceAggregationsItem !== undefined) { 422*6dbdd20aSAndroid Build Coastguard Worker popupItems.push(sliceAggregationsItem); 423*6dbdd20aSAndroid Build Coastguard Worker } 424*6dbdd20aSAndroid Build Coastguard Worker 425*6dbdd20aSAndroid Build Coastguard Worker return { 426*6dbdd20aSAndroid Build Coastguard Worker extraClass: '.aggregation' + markFirst(index), 427*6dbdd20aSAndroid Build Coastguard Worker content: [ 428*6dbdd20aSAndroid Build Coastguard Worker this.readableAggregationName(aggregation), 429*6dbdd20aSAndroid Build Coastguard Worker m( 430*6dbdd20aSAndroid Build Coastguard Worker PopupMenu2, 431*6dbdd20aSAndroid Build Coastguard Worker { 432*6dbdd20aSAndroid Build Coastguard Worker trigger: m(Button, { 433*6dbdd20aSAndroid Build Coastguard Worker icon: popupMenuIcon(aggregation.sortDirection), 434*6dbdd20aSAndroid Build Coastguard Worker }), 435*6dbdd20aSAndroid Build Coastguard Worker }, 436*6dbdd20aSAndroid Build Coastguard Worker popupItems, 437*6dbdd20aSAndroid Build Coastguard Worker ), 438*6dbdd20aSAndroid Build Coastguard Worker ], 439*6dbdd20aSAndroid Build Coastguard Worker }; 440*6dbdd20aSAndroid Build Coastguard Worker } 441*6dbdd20aSAndroid Build Coastguard Worker 442*6dbdd20aSAndroid Build Coastguard Worker attributeModalHolder: AttributeModalHolder; 443*6dbdd20aSAndroid Build Coastguard Worker 444*6dbdd20aSAndroid Build Coastguard Worker renderPivotColumnHeader( 445*6dbdd20aSAndroid Build Coastguard Worker queryResult: PivotTableResult, 446*6dbdd20aSAndroid Build Coastguard Worker pivot: TableColumn, 447*6dbdd20aSAndroid Build Coastguard Worker selectedPivots: Set<string>, 448*6dbdd20aSAndroid Build Coastguard Worker ): ReorderableCell { 449*6dbdd20aSAndroid Build Coastguard Worker const pivotMgr = this.pivotMgr; 450*6dbdd20aSAndroid Build Coastguard Worker const items: m.Child[] = [ 451*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 452*6dbdd20aSAndroid Build Coastguard Worker label: 'Add argument pivot', 453*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 454*6dbdd20aSAndroid Build Coastguard Worker this.attributeModalHolder.start(); 455*6dbdd20aSAndroid Build Coastguard Worker }, 456*6dbdd20aSAndroid Build Coastguard Worker }), 457*6dbdd20aSAndroid Build Coastguard Worker ]; 458*6dbdd20aSAndroid Build Coastguard Worker if (queryResult.metadata.pivotColumns.length > 1) { 459*6dbdd20aSAndroid Build Coastguard Worker items.push( 460*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 461*6dbdd20aSAndroid Build Coastguard Worker label: 'Remove', 462*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 463*6dbdd20aSAndroid Build Coastguard Worker pivotMgr.setPivotSelected({column: pivot, selected: false}); 464*6dbdd20aSAndroid Build Coastguard Worker }, 465*6dbdd20aSAndroid Build Coastguard Worker }), 466*6dbdd20aSAndroid Build Coastguard Worker ); 467*6dbdd20aSAndroid Build Coastguard Worker } 468*6dbdd20aSAndroid Build Coastguard Worker 469*6dbdd20aSAndroid Build Coastguard Worker for (const table of tables) { 470*6dbdd20aSAndroid Build Coastguard Worker const group: m.Child[] = []; 471*6dbdd20aSAndroid Build Coastguard Worker for (const columnName of table.columns) { 472*6dbdd20aSAndroid Build Coastguard Worker const column: TableColumn = { 473*6dbdd20aSAndroid Build Coastguard Worker kind: 'regular', 474*6dbdd20aSAndroid Build Coastguard Worker table: table.name, 475*6dbdd20aSAndroid Build Coastguard Worker column: columnName, 476*6dbdd20aSAndroid Build Coastguard Worker }; 477*6dbdd20aSAndroid Build Coastguard Worker if (selectedPivots.has(columnKey(column))) { 478*6dbdd20aSAndroid Build Coastguard Worker continue; 479*6dbdd20aSAndroid Build Coastguard Worker } 480*6dbdd20aSAndroid Build Coastguard Worker group.push( 481*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 482*6dbdd20aSAndroid Build Coastguard Worker label: columnName, 483*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 484*6dbdd20aSAndroid Build Coastguard Worker pivotMgr.setPivotSelected({column, selected: true}); 485*6dbdd20aSAndroid Build Coastguard Worker }, 486*6dbdd20aSAndroid Build Coastguard Worker }), 487*6dbdd20aSAndroid Build Coastguard Worker ); 488*6dbdd20aSAndroid Build Coastguard Worker } 489*6dbdd20aSAndroid Build Coastguard Worker items.push( 490*6dbdd20aSAndroid Build Coastguard Worker m( 491*6dbdd20aSAndroid Build Coastguard Worker MenuItem, 492*6dbdd20aSAndroid Build Coastguard Worker { 493*6dbdd20aSAndroid Build Coastguard Worker label: `Add ${table.displayName} pivot`, 494*6dbdd20aSAndroid Build Coastguard Worker }, 495*6dbdd20aSAndroid Build Coastguard Worker group, 496*6dbdd20aSAndroid Build Coastguard Worker ), 497*6dbdd20aSAndroid Build Coastguard Worker ); 498*6dbdd20aSAndroid Build Coastguard Worker } 499*6dbdd20aSAndroid Build Coastguard Worker 500*6dbdd20aSAndroid Build Coastguard Worker return { 501*6dbdd20aSAndroid Build Coastguard Worker content: [ 502*6dbdd20aSAndroid Build Coastguard Worker readableColumnName(pivot), 503*6dbdd20aSAndroid Build Coastguard Worker m(PopupMenu2, {trigger: m(Button, {icon: 'more_horiz'})}, items), 504*6dbdd20aSAndroid Build Coastguard Worker ], 505*6dbdd20aSAndroid Build Coastguard Worker }; 506*6dbdd20aSAndroid Build Coastguard Worker } 507*6dbdd20aSAndroid Build Coastguard Worker 508*6dbdd20aSAndroid Build Coastguard Worker renderResultsTable(attrs: PivotTableAttrs) { 509*6dbdd20aSAndroid Build Coastguard Worker const state = this.pivotState; 510*6dbdd20aSAndroid Build Coastguard Worker const queryResult = state.queryResult; 511*6dbdd20aSAndroid Build Coastguard Worker if (queryResult === undefined) { 512*6dbdd20aSAndroid Build Coastguard Worker return m('div', 'Loading...'); 513*6dbdd20aSAndroid Build Coastguard Worker } 514*6dbdd20aSAndroid Build Coastguard Worker 515*6dbdd20aSAndroid Build Coastguard Worker const renderedRows: m.Vnode[] = []; 516*6dbdd20aSAndroid Build Coastguard Worker 517*6dbdd20aSAndroid Build Coastguard Worker // We should not even be showing the tab if there's no results. 518*6dbdd20aSAndroid Build Coastguard Worker const tree = queryResult.tree; 519*6dbdd20aSAndroid Build Coastguard Worker assertFalse(tree.children.size === 0 && tree.rows.length === 0); 520*6dbdd20aSAndroid Build Coastguard Worker 521*6dbdd20aSAndroid Build Coastguard Worker this.renderTree(attrs, [], tree, queryResult, renderedRows); 522*6dbdd20aSAndroid Build Coastguard Worker 523*6dbdd20aSAndroid Build Coastguard Worker const selectedPivots = new Set( 524*6dbdd20aSAndroid Build Coastguard Worker this.pivotState.selectedPivots.map(columnKey), 525*6dbdd20aSAndroid Build Coastguard Worker ); 526*6dbdd20aSAndroid Build Coastguard Worker const pivotTableHeaders = state.selectedPivots.map((pivot) => 527*6dbdd20aSAndroid Build Coastguard Worker this.renderPivotColumnHeader(queryResult, pivot, selectedPivots), 528*6dbdd20aSAndroid Build Coastguard Worker ); 529*6dbdd20aSAndroid Build Coastguard Worker 530*6dbdd20aSAndroid Build Coastguard Worker const removeItem = queryResult.metadata.aggregationColumns.length > 1; 531*6dbdd20aSAndroid Build Coastguard Worker const aggregationTableHeaders = queryResult.metadata.aggregationColumns.map( 532*6dbdd20aSAndroid Build Coastguard Worker (aggregation, index) => 533*6dbdd20aSAndroid Build Coastguard Worker this.renderAggregationHeaderCell(aggregation, index, removeItem), 534*6dbdd20aSAndroid Build Coastguard Worker ); 535*6dbdd20aSAndroid Build Coastguard Worker 536*6dbdd20aSAndroid Build Coastguard Worker return m( 537*6dbdd20aSAndroid Build Coastguard Worker 'table.pivot-table', 538*6dbdd20aSAndroid Build Coastguard Worker m( 539*6dbdd20aSAndroid Build Coastguard Worker 'thead', 540*6dbdd20aSAndroid Build Coastguard Worker // First row of the table, containing names of pivot and aggregation 541*6dbdd20aSAndroid Build Coastguard Worker // columns, as well as popup menus to modify the columns. Last cell 542*6dbdd20aSAndroid Build Coastguard Worker // is empty because of an extra column with "drill down" button for 543*6dbdd20aSAndroid Build Coastguard Worker // each pivot table row. 544*6dbdd20aSAndroid Build Coastguard Worker m( 545*6dbdd20aSAndroid Build Coastguard Worker 'tr.header', 546*6dbdd20aSAndroid Build Coastguard Worker m(ReorderableCellGroup, { 547*6dbdd20aSAndroid Build Coastguard Worker cells: pivotTableHeaders, 548*6dbdd20aSAndroid Build Coastguard Worker onReorder: (from: number, to: number, direction: DropDirection) => { 549*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr.setOrder(from, to, direction); 550*6dbdd20aSAndroid Build Coastguard Worker }, 551*6dbdd20aSAndroid Build Coastguard Worker }), 552*6dbdd20aSAndroid Build Coastguard Worker m(ReorderableCellGroup, { 553*6dbdd20aSAndroid Build Coastguard Worker cells: aggregationTableHeaders, 554*6dbdd20aSAndroid Build Coastguard Worker onReorder: (from: number, to: number, direction: DropDirection) => { 555*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr.setAggregationOrder(from, to, direction); 556*6dbdd20aSAndroid Build Coastguard Worker }, 557*6dbdd20aSAndroid Build Coastguard Worker }), 558*6dbdd20aSAndroid Build Coastguard Worker m( 559*6dbdd20aSAndroid Build Coastguard Worker 'td.menu', 560*6dbdd20aSAndroid Build Coastguard Worker m( 561*6dbdd20aSAndroid Build Coastguard Worker PopupMenu2, 562*6dbdd20aSAndroid Build Coastguard Worker { 563*6dbdd20aSAndroid Build Coastguard Worker trigger: m(Button, {icon: 'menu'}), 564*6dbdd20aSAndroid Build Coastguard Worker }, 565*6dbdd20aSAndroid Build Coastguard Worker m(MenuItem, { 566*6dbdd20aSAndroid Build Coastguard Worker label: state.constrainToArea 567*6dbdd20aSAndroid Build Coastguard Worker ? 'Query data for the whole timeline' 568*6dbdd20aSAndroid Build Coastguard Worker : 'Constrain to selected area', 569*6dbdd20aSAndroid Build Coastguard Worker onclick: () => { 570*6dbdd20aSAndroid Build Coastguard Worker this.pivotMgr.setConstrainedToArea(!state.constrainToArea); 571*6dbdd20aSAndroid Build Coastguard Worker }, 572*6dbdd20aSAndroid Build Coastguard Worker }), 573*6dbdd20aSAndroid Build Coastguard Worker ), 574*6dbdd20aSAndroid Build Coastguard Worker ), 575*6dbdd20aSAndroid Build Coastguard Worker ), 576*6dbdd20aSAndroid Build Coastguard Worker ), 577*6dbdd20aSAndroid Build Coastguard Worker m('tbody', this.renderTotalsRow(queryResult), renderedRows), 578*6dbdd20aSAndroid Build Coastguard Worker ); 579*6dbdd20aSAndroid Build Coastguard Worker } 580*6dbdd20aSAndroid Build Coastguard Worker 581*6dbdd20aSAndroid Build Coastguard Worker view({attrs}: m.Vnode<PivotTableAttrs>): m.Children { 582*6dbdd20aSAndroid Build Coastguard Worker return m('.pivot-table', this.renderResultsTable(attrs)); 583*6dbdd20aSAndroid Build Coastguard Worker } 584*6dbdd20aSAndroid Build Coastguard Worker} 585