| /* |
| * Copyright (C) 2022 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 {Engine} from '../common/engine'; |
| import {featureFlags} from '../common/feature_flags'; |
| import {ColumnType} from '../common/query_result'; |
| import { |
| PivotTableReduxQueryMetadata, |
| PivotTableReduxResult |
| } from '../common/state'; |
| import {aggregationIndex} from '../frontend/pivot_table_redux_query_generator'; |
| |
| import {Controller} from './controller'; |
| import {globals} from './globals'; |
| |
| export const PIVOT_TABLE_REDUX_FLAG = featureFlags.register({ |
| id: 'pivotTableRedux', |
| name: 'Pivot tables V2', |
| description: 'Second version of pivot table', |
| defaultValue: false, |
| }); |
| |
| // Node in the hierarchical pivot tree. Only leaf nodes contain data from the |
| // query result. |
| export interface PivotTree { |
| // Whether the node should be collapsed in the UI, false by default and can |
| // be toggled with the button. |
| isCollapsed: boolean; |
| |
| // Non-empty only in internal nodes. |
| children: Map<ColumnType, PivotTree>; |
| aggregates: ColumnType[]; |
| |
| // Non-empty only in leaf nodes. |
| rows: ColumnType[][]; |
| } |
| |
| // Auxiliary class to build the tree from query response. |
| class TreeBuilder { |
| private readonly root: PivotTree; |
| lastRow: ColumnType[]; |
| pivotColumns: number; |
| aggregateColumns: number; |
| |
| constructor( |
| pivotColumns: number, aggregateColumns: number, firstRow: ColumnType[]) { |
| this.pivotColumns = pivotColumns; |
| this.aggregateColumns = aggregateColumns; |
| this.root = this.createNode(0, firstRow); |
| let tree = this.root; |
| for (let i = 0; i + 1 < this.pivotColumns; i++) { |
| const value = firstRow[i]; |
| tree = TreeBuilder.insertChild( |
| tree, value, this.createNode(i + 1, firstRow)); |
| } |
| this.lastRow = firstRow; |
| } |
| |
| // Add incoming row to the tree being built. |
| ingestRow(row: ColumnType[]) { |
| let tree = this.root; |
| for (let i = 0; i + 1 < this.pivotColumns; i++) { |
| const nextTree = tree.children.get(row[i]); |
| if (nextTree === undefined) { |
| // Insert the new node into the tree, and make variable `tree` point |
| // to the newly created node. |
| tree = |
| TreeBuilder.insertChild(tree, row[i], this.createNode(i + 1, row)); |
| } else { |
| tree = nextTree; |
| } |
| } |
| tree.rows.push(row); |
| this.lastRow = row; |
| } |
| |
| build(): PivotTree { |
| return this.root; |
| } |
| |
| // Helper method that inserts child node into the tree and returns it, used |
| // for more concise modification of local variable pointing to the current |
| // node being built. |
| static insertChild(tree: PivotTree, key: ColumnType, child: PivotTree): |
| PivotTree { |
| tree.children.set(key, child); |
| return child; |
| } |
| |
| // Initialize PivotTree from a row. |
| createNode(depth: number, row: ColumnType[]): PivotTree { |
| const aggregates = []; |
| |
| for (let j = 0; j < this.aggregateColumns; j++) { |
| aggregates.push(row[aggregationIndex(this.pivotColumns, j, depth)]); |
| } |
| |
| return { |
| isCollapsed: false, |
| children: new Map(), |
| aggregates, |
| rows: [], |
| }; |
| } |
| } |
| |
| function createEmptyQueryResult(metadata: PivotTableReduxQueryMetadata): |
| PivotTableReduxResult { |
| return { |
| tree: { |
| aggregates: [], |
| isCollapsed: false, |
| children: new Map(), |
| rows: [], |
| }, |
| metadata |
| }; |
| } |
| |
| |
| // Controller responsible for showing the panel with pivot table, as well as |
| // executing its queries and post-processing query results. |
| export class PivotTableReduxController extends Controller<{}> { |
| engine: Engine; |
| lastStartedQueryId: number; |
| |
| constructor(args: {engine: Engine}) { |
| super({}); |
| this.engine = args.engine; |
| this.lastStartedQueryId = 0; |
| } |
| |
| run() { |
| if (!PIVOT_TABLE_REDUX_FLAG.get()) { |
| return; |
| } |
| |
| const pivotTableState = globals.state.pivotTableRedux; |
| if (pivotTableState.queryId > this.lastStartedQueryId && |
| pivotTableState.query !== null) { |
| this.lastStartedQueryId = pivotTableState.queryId; |
| const query = pivotTableState.query; |
| |
| this.engine.query(query.text).then(async (result) => { |
| try { |
| await result.waitAllRows(); |
| } catch { |
| // waitAllRows() frequently throws an exception, which is ignored in |
| // its other calls, so it's ignored here as well. |
| } |
| |
| const columns = result.columns(); |
| |
| const it = result.iter({}); |
| function nextRow(): ColumnType[] { |
| const row: ColumnType[] = []; |
| for (const column of columns) { |
| row.push(it.get(column)); |
| } |
| it.next(); |
| return row; |
| } |
| |
| if (!it.valid()) { |
| // Iterator is invalid after creation; means that there are no rows |
| // satisfying filtering criteria. Return an empty tree. |
| globals.dispatch(Actions.setPivotStateReduxState({ |
| pivotTableState: { |
| queryId: this.lastStartedQueryId, |
| query: null, |
| queryResult: createEmptyQueryResult(query.metadata), |
| selectionArea: pivotTableState.selectionArea |
| } |
| })); |
| return; |
| } |
| |
| const treeBuilder = new TreeBuilder( |
| query.metadata.pivotColumns.length, |
| query.metadata.aggregationColumns.length, |
| nextRow()); |
| while (it.valid()) { |
| treeBuilder.ingestRow(nextRow()); |
| } |
| |
| globals.dispatch(Actions.setPivotStateReduxState({ |
| pivotTableState: { |
| queryId: this.lastStartedQueryId, |
| query: null, |
| queryResult: { |
| tree: treeBuilder.build(), |
| metadata: query.metadata, |
| }, |
| selectionArea: pivotTableState.selectionArea |
| } |
| })); |
| }); |
| } |
| |
| const selection = globals.state.currentSelection; |
| if (selection !== null && selection.kind === 'AREA') { |
| const enabledArea = globals.state.areas[selection.areaId]; |
| globals.dispatch( |
| Actions.togglePivotTableRedux({selectionArea: enabledArea})); |
| } else { |
| globals.dispatch(Actions.togglePivotTableRedux({selectionArea: null})); |
| } |
| } |
| } |