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 {Actions} from '../common/actions'; 16import { 17 AggregationAttrs, 18 isStackPivot, 19 PivotAttrs, 20 TableAttrs 21} from '../common/pivot_table_common'; 22import {globals} from './globals'; 23 24export function isAggregationAttrs(attrs: PivotAttrs|AggregationAttrs): 25 attrs is AggregationAttrs { 26 return (attrs as AggregationAttrs).aggregation !== undefined; 27} 28 29function equalTableAttrs( 30 left: PivotAttrs|AggregationAttrs, right: PivotAttrs|AggregationAttrs) { 31 if (left.columnName !== right.columnName) { 32 return false; 33 } 34 35 if (left.tableName !== right.tableName) { 36 return false; 37 } 38 39 if (isAggregationAttrs(left) && isAggregationAttrs(right)) { 40 if (left.aggregation !== right.aggregation) { 41 return false; 42 } 43 } 44 return true; 45} 46 47export function getDataTransferType(isPivot: boolean) { 48 if (isPivot) { 49 return 'perfetto/pivot-table-dragged-pivot'; 50 } 51 return 'perfetto/pivot-table-dragged-aggregation'; 52} 53 54export class PivotTableHelper { 55 readonly pivotTableId: string; 56 readonly availableColumns: TableAttrs[]; 57 readonly availableAggregations: string[]; 58 readonly totalColumnsCount = 0; 59 60 private _selectedPivots: PivotAttrs[] = []; 61 private _selectedAggregations: AggregationAttrs[] = []; 62 private _isPivot = true; 63 private _selectedColumnIndex = 0; 64 private _selectedAggregationIndex = 0; 65 private _editPivotTableModalOpen = false; 66 67 constructor( 68 pivotTableId: string, availableColumns: TableAttrs[], 69 availableAggregations: string[], selectedPivots: PivotAttrs[], 70 selectedAggregations: AggregationAttrs[]) { 71 this.pivotTableId = pivotTableId; 72 this.availableColumns = availableColumns; 73 for (const table of this.availableColumns) { 74 this.totalColumnsCount += table.columns.length; 75 } 76 this.availableAggregations = availableAggregations; 77 this.setSelectedPivotsAndAggregations(selectedPivots, selectedAggregations); 78 } 79 80 // Sets selected pivots and aggregations if the editor modal is not open. 81 setSelectedPivotsAndAggregations( 82 selectedPivots: PivotAttrs[], selectedAggregations: AggregationAttrs[]) { 83 if (!this.editPivotTableModalOpen) { 84 // Making a copy of selectedPivots and selectedAggregations to preserve 85 // the original state. 86 this._selectedPivots = 87 selectedPivots.map(pivot => Object.assign({}, pivot)); 88 this._selectedAggregations = selectedAggregations.map( 89 aggregation => Object.assign({}, aggregation)); 90 } 91 } 92 93 // Dictates if the selected indexes refer to a pivot or aggregation. 94 togglePivotSelection() { 95 this._isPivot = !this._isPivot; 96 if (!this._isPivot) { 97 const selectedColumn = this.getSelectedPivotTableColumnAttrs(); 98 if (isStackPivot(selectedColumn.tableName, selectedColumn.columnName)) { 99 this._selectedColumnIndex = Math.max(0, this._selectedColumnIndex - 1); 100 } 101 } 102 } 103 104 setSelectedPivotTableColumnIndex(index: number) { 105 if (index < 0 && index >= this.totalColumnsCount) { 106 throw Error(`Selected column index "${index}" out of bounds.`); 107 } 108 this._selectedColumnIndex = index; 109 } 110 111 setSelectedPivotTableAggregationIndex(index: number) { 112 if (index < 0 && index >= this.availableAggregations.length) { 113 throw Error(`Selected aggregation index "${index}" out of bounds.`); 114 } 115 this._selectedAggregationIndex = index; 116 } 117 118 // Get column attributes on selectedColumnIndex and 119 // selectedAggregationIndex. 120 getSelectedPivotTableColumnAttrs(): PivotAttrs|AggregationAttrs { 121 let tableName, columnName; 122 // Finds column index relative to its table. 123 let colIdx = this._selectedColumnIndex; 124 for (const {tableName: tblName, columns} of this.availableColumns) { 125 if (colIdx < columns.length) { 126 tableName = tblName; 127 columnName = columns[colIdx]; 128 break; 129 } 130 colIdx -= columns.length; 131 } 132 if (tableName === undefined || columnName === undefined) { 133 throw Error( 134 'Pivot table selected column does not exist in availableColumns.'); 135 } 136 137 // Get aggregation if selected column is not a pivot, undefined otherwise. 138 if (!this._isPivot) { 139 const aggregation = 140 this.availableAggregations[this._selectedAggregationIndex]; 141 return {tableName, columnName, aggregation, order: 'DESC'}; 142 } 143 144 return { 145 tableName, 146 columnName, 147 isStackPivot: isStackPivot(tableName, columnName) 148 }; 149 } 150 151 // Adds column based on selected index to selectedPivots or 152 // selectedAggregations if it doesn't already exist, remove otherwise. 153 updatePivotTableColumnOnSelectedIndex() { 154 const columnAttrs = this.getSelectedPivotTableColumnAttrs(); 155 this.updatePivotTableColumnOnColumnAttributes(columnAttrs); 156 } 157 158 // Adds column based on column attributes to selectedPivots or 159 // selectedAggregations if it doesn't already exist, remove otherwise. 160 updatePivotTableColumnOnColumnAttributes(columnAttrs: PivotAttrs| 161 AggregationAttrs) { 162 let storage: Array<PivotAttrs|AggregationAttrs>; 163 let attrs: PivotAttrs|AggregationAttrs; 164 if (isAggregationAttrs(columnAttrs)) { 165 if (isStackPivot(columnAttrs.tableName, columnAttrs.columnName)) { 166 throw Error( 167 `Stack column "${columnAttrs.tableName} ${ 168 columnAttrs.columnName}" should not ` + 169 `be added as an aggregation.`); 170 } 171 storage = this._selectedAggregations; 172 attrs = { 173 tableName: columnAttrs.tableName, 174 columnName: columnAttrs.columnName, 175 aggregation: columnAttrs.aggregation, 176 order: columnAttrs.order 177 }; 178 } else { 179 storage = this._selectedPivots; 180 attrs = { 181 tableName: columnAttrs.tableName, 182 columnName: columnAttrs.columnName, 183 isStackPivot: columnAttrs.isStackPivot 184 }; 185 } 186 const index = 187 storage.findIndex(element => equalTableAttrs(element, columnAttrs)); 188 189 if (index === -1) { 190 storage.push(attrs); 191 } else { 192 storage.splice(index, 1); 193 } 194 } 195 196 clearPivotTableColumns() { 197 this._selectedPivots = []; 198 this._selectedAggregations = []; 199 } 200 201 // Changes aggregation sorting from DESC to ASC and vice versa. 202 togglePivotTableAggregationSorting(index: number) { 203 if (index < 0 || index >= this._selectedAggregations.length) { 204 throw Error(`Column index "${index}" is out of bounds.`); 205 } 206 this._selectedAggregations[index].order = 207 this._selectedAggregations[index].order === 'DESC' ? 'ASC' : 'DESC'; 208 } 209 210 // Change aggregation function to existing column. 211 changeAggregation(index: number, aggregation: string) { 212 if (index < 0 || index >= this._selectedAggregations.length) { 213 throw Error(`Column index "${index}" is out of bounds.`); 214 } 215 this._selectedAggregations[index].aggregation = aggregation; 216 } 217 218 // Moves target column to the requested destination. 219 reorderPivotTableDraggedColumn( 220 isPivot: boolean, targetColumnIdx: number, destinationColumnIdx: number) { 221 let storage: Array<PivotAttrs|AggregationAttrs>; 222 if (isPivot) { 223 storage = this._selectedPivots; 224 } else { 225 storage = this._selectedAggregations; 226 } 227 228 if (targetColumnIdx < 0 || targetColumnIdx >= storage.length) { 229 throw Error(`Target column index "${targetColumnIdx}" out of bounds.`); 230 } 231 if (destinationColumnIdx < 0 || destinationColumnIdx >= storage.length) { 232 throw Error( 233 `Destination column index "${destinationColumnIdx}" out of bounds.`); 234 } 235 236 const targetColumn: PivotAttrs|AggregationAttrs = storage[targetColumnIdx]; 237 storage.splice(targetColumnIdx, 1); 238 storage.splice(destinationColumnIdx, 0, targetColumn); 239 } 240 241 selectedColumnOnDrag(e: DragEvent, isPivot: boolean, targetIdx: number) { 242 const dataTransferType = getDataTransferType(isPivot); 243 if (e.dataTransfer === null) { 244 return; 245 } 246 e.dataTransfer.setData(dataTransferType, targetIdx.toString()); 247 } 248 249 selectedColumnOnDrop( 250 e: DragEvent, isPivot: boolean, destinationColumnIdx: number) { 251 const dataTransferType = getDataTransferType(isPivot); 252 if (e.dataTransfer === null) { 253 return; 254 } 255 // Prevents dragging pivots to aggregations and vice versa. 256 if (!e.dataTransfer.types.includes(dataTransferType)) { 257 return; 258 } 259 260 const targetColumnIdxString = e.dataTransfer.getData(dataTransferType); 261 const targetColumnIdx = Number(targetColumnIdxString); 262 if (!Number.isInteger(targetColumnIdx)) { 263 throw Error( 264 `Target column index "${targetColumnIdxString}" is not valid.`); 265 } 266 267 this.reorderPivotTableDraggedColumn( 268 isPivot, targetColumnIdx, destinationColumnIdx); 269 e.dataTransfer.clearData(dataTransferType); 270 } 271 272 273 // Highlights valid drop locations when dragging over them. 274 highlightDropLocation(e: DragEvent, isPivot: boolean) { 275 if (e.dataTransfer === null) { 276 return; 277 } 278 // Prevents highlighting aggregations when dragging pivots over them 279 // and vice versa. 280 if (!e.dataTransfer.types.includes(getDataTransferType(isPivot))) { 281 return; 282 } 283 (e.target as HTMLTableDataCellElement).classList.add('drop-location'); 284 } 285 286 removeHighlightFromDropLocation(e: DragEvent) { 287 (e.target as HTMLTableDataCellElement).classList.remove('drop-location'); 288 } 289 290 // Gets column index in availableColumns based on its attributes. 291 getColumnIndex(columnAttrs: PivotAttrs|AggregationAttrs) { 292 let index = 0; 293 for (const {tableName, columns} of this.availableColumns) { 294 if (tableName === columnAttrs.tableName) { 295 const colIdx = 296 columns.findIndex(column => column === columnAttrs.columnName); 297 return colIdx === -1 ? -1 : index + colIdx; 298 } 299 index += columns.length; 300 } 301 return -1; 302 } 303 304 selectPivotTableColumn(columnAttrs: PivotAttrs|AggregationAttrs) { 305 this._isPivot = !isAggregationAttrs(columnAttrs); 306 307 const colIndex = this.getColumnIndex(columnAttrs); 308 if (colIndex === -1) { 309 throw Error(`Selected column "${columnAttrs.tableName} ${ 310 columnAttrs.columnName}" not found in availableColumns.`); 311 } 312 this.setSelectedPivotTableColumnIndex(colIndex); 313 314 if (isAggregationAttrs(columnAttrs)) { 315 const aggIndex = this.availableAggregations.findIndex( 316 aggregation => aggregation === columnAttrs.aggregation); 317 if (aggIndex === -1) { 318 throw Error(`Selected aggregation "${ 319 columnAttrs.aggregation}" not found in availableAggregations.`); 320 } 321 this.setSelectedPivotTableAggregationIndex(aggIndex); 322 } 323 } 324 325 queryPivotTableChanges() { 326 globals.dispatch(Actions.setSelectedPivotsAndAggregations({ 327 pivotTableId: this.pivotTableId, 328 selectedPivots: this._selectedPivots, 329 selectedAggregations: this._selectedAggregations 330 })); 331 globals.dispatch(Actions.setPivotTableRequest( 332 {pivotTableId: this.pivotTableId, action: 'QUERY'})); 333 } 334 335 toggleEditPivotTableModal() { 336 this._editPivotTableModalOpen = !this._editPivotTableModalOpen; 337 } 338 339 get selectedPivots() { 340 return this._selectedPivots.map(pivot => Object.assign({}, pivot)); 341 } 342 343 get selectedAggregations() { 344 return this._selectedAggregations.map( 345 aggregation => Object.assign({}, aggregation)); 346 } 347 348 get isPivot() { 349 return this._isPivot; 350 } 351 352 get selectedColumnIndex() { 353 return this._selectedColumnIndex; 354 } 355 356 get selectedAggregationIndex() { 357 return this._selectedAggregationIndex; 358 } 359 360 get editPivotTableModalOpen() { 361 return this._editPivotTableModalOpen; 362 } 363} 364