| // Copyright (C) 2021 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {Actions} from '../common/actions'; |
| import { |
| AggregationAttrs, |
| isStackPivot, |
| PivotAttrs, |
| TableAttrs |
| } from '../common/pivot_table_common'; |
| import {globals} from './globals'; |
| |
| export function isAggregationAttrs(attrs: PivotAttrs|AggregationAttrs): |
| attrs is AggregationAttrs { |
| return (attrs as AggregationAttrs).aggregation !== undefined; |
| } |
| |
| function equalTableAttrs( |
| left: PivotAttrs|AggregationAttrs, right: PivotAttrs|AggregationAttrs) { |
| if (left.columnName !== right.columnName) { |
| return false; |
| } |
| |
| if (left.tableName !== right.tableName) { |
| return false; |
| } |
| |
| if (isAggregationAttrs(left) && isAggregationAttrs(right)) { |
| if (left.aggregation !== right.aggregation) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| export function getDataTransferType(isPivot: boolean) { |
| if (isPivot) { |
| return 'perfetto/pivot-table-dragged-pivot'; |
| } |
| return 'perfetto/pivot-table-dragged-aggregation'; |
| } |
| |
| export class PivotTableHelper { |
| readonly pivotTableId: string; |
| readonly availableColumns: TableAttrs[]; |
| readonly availableAggregations: string[]; |
| readonly totalColumnsCount = 0; |
| |
| private _selectedPivots: PivotAttrs[] = []; |
| private _selectedAggregations: AggregationAttrs[] = []; |
| private _isPivot = true; |
| private _selectedColumnIndex = 0; |
| private _selectedAggregationIndex = 0; |
| private _editPivotTableModalOpen = false; |
| |
| constructor( |
| pivotTableId: string, availableColumns: TableAttrs[], |
| availableAggregations: string[], selectedPivots: PivotAttrs[], |
| selectedAggregations: AggregationAttrs[]) { |
| this.pivotTableId = pivotTableId; |
| this.availableColumns = availableColumns; |
| for (const table of this.availableColumns) { |
| this.totalColumnsCount += table.columns.length; |
| } |
| this.availableAggregations = availableAggregations; |
| this.setSelectedPivotsAndAggregations(selectedPivots, selectedAggregations); |
| } |
| |
| // Sets selected pivots and aggregations if the editor modal is not open. |
| setSelectedPivotsAndAggregations( |
| selectedPivots: PivotAttrs[], selectedAggregations: AggregationAttrs[]) { |
| if (!this.editPivotTableModalOpen) { |
| // Making a copy of selectedPivots and selectedAggregations to preserve |
| // the original state. |
| this._selectedPivots = |
| selectedPivots.map(pivot => Object.assign({}, pivot)); |
| this._selectedAggregations = selectedAggregations.map( |
| aggregation => Object.assign({}, aggregation)); |
| } |
| } |
| |
| // Dictates if the selected indexes refer to a pivot or aggregation. |
| togglePivotSelection() { |
| this._isPivot = !this._isPivot; |
| if (!this._isPivot) { |
| const selectedColumn = this.getSelectedPivotTableColumnAttrs(); |
| if (isStackPivot(selectedColumn.tableName, selectedColumn.columnName)) { |
| this._selectedColumnIndex = Math.max(0, this._selectedColumnIndex - 1); |
| } |
| } |
| } |
| |
| setSelectedPivotTableColumnIndex(index: number) { |
| if (index < 0 && index >= this.totalColumnsCount) { |
| throw Error(`Selected column index "${index}" out of bounds.`); |
| } |
| this._selectedColumnIndex = index; |
| } |
| |
| setSelectedPivotTableAggregationIndex(index: number) { |
| if (index < 0 && index >= this.availableAggregations.length) { |
| throw Error(`Selected aggregation index "${index}" out of bounds.`); |
| } |
| this._selectedAggregationIndex = index; |
| } |
| |
| // Get column attributes on selectedColumnIndex and |
| // selectedAggregationIndex. |
| getSelectedPivotTableColumnAttrs(): PivotAttrs|AggregationAttrs { |
| let tableName, columnName; |
| // Finds column index relative to its table. |
| let colIdx = this._selectedColumnIndex; |
| for (const {tableName: tblName, columns} of this.availableColumns) { |
| if (colIdx < columns.length) { |
| tableName = tblName; |
| columnName = columns[colIdx]; |
| break; |
| } |
| colIdx -= columns.length; |
| } |
| if (tableName === undefined || columnName === undefined) { |
| throw Error( |
| 'Pivot table selected column does not exist in availableColumns.'); |
| } |
| |
| // Get aggregation if selected column is not a pivot, undefined otherwise. |
| if (!this._isPivot) { |
| const aggregation = |
| this.availableAggregations[this._selectedAggregationIndex]; |
| return {tableName, columnName, aggregation, order: 'DESC'}; |
| } |
| |
| return { |
| tableName, |
| columnName, |
| isStackPivot: isStackPivot(tableName, columnName) |
| }; |
| } |
| |
| // Adds column based on selected index to selectedPivots or |
| // selectedAggregations if it doesn't already exist, remove otherwise. |
| updatePivotTableColumnOnSelectedIndex() { |
| const columnAttrs = this.getSelectedPivotTableColumnAttrs(); |
| this.updatePivotTableColumnOnColumnAttributes(columnAttrs); |
| } |
| |
| // Adds column based on column attributes to selectedPivots or |
| // selectedAggregations if it doesn't already exist, remove otherwise. |
| updatePivotTableColumnOnColumnAttributes(columnAttrs: PivotAttrs| |
| AggregationAttrs) { |
| let storage: Array<PivotAttrs|AggregationAttrs>; |
| let attrs: PivotAttrs|AggregationAttrs; |
| if (isAggregationAttrs(columnAttrs)) { |
| if (isStackPivot(columnAttrs.tableName, columnAttrs.columnName)) { |
| throw Error( |
| `Stack column "${columnAttrs.tableName} ${ |
| columnAttrs.columnName}" should not ` + |
| `be added as an aggregation.`); |
| } |
| storage = this._selectedAggregations; |
| attrs = { |
| tableName: columnAttrs.tableName, |
| columnName: columnAttrs.columnName, |
| aggregation: columnAttrs.aggregation, |
| order: columnAttrs.order |
| }; |
| } else { |
| storage = this._selectedPivots; |
| attrs = { |
| tableName: columnAttrs.tableName, |
| columnName: columnAttrs.columnName, |
| isStackPivot: columnAttrs.isStackPivot |
| }; |
| } |
| const index = |
| storage.findIndex(element => equalTableAttrs(element, columnAttrs)); |
| |
| if (index === -1) { |
| storage.push(attrs); |
| } else { |
| storage.splice(index, 1); |
| } |
| } |
| |
| clearPivotTableColumns() { |
| this._selectedPivots = []; |
| this._selectedAggregations = []; |
| } |
| |
| // Changes aggregation sorting from DESC to ASC and vice versa. |
| togglePivotTableAggregationSorting(index: number) { |
| if (index < 0 || index >= this._selectedAggregations.length) { |
| throw Error(`Column index "${index}" is out of bounds.`); |
| } |
| this._selectedAggregations[index].order = |
| this._selectedAggregations[index].order === 'DESC' ? 'ASC' : 'DESC'; |
| } |
| |
| // Change aggregation function to existing column. |
| changeAggregation(index: number, aggregation: string) { |
| if (index < 0 || index >= this._selectedAggregations.length) { |
| throw Error(`Column index "${index}" is out of bounds.`); |
| } |
| this._selectedAggregations[index].aggregation = aggregation; |
| } |
| |
| // Moves target column to the requested destination. |
| reorderPivotTableDraggedColumn( |
| isPivot: boolean, targetColumnIdx: number, destinationColumnIdx: number) { |
| let storage: Array<PivotAttrs|AggregationAttrs>; |
| if (isPivot) { |
| storage = this._selectedPivots; |
| } else { |
| storage = this._selectedAggregations; |
| } |
| |
| if (targetColumnIdx < 0 || targetColumnIdx >= storage.length) { |
| throw Error(`Target column index "${targetColumnIdx}" out of bounds.`); |
| } |
| if (destinationColumnIdx < 0 || destinationColumnIdx >= storage.length) { |
| throw Error( |
| `Destination column index "${destinationColumnIdx}" out of bounds.`); |
| } |
| |
| const targetColumn: PivotAttrs|AggregationAttrs = storage[targetColumnIdx]; |
| storage.splice(targetColumnIdx, 1); |
| storage.splice(destinationColumnIdx, 0, targetColumn); |
| } |
| |
| selectedColumnOnDrag(e: DragEvent, isPivot: boolean, targetIdx: number) { |
| const dataTransferType = getDataTransferType(isPivot); |
| if (e.dataTransfer === null) { |
| return; |
| } |
| e.dataTransfer.setData(dataTransferType, targetIdx.toString()); |
| } |
| |
| selectedColumnOnDrop( |
| e: DragEvent, isPivot: boolean, destinationColumnIdx: number) { |
| const dataTransferType = getDataTransferType(isPivot); |
| if (e.dataTransfer === null) { |
| return; |
| } |
| // Prevents dragging pivots to aggregations and vice versa. |
| if (!e.dataTransfer.types.includes(dataTransferType)) { |
| return; |
| } |
| |
| const targetColumnIdxString = e.dataTransfer.getData(dataTransferType); |
| const targetColumnIdx = Number(targetColumnIdxString); |
| if (!Number.isInteger(targetColumnIdx)) { |
| throw Error( |
| `Target column index "${targetColumnIdxString}" is not valid.`); |
| } |
| |
| this.reorderPivotTableDraggedColumn( |
| isPivot, targetColumnIdx, destinationColumnIdx); |
| e.dataTransfer.clearData(dataTransferType); |
| } |
| |
| |
| // Highlights valid drop locations when dragging over them. |
| highlightDropLocation(e: DragEvent, isPivot: boolean) { |
| if (e.dataTransfer === null) { |
| return; |
| } |
| // Prevents highlighting aggregations when dragging pivots over them |
| // and vice versa. |
| if (!e.dataTransfer.types.includes(getDataTransferType(isPivot))) { |
| return; |
| } |
| (e.target as HTMLTableDataCellElement).classList.add('drop-location'); |
| } |
| |
| removeHighlightFromDropLocation(e: DragEvent) { |
| (e.target as HTMLTableDataCellElement).classList.remove('drop-location'); |
| } |
| |
| // Gets column index in availableColumns based on its attributes. |
| getColumnIndex(columnAttrs: PivotAttrs|AggregationAttrs) { |
| let index = 0; |
| for (const {tableName, columns} of this.availableColumns) { |
| if (tableName === columnAttrs.tableName) { |
| const colIdx = |
| columns.findIndex(column => column === columnAttrs.columnName); |
| return colIdx === -1 ? -1 : index + colIdx; |
| } |
| index += columns.length; |
| } |
| return -1; |
| } |
| |
| selectPivotTableColumn(columnAttrs: PivotAttrs|AggregationAttrs) { |
| this._isPivot = !isAggregationAttrs(columnAttrs); |
| |
| const colIndex = this.getColumnIndex(columnAttrs); |
| if (colIndex === -1) { |
| throw Error(`Selected column "${columnAttrs.tableName} ${ |
| columnAttrs.columnName}" not found in availableColumns.`); |
| } |
| this.setSelectedPivotTableColumnIndex(colIndex); |
| |
| if (isAggregationAttrs(columnAttrs)) { |
| const aggIndex = this.availableAggregations.findIndex( |
| aggregation => aggregation === columnAttrs.aggregation); |
| if (aggIndex === -1) { |
| throw Error(`Selected aggregation "${ |
| columnAttrs.aggregation}" not found in availableAggregations.`); |
| } |
| this.setSelectedPivotTableAggregationIndex(aggIndex); |
| } |
| } |
| |
| queryPivotTableChanges() { |
| globals.dispatch(Actions.setSelectedPivotsAndAggregations({ |
| pivotTableId: this.pivotTableId, |
| selectedPivots: this._selectedPivots, |
| selectedAggregations: this._selectedAggregations |
| })); |
| globals.dispatch(Actions.setPivotTableRequest( |
| {pivotTableId: this.pivotTableId, action: 'QUERY'})); |
| } |
| |
| toggleEditPivotTableModal() { |
| this._editPivotTableModalOpen = !this._editPivotTableModalOpen; |
| } |
| |
| get selectedPivots() { |
| return this._selectedPivots.map(pivot => Object.assign({}, pivot)); |
| } |
| |
| get selectedAggregations() { |
| return this._selectedAggregations.map( |
| aggregation => Object.assign({}, aggregation)); |
| } |
| |
| get isPivot() { |
| return this._isPivot; |
| } |
| |
| get selectedColumnIndex() { |
| return this._selectedColumnIndex; |
| } |
| |
| get selectedAggregationIndex() { |
| return this._selectedAggregationIndex; |
| } |
| |
| get editPivotTableModalOpen() { |
| return this._editPivotTableModalOpen; |
| } |
| } |