• 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 {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