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