1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {Actions} from '../common/actions'; 18import {Engine} from '../common/engine'; 19import {featureFlags} from '../common/feature_flags'; 20import {ColumnType} from '../common/query_result'; 21import { 22 PivotTableReduxQueryMetadata, 23 PivotTableReduxResult 24} from '../common/state'; 25import {aggregationIndex} from '../frontend/pivot_table_redux_query_generator'; 26 27import {Controller} from './controller'; 28import {globals} from './globals'; 29 30export const PIVOT_TABLE_REDUX_FLAG = featureFlags.register({ 31 id: 'pivotTableRedux', 32 name: 'Pivot tables V2', 33 description: 'Second version of pivot table', 34 defaultValue: false, 35}); 36 37// Node in the hierarchical pivot tree. Only leaf nodes contain data from the 38// query result. 39export interface PivotTree { 40 // Whether the node should be collapsed in the UI, false by default and can 41 // be toggled with the button. 42 isCollapsed: boolean; 43 44 // Non-empty only in internal nodes. 45 children: Map<ColumnType, PivotTree>; 46 aggregates: ColumnType[]; 47 48 // Non-empty only in leaf nodes. 49 rows: ColumnType[][]; 50} 51 52// Auxiliary class to build the tree from query response. 53class TreeBuilder { 54 private readonly root: PivotTree; 55 lastRow: ColumnType[]; 56 pivotColumns: number; 57 aggregateColumns: number; 58 59 constructor( 60 pivotColumns: number, aggregateColumns: number, firstRow: ColumnType[]) { 61 this.pivotColumns = pivotColumns; 62 this.aggregateColumns = aggregateColumns; 63 this.root = this.createNode(0, firstRow); 64 let tree = this.root; 65 for (let i = 0; i + 1 < this.pivotColumns; i++) { 66 const value = firstRow[i]; 67 tree = TreeBuilder.insertChild( 68 tree, value, this.createNode(i + 1, firstRow)); 69 } 70 this.lastRow = firstRow; 71 } 72 73 // Add incoming row to the tree being built. 74 ingestRow(row: ColumnType[]) { 75 let tree = this.root; 76 for (let i = 0; i + 1 < this.pivotColumns; i++) { 77 const nextTree = tree.children.get(row[i]); 78 if (nextTree === undefined) { 79 // Insert the new node into the tree, and make variable `tree` point 80 // to the newly created node. 81 tree = 82 TreeBuilder.insertChild(tree, row[i], this.createNode(i + 1, row)); 83 } else { 84 tree = nextTree; 85 } 86 } 87 tree.rows.push(row); 88 this.lastRow = row; 89 } 90 91 build(): PivotTree { 92 return this.root; 93 } 94 95 // Helper method that inserts child node into the tree and returns it, used 96 // for more concise modification of local variable pointing to the current 97 // node being built. 98 static insertChild(tree: PivotTree, key: ColumnType, child: PivotTree): 99 PivotTree { 100 tree.children.set(key, child); 101 return child; 102 } 103 104 // Initialize PivotTree from a row. 105 createNode(depth: number, row: ColumnType[]): PivotTree { 106 const aggregates = []; 107 108 for (let j = 0; j < this.aggregateColumns; j++) { 109 aggregates.push(row[aggregationIndex(this.pivotColumns, j, depth)]); 110 } 111 112 return { 113 isCollapsed: false, 114 children: new Map(), 115 aggregates, 116 rows: [], 117 }; 118 } 119} 120 121function createEmptyQueryResult(metadata: PivotTableReduxQueryMetadata): 122 PivotTableReduxResult { 123 return { 124 tree: { 125 aggregates: [], 126 isCollapsed: false, 127 children: new Map(), 128 rows: [], 129 }, 130 metadata 131 }; 132} 133 134 135// Controller responsible for showing the panel with pivot table, as well as 136// executing its queries and post-processing query results. 137export class PivotTableReduxController extends Controller<{}> { 138 engine: Engine; 139 lastStartedQueryId: number; 140 141 constructor(args: {engine: Engine}) { 142 super({}); 143 this.engine = args.engine; 144 this.lastStartedQueryId = 0; 145 } 146 147 run() { 148 if (!PIVOT_TABLE_REDUX_FLAG.get()) { 149 return; 150 } 151 152 const pivotTableState = globals.state.pivotTableRedux; 153 if (pivotTableState.queryId > this.lastStartedQueryId && 154 pivotTableState.query !== null) { 155 this.lastStartedQueryId = pivotTableState.queryId; 156 const query = pivotTableState.query; 157 158 this.engine.query(query.text).then(async (result) => { 159 try { 160 await result.waitAllRows(); 161 } catch { 162 // waitAllRows() frequently throws an exception, which is ignored in 163 // its other calls, so it's ignored here as well. 164 } 165 166 const columns = result.columns(); 167 168 const it = result.iter({}); 169 function nextRow(): ColumnType[] { 170 const row: ColumnType[] = []; 171 for (const column of columns) { 172 row.push(it.get(column)); 173 } 174 it.next(); 175 return row; 176 } 177 178 if (!it.valid()) { 179 // Iterator is invalid after creation; means that there are no rows 180 // satisfying filtering criteria. Return an empty tree. 181 globals.dispatch(Actions.setPivotStateReduxState({ 182 pivotTableState: { 183 queryId: this.lastStartedQueryId, 184 query: null, 185 queryResult: createEmptyQueryResult(query.metadata), 186 selectionArea: pivotTableState.selectionArea 187 } 188 })); 189 return; 190 } 191 192 const treeBuilder = new TreeBuilder( 193 query.metadata.pivotColumns.length, 194 query.metadata.aggregationColumns.length, 195 nextRow()); 196 while (it.valid()) { 197 treeBuilder.ingestRow(nextRow()); 198 } 199 200 globals.dispatch(Actions.setPivotStateReduxState({ 201 pivotTableState: { 202 queryId: this.lastStartedQueryId, 203 query: null, 204 queryResult: { 205 tree: treeBuilder.build(), 206 metadata: query.metadata, 207 }, 208 selectionArea: pivotTableState.selectionArea 209 } 210 })); 211 }); 212 } 213 214 const selection = globals.state.currentSelection; 215 if (selection !== null && selection.kind === 'AREA') { 216 const enabledArea = globals.state.areas[selection.areaId]; 217 globals.dispatch( 218 Actions.togglePivotTableRedux({selectionArea: enabledArea})); 219 } else { 220 globals.dispatch(Actions.togglePivotTableRedux({selectionArea: null})); 221 } 222 } 223}