@finos/legend-data-cube
Version:
360 lines • 16.4 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* 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 { guaranteeNonNullable, isNonNullable, uniq, uniqBy, } from '@finos/legend-shared';
import { DataCubeConfiguration } from '../../core/model/DataCubeConfiguration.js';
import {} from '../../core/DataCubeSnapshot.js';
import { _findCol, _toCol, } from '../../core/model/DataCubeColumn.js';
import { DataCubeSnapshotController } from '../../services/DataCubeSnapshotService.js';
import { DataCubeColumnKind, DataCubeQueryFilterGroupOperator, DataCubeQuerySortDirection, isPivotResultColumnName, getPivotResultColumnBaseColumnName, isDimensionalGridMode, } from '../../core/DataCubeQueryEngine.js';
import { generateDimensionalMenuBuilder, generateMenuBuilder, } from './DataCubeGridMenuBuilder.js';
import { buildFilterEditorTree, buildFilterSnapshot, DataCubeFilterEditorConditionGroupTreeNode, } from '../../core/filter/DataCubeQueryFilterEditorState.js';
import { _pruneExpandedPaths } from '../../core/DataCubeSnapshotBuilderUtils.js';
/**
* This query editor state is responsible for capturing updates to the data cube query
* caused by interactions with the grid which are either not captured by the server-side row model
* datasource, e.g. column pinning, column visibility changes, etc or done programatically via grid
* context menu. Think of this as a companion state for grid editor which bridges the gap between
* ag-grid state and data cube query state.
*
* More technically, this handles interactions that result in instant (not batched) change to the query.
* For example, in the editor, users can make changes to multiple parts of the query, but until they are
* explicit applied, these changes will not impact the query; whereas here a change immediately take effect.
*
* NOTE: since typically, each grid action causes a new snapshot to be created,
* we MUST NEVER use the editor here, as it could potentially create illegal state
* while the editor is still in the middle of a modification that has not been applied.
*/
export class DataCubeGridControllerState extends DataCubeSnapshotController {
view;
constructor(view) {
super(view.engine, view.settingService, view.snapshotService);
this.view = view;
}
configuration = new DataCubeConfiguration();
menuBuilder;
getColumnConfiguration(colName) {
return _findCol(this.configuration.columns, colName);
}
// --------------------------------- FILTER ---------------------------------
filterTree = {
nodes: new Map(),
};
/**
* Add a new filter condition to the root of the filter tree.
* 1. If the root is empty, add a new AND group with the condition as the root
* 2. If the root is an AND group, add the condition to the root
* 3. If the root is an OR group, create a new AND group with the condition and
* wrapping the current root and set that as the new root
*/
addNewFilterCondition(condition) {
if (!this.filterTree.root) {
const root = new DataCubeFilterEditorConditionGroupTreeNode(undefined, DataCubeQueryFilterGroupOperator.AND, undefined);
this.filterTree.nodes.set(root.uuid, root);
this.filterTree.root = root;
root.addChild(condition);
this.filterTree.nodes.set(condition.uuid, condition);
}
else if (this.filterTree.root.operation === DataCubeQueryFilterGroupOperator.AND) {
this.filterTree.root.addChild(condition);
this.filterTree.nodes.set(condition.uuid, condition);
}
else {
// Normally, for this case, we just wrap the current root with a new AND group
// but if the current (OR group) root has only 1 condition (this is only allowed
// if the group is root), we can just simply change the group operator to AND
const currentRoot = this.filterTree.root;
if (currentRoot.children.length === 1) {
currentRoot.operation = DataCubeQueryFilterGroupOperator.AND;
currentRoot.addChild(condition);
this.filterTree.nodes.set(condition.uuid, condition);
}
else {
const newRoot = new DataCubeFilterEditorConditionGroupTreeNode(undefined, DataCubeQueryFilterGroupOperator.AND, undefined);
this.filterTree.nodes.set(newRoot.uuid, newRoot);
this.filterTree.root = newRoot;
newRoot.addChild(currentRoot);
newRoot.addChild(condition);
this.filterTree.nodes.set(condition.uuid, condition);
}
}
this.applyChanges();
}
clearFilters() {
this.filterTree.root = undefined;
this.filterTree.nodes = new Map();
this.applyChanges();
}
// --------------------------------- COLUMNS ---------------------------------
selectColumns = [];
leafExtendedColumns = [];
groupExtendedColumns = [];
pinColumn(colName, placement) {
const columnConfiguration = this.getColumnConfiguration(colName);
if (columnConfiguration) {
columnConfiguration.pinned = placement;
this.applyChanges();
}
}
rearrangeColumns(columns) {
const rearrangedColumnConfigurations = columns
.map((colName) => this.getColumnConfiguration(colName))
.filter(isNonNullable);
this.configuration.columns = [
...rearrangedColumnConfigurations,
...this.configuration.columns.filter((col) => !rearrangedColumnConfigurations.includes(col)),
];
const rearrangedSelectColumns = columns
.map((colName) => _findCol(this.selectColumns, colName))
.filter(isNonNullable);
this.selectColumns = [
...rearrangedSelectColumns,
...rearrangedSelectColumns.filter((col) => !rearrangedSelectColumns.includes(col)),
];
this.applyChanges();
}
removeAllPins() {
this.configuration.columns.forEach((col) => (col.pinned = undefined));
this.applyChanges();
}
showColumn(colName, isVisible) {
const columnConfiguration = this.getColumnConfiguration(colName);
if (columnConfiguration) {
columnConfiguration.hideFromView = !isVisible;
this.applyChanges();
}
}
// --------------------------------- PIVOT ---------------------------------
horizontalPivotColumns = [];
horizontalPivotCastColumns = [];
get horizontalPivotResultColumns() {
return this.horizontalPivotCastColumns
.filter((col) => isPivotResultColumnName(col.name))
.map(_toCol);
}
getHorizontalPivotableColumn(colName) {
return _findCol(this.configuration.columns.filter((col) => col.kind === DataCubeColumnKind.DIMENSION &&
// exclude group-level extended columns
!_findCol(this.groupExtendedColumns, col.name)), colName);
}
setHorizontalPivotOnColumn(colName) {
const column = this.getHorizontalPivotableColumn(colName);
if (column) {
this.horizontalPivotColumns = [column];
this.applyChanges();
}
}
addHorizontalPivotOnColumn(colName) {
const column = this.getHorizontalPivotableColumn(colName);
if (column) {
this.horizontalPivotColumns = [...this.horizontalPivotColumns, column];
this.applyChanges();
}
}
clearAllHorizontalPivots() {
this.horizontalPivotColumns = [];
this.horizontalPivotCastColumns = [];
this.applyChanges();
}
excludeColumnFromHorizontalPivot(colName) {
if (isPivotResultColumnName(colName)) {
const baseColumnName = getPivotResultColumnBaseColumnName(colName);
const columnConfiguration = this.getColumnConfiguration(baseColumnName);
if (columnConfiguration && !columnConfiguration.excludedFromPivot) {
columnConfiguration.excludedFromPivot = true;
this.applyChanges();
}
}
}
includeColumnInHorizontalPivot(colName) {
const columnConfiguration = this.getColumnConfiguration(colName);
if (columnConfiguration?.excludedFromPivot) {
columnConfiguration.excludedFromPivot = false;
this.applyChanges();
}
}
// --------------------------------- GROUP BY ---------------------------------
verticalPivotColumns = [];
getVerticalPivotableColumn(colName) {
return _findCol(this.configuration.columns.filter((col) => col.kind === DataCubeColumnKind.DIMENSION &&
// exclude group-level extended columns
!_findCol(this.groupExtendedColumns, col.name) &&
// exclude pivot columns
!_findCol(this.horizontalPivotColumns, col.name)), colName);
}
setVerticalPivotOnColumn(colName) {
const column = this.getVerticalPivotableColumn(colName);
if (column) {
this.verticalPivotColumns = [column];
this.applyChanges();
}
}
addVerticalPivotOnColumn(colName) {
const column = this.getVerticalPivotableColumn(colName);
if (column) {
this.verticalPivotColumns = [...this.verticalPivotColumns, column];
this.applyChanges();
}
}
removeVerticalPivotOnColumn(colName) {
this.verticalPivotColumns = this.verticalPivotColumns.filter((col) => col.name !== colName);
this.applyChanges();
}
clearAllVerticalPivots() {
this.verticalPivotColumns = [];
this.applyChanges();
}
collapseAllPaths() {
this.view.grid.client.collapseAll();
this.configuration.pivotLayout.expandedPaths = [];
this.applyChanges();
}
expandPath(path) {
this.configuration.pivotLayout.expandedPaths = uniq([
...this.configuration.pivotLayout.expandedPaths,
path,
]).sort();
this.applyChanges();
}
collapsePath(path) {
this.configuration.pivotLayout.expandedPaths =
this.configuration.pivotLayout.expandedPaths.filter((p) => p !== path);
this.applyChanges();
}
// --------------------------------- SORT ---------------------------------
sortColumns = [];
getSortableColumn(colName) {
if (!colName) {
return undefined;
}
return _findCol([
...(this.horizontalPivotCastColumns.length
? this.horizontalPivotCastColumns
: this.selectColumns),
...this.groupExtendedColumns,
], colName);
}
setSortByColumn(colName, direction) {
const column = this.getSortableColumn(colName);
if (!column) {
return;
}
this.sortColumns = [
{
...column,
direction,
},
];
this.applyChanges();
}
addSortByColumn(colName, direction) {
const column = this.getSortableColumn(colName);
if (!column) {
return;
}
this.sortColumns = [...this.sortColumns, { ...column, direction }];
this.applyChanges();
}
clearSortByColumn(colName) {
this.sortColumns = this.sortColumns.filter((col) => col.name !== colName);
this.applyChanges();
}
clearAllSorts() {
this.sortColumns = [];
this.applyChanges();
}
// --------------------------------- MAIN ---------------------------------
getSnapshotSubscriberName() {
return 'grid-controller';
}
async applySnapshot(snapshot, previousSnapshot) {
this.configuration = DataCubeConfiguration.serialization.fromJson(snapshot.data.configuration);
this.filterTree.nodes = new Map();
this.filterTree.root = snapshot.data.filter
? buildFilterEditorTree(snapshot.data.filter, undefined, this.filterTree.nodes, (operator) => this.view.engine.getFilterOperation(operator))
: undefined;
this.selectColumns = snapshot.data.selectColumns;
this.leafExtendedColumns = snapshot.data.leafExtendedColumns;
this.groupExtendedColumns = snapshot.data.groupExtendedColumns;
this.horizontalPivotColumns = snapshot.data.pivot?.columns ?? [];
this.horizontalPivotCastColumns = snapshot.data.pivot?.castColumns ?? [];
this.verticalPivotColumns = snapshot.data.groupBy?.columns ?? [];
this.sortColumns = snapshot.data.sortColumns;
this.menuBuilder = isDimensionalGridMode(this.view.info.configuration.gridMode)
? generateDimensionalMenuBuilder(this)
: generateMenuBuilder(this);
}
propagateChanges(baseSnapshot) {
this.verticalPivotColumns = this.verticalPivotColumns.filter((col) => !_findCol(this.horizontalPivotColumns, col.name));
this.configuration.pivotLayout.expandedPaths = _pruneExpandedPaths(baseSnapshot.data.groupBy?.columns ?? [], this.verticalPivotColumns, this.configuration.pivotLayout.expandedPaths);
this.configuration.columns.forEach((col) => {
col.pivotSortDirection = _findCol(this.horizontalPivotColumns, col.name)
? (col.pivotSortDirection ?? DataCubeQuerySortDirection.ASCENDING)
: undefined;
});
this.selectColumns = uniqBy([
...this.configuration.columns.filter((col) => col.isSelected && !_findCol(this.groupExtendedColumns, col.name)),
...this.horizontalPivotColumns,
...this.verticalPivotColumns,
], (col) => col.name).map(_toCol);
const sortableColumns = uniqBy([
// if pivot is active, take the pivot result columns and include
// selected dimension columns which are not part of pivot columns
...(this.horizontalPivotColumns.length
? [
...this.horizontalPivotResultColumns,
...[
...this.configuration.columns.filter((col) => col.isSelected),
...this.verticalPivotColumns,
].filter((column) => this.getColumnConfiguration(column.name)?.kind ===
DataCubeColumnKind.DIMENSION &&
!_findCol(this.horizontalPivotColumns, column.name)),
]
: [
...this.configuration.columns.filter((col) => col.isSelected),
...this.verticalPivotColumns,
]),
...this.groupExtendedColumns,
], (col) => col.name);
this.sortColumns = this.sortColumns.filter((col) => _findCol(sortableColumns, col.name));
}
applyChanges() {
const baseSnapshot = guaranteeNonNullable(this.getLatestSnapshot());
const snapshot = baseSnapshot.clone();
this.propagateChanges(baseSnapshot);
snapshot.data.configuration = this.configuration.serialize();
snapshot.data.filter = this.filterTree.root
? buildFilterSnapshot(this.filterTree.root)
: undefined;
snapshot.data.selectColumns = this.selectColumns;
snapshot.data.pivot = this.horizontalPivotColumns.length
? {
columns: this.horizontalPivotColumns,
castColumns: this.horizontalPivotCastColumns,
}
: undefined;
snapshot.data.groupBy = this.verticalPivotColumns.length
? {
columns: this.verticalPivotColumns,
}
: undefined;
snapshot.data.sortColumns = this.sortColumns;
snapshot.finalize();
if (snapshot.hashCode !== baseSnapshot.hashCode) {
this.publishSnapshot(snapshot);
}
}
}
//# sourceMappingURL=DataCubeGridControllerState.js.map