@finos/legend-data-cube
Version:
646 lines • 61.3 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* 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 { observer } from 'mobx-react-lite';
import { cn, DataCubeIcon } from '@finos/legend-art';
import { AllCommunityModule, } from 'ag-grid-community';
import { useCallback, useEffect, useRef, useState } from 'react';
import { AgGridReact, } from 'ag-grid-react';
import { filterByType, isNonNullable } from '@finos/legend-shared';
import { getAllNodes, getDataForAllFilteredNodes, } from '../../../stores/view/grid/DataCubeGridClientEngine.js';
import { FormAlert, FormBadge_WIP, FormCheckbox, } from '../../core/DataCubeFormUtils.js';
import { DataCubeGridMode } from '../../../stores/core/DataCubeQueryEngine.js';
import { DataCubeEditorColumnsSelectorColumnState } from '../../../stores/view/editor/DataCubeEditorColumnsSelectorState.js';
import { getColumnsSelectorBaseGridProps, INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT, } from './DataCubeEditorColumnsSelector.js';
import { DataCubeEditorDimensionState, } from '../../../stores/view/editor/DataCubeEditorDimensionsPanelState.js';
import { TreeDataModule } from 'ag-grid-enterprise';
import { _findCol } from '../../../stores/core/model/DataCubeColumn.js';
import { AlertType } from '../../../stores/services/DataCubeAlertService.js';
import { DEFAULT_ALERT_WINDOW_CONFIG } from '../../../stores/services/DataCubeLayoutService.js';
// NOTE: This is a workaround to prevent ag-grid license key check from flooding the console screen
// with its stack trace in Chrome.
// We MUST NEVER completely surpress this warning in production, else it's a violation of the ag-grid license!
// See https://www.ag-grid.com/react-data-grid/licensing/
const __INTERNAL__original_console_error = console.error; // eslint-disable-line no-console
/**
* Move this display to a separate component to avoid re-rendering the header too frequently
*/
const AvailableColumnsSearchResultCountBadge = observer((props) => {
const { panel, gridApi } = props;
return (_jsxs("div", { className: "flex items-center justify-center rounded-lg bg-neutral-500 px-1 py-0.5 font-mono text-xs font-bold text-white", children: [`${getDataForAllFilteredNodes(gridApi).length}/${panel.availableColumnsForDisplay.length}`, _jsx("span", { className: "hidden", children: panel.availableColumnsSearchText })] }));
});
/**
* Move this display to a separate component to avoid re-rendering the header too frequently
*/
const DimensionsTreeSearchResultCountBadge = observer((props) => {
const { panel, gridApi } = props;
return (_jsxs("div", { className: "flex items-center justify-center rounded-lg bg-neutral-500 px-1 py-0.5 font-mono text-xs font-bold text-white", children: [`${getDataForAllFilteredNodes(gridApi)
.map((node) => node.data)
.filter((node) => node instanceof DataCubeEditorDimensionState)
.length}/${panel.dimensions.length}`, _jsx("span", { className: "hidden", children: panel.dimensionsTreeSearchText })] }));
});
/**
* Move this display to a separate component to avoid re-rendering too frequently
*/
const DimensionLabel = observer((props) => {
const { panel, dimension, gridApi } = props;
const nameInputRef = useRef(null);
const [name, setName] = useState(dimension.name);
const validationError = name.trim() === ''
? `Name is required`
: Boolean(panel.dimensions
.filter((dim) => dim.name !== dimension.name)
.find((dim) => dim.name === name))
? `Name must be unique`
: undefined;
const isNameValid = !validationError;
const updateName = () => {
if (!isNameValid) {
return;
}
dimension.setName(name);
dimension.setIsRenaming(false);
gridApi.clearFocusedCell();
};
useEffect(() => {
setName(dimension.name);
}, [dimension.name]);
useEffect(() => {
if (dimension.isRenaming) {
nameInputRef.current?.focus();
}
}, [dimension.isRenaming]);
return (_jsxs("div", { className: "flex h-full w-full items-center pl-1", title: `[Dimension: ${dimension.name}]\nDouble-click to remove all columns`, children: [_jsx("div", { className: "text-2xs flex h-3 flex-shrink-0 items-center justify-center rounded-sm bg-neutral-500 px-1 font-bold text-white", children: "DIM" }), !dimension.isRenaming && (_jsx("div", { className: "h-full flex-1 cursor-pointer items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-1", title: "Double-click to remove dimension",
/**
* ag-grid row select event listener is at a deeper layer so we need to stop
* the propagation as event capturing is happening, not when it's bubbling.
*/
onDoubleClickCapture: (event) => {
event.stopPropagation();
dimension.setIsRenaming(true);
gridApi.clearFocusedCell();
}, children: dimension.name })), dimension.isRenaming && (_jsxs("div", { className: "relative flex items-center",
/**
* ag-grid row select event listener is at a deeper layer so we need to stop
* the propagation as event capturing is happening, not when it's bubbling.
*/
onDoubleClickCapture: (event) => {
event.stopPropagation();
gridApi.clearFocusedCell();
}, children: [_jsx("input", { ref: nameInputRef, className: cn('ml-0.5 h-4 border border-neutral-300 pl-[1px] pr-6 focus:border-sky-600 focus:!outline-none', {
'border-red-600 focus:border-red-600': !isNameValid,
}), value: name, onChange: (event) => {
setName(event.target.value);
}, onKeyDown: (event) => {
if (event.code === 'Escape') {
nameInputRef.current?.focus();
nameInputRef.current?.select();
}
}, onKeyDownCapture: (event) => {
if (event.code === 'Enter') {
event.stopPropagation();
updateName();
}
}, onBlur: () => {
dimension.setIsRenaming(false);
setName(dimension.name);
} }), isNameValid && (_jsx("button", { className: "text-2xs absolute right-0.5 flex h-3 w-5 items-center justify-center rounded-sm bg-sky-600 text-white", onClick: (event) => {
event.stopPropagation();
updateName();
}, children: "Save" }))] }))] }));
});
function confirmPopulateStubDimensionWhenAddingColumns(view, handler) {
view.alertService.alert({
message: `No target dimensions to add columns to`,
text: `Columns need to be added to a dimension, but no dimension has been specified.\nDo you want to proceed with creating a new dimension and add the columns to it?`,
type: AlertType.WARNING,
actions: [
{
label: 'Proceed',
handler,
},
{
label: 'Cancel',
handler: () => { },
},
],
windowConfig: {
...DEFAULT_ALERT_WINDOW_CONFIG,
width: 550,
height: 180,
},
});
}
function computeDragOverNode(event) {
let overIndex = event.overIndex === -1 ? undefined : event.overIndex;
// if the drag point passes the middle of the row, we consider
// the spot to be the one before the row
if (overIndex !== undefined &&
event.y % INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT <
INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT / 2) {
overIndex -= 1;
}
return {
overIndex,
overNode: (overIndex !== undefined
? getAllNodes(event.api).at(overIndex === -1 ? 0 : overIndex)
: undefined),
};
}
export const DataCubeEditorDimensionsPanel = observer((props) => {
const { view } = props;
const editor = view.editor;
const panel = editor.dimensions;
const searchAvailableColumnsInputRef = useRef(null);
const searchDimensionsTreeInputRef = useRef(null);
const [selectedAvailableColumns, setSelectedAvailableColumns] = useState([]);
const [selectedSelectedColumns, setSelectedSelectedColumns] = useState([]);
const [selectedDimensions, setSelectedDimensions] = useState([]);
const [availableColumnsGridApi, setAvailableColumnsGridApi] = useState(null);
const [dimensionsTreeGridApi, setDimensionsTreeGridApi] = useState(null);
const onAvailableColumnsExternalDragStop = useCallback((params) => {
// NOTE: here, we do not scope the columns being moved by the search
// this is a complicated behavior to implement and it's not clear if
// it's necessary, the current behavior is sensible in its own way.
const columnsToMove = params.nodes
.map((node) => node.data?.data)
.filter(filterByType(DataCubeEditorColumnsSelectorColumnState));
panel.deselectColumns(columnsToMove);
panel.refreshDimensionsTreeData();
}, [panel]);
const onDimensionsTreeExternalDragStop = useCallback((event) => {
const { overNode } = computeDragOverNode(event);
// NOTE: here, we do not scope the columns being moved by the search
// this is a complicated behavior to implement and it's not clear if
// it's necessary, the current behavior is sensible in its own way.
const columnsToMove = event.nodes
.map((node) => node.data)
.filter(isNonNullable);
const dimension = (!overNode?.data
? undefined
: overNode.data.data instanceof DataCubeEditorDimensionState
? overNode.data.data
: overNode.parent?.data?.data instanceof
DataCubeEditorDimensionState
? overNode.parent.data.data
: undefined) ?? panel.dimensions.at(-1);
if (dimension) {
// dropping position will be honored and affect columns
// ordering within the dimension accordingly
const colIdx = overNode?.data?.data instanceof
DataCubeEditorColumnsSelectorColumnState
? dimension.columns.indexOf(overNode.data.data)
: overNode?.data?.data instanceof DataCubeEditorDimensionState
? -1
: undefined;
dimension.setColumns(colIdx !== undefined
? [
...dimension.columns.slice(0, colIdx + 1),
...columnsToMove,
...dimension.columns.slice(colIdx + 1),
]
: [...dimension.columns, ...columnsToMove]);
panel.refreshDimensionsTreeData();
}
else {
confirmPopulateStubDimensionWhenAddingColumns(view, () => {
const newDimension = panel.newDimension();
newDimension.setColumns([...columnsToMove]);
panel.refreshDimensionsTreeData();
});
}
}, [panel, view]);
/**
* Setup drop zones for each grid to allow moving columns between them
* See https://www.ag-grid.com/react-data-grid/row-dragging-to-grid/
*/
useEffect(() => {
if (!availableColumnsGridApi || !dimensionsTreeGridApi) {
return;
}
const dimensionstreeDropZoneParams = !dimensionsTreeGridApi.isDestroyed()
? dimensionsTreeGridApi.getRowDropZoneParams({
onDragStop: (event) => {
onDimensionsTreeExternalDragStop(event);
availableColumnsGridApi.clearFocusedCell();
},
})
: undefined;
if (dimensionstreeDropZoneParams &&
!availableColumnsGridApi.isDestroyed()) {
availableColumnsGridApi.addRowDropZone(dimensionstreeDropZoneParams);
}
const availableColumnsDropZoneParams = !availableColumnsGridApi.isDestroyed()
? availableColumnsGridApi.getRowDropZoneParams({
onDragStop: (event) => {
onAvailableColumnsExternalDragStop(event);
dimensionsTreeGridApi.clearFocusedCell();
},
})
: undefined;
if (availableColumnsDropZoneParams &&
!dimensionsTreeGridApi.isDestroyed()) {
dimensionsTreeGridApi.addRowDropZone(availableColumnsDropZoneParams);
}
}, [
availableColumnsGridApi,
dimensionsTreeGridApi,
onDimensionsTreeExternalDragStop,
onAvailableColumnsExternalDragStop,
]);
const [dimensionsTreeOnHoverIndex, setDimensionsTreeOnHoverIndex] = useState(undefined);
// This event will be triggered when user drops any node on the dimensions tree,
// regardless of the source, i.e. this includes columns from available columns grid
// so we have to guard against that case because the logic should already been handled
// by the drag stop hook in the external drop zone config for dimensions tree.
const isDimensionsTreeRowDragEnabled = selectedDimensions.length === 0 || selectedSelectedColumns.length === 0;
const onDimensionsTreeRowDragEnd = useCallback((event) => {
const { overIndex, overNode } = computeDragOverNode(event);
setDimensionsTreeOnHoverIndex(undefined);
// NOTE: here, we do not scope the columns being moved by the search
// this is a complicated behavior to implement and it's not clear if
// it's necessary, the current behavior is sensible in its own way.
const dragEntities = event.nodes
.map((node) =>
// this assertion is valid because we also need to handle columns being dragged from external sources
node.data)
.filter((data) => !(data instanceof DataCubeEditorDimensionState));
// we guard against DnD from the available columns grid to the dimensions tree
if (dragEntities.length === 0) {
return;
}
const dragColumns = [];
const dragDimensions = [];
dragEntities.forEach((node) => {
if (node.data instanceof DataCubeEditorDimensionState) {
dragDimensions.push(node.data);
}
else if (node.data instanceof DataCubeEditorColumnsSelectorColumnState) {
dragColumns.push(node.data);
}
});
if (dragColumns.length > 0 && dragDimensions.length > 0) {
// we don't support DnD for a mixture of columns and dimensions
return;
}
// moving dimensions
if (dragDimensions.length > 0) {
// dropping position will be honored and affect dimensions ordering
let dimensionIdx = undefined;
if (overIndex === -1) {
dimensionIdx = -1;
}
else if (overNode !== undefined) {
const dimension = !overNode.data
? undefined
: overNode.data.data instanceof DataCubeEditorDimensionState
? overNode.data.data
: overNode.parent?.data?.data instanceof
DataCubeEditorDimensionState
? overNode.parent.data.data
: undefined;
if (dimension) {
const _idx = panel.dimensions.indexOf(dimension);
// when compute the move position, account for dimensions which will be moved out
// these would shift the move position up.
dimensionIdx =
_idx === -1
? -1
: _idx -
panel.dimensions
.slice(0, _idx + 1)
.filter((dim) => dragDimensions.find((_dim) => dim.name === _dim.name)).length;
}
}
const dimensions = panel.dimensions.filter((dim) => !dragDimensions.find((_dim) => dim.name === _dim.name));
panel.setDimensions(dimensionIdx !== undefined
? [
...dimensions.slice(0, dimensionIdx + 1),
...dragDimensions,
...dimensions.slice(dimensionIdx + 1),
]
: [...dimensions, ...dragDimensions]);
panel.refreshDimensionsTreeData();
return;
}
// moving columns
if (dragColumns.length > 0) {
const dimension = (!overNode?.data
? undefined
: overNode.data.data instanceof DataCubeEditorDimensionState
? overNode.data.data
: overNode.parent?.data?.data instanceof
DataCubeEditorDimensionState
? overNode.parent.data.data
: undefined) ?? panel.dimensions.at(-1);
if (dimension) {
// dropping position will be honored and affect columns
// ordering within the dimension accordingly
let colIdx = undefined;
if (overNode?.data?.data instanceof DataCubeEditorDimensionState) {
colIdx = -1;
}
else if (overNode?.data?.data instanceof
DataCubeEditorColumnsSelectorColumnState) {
const _idx = dimension.columns.indexOf(overNode.data.data);
// when compute the move position, account for columns which will be moved out
// these would shift the move position up.
colIdx =
_idx === -1
? -1
: _idx -
dimension.columns
.slice(0, _idx + 1)
.filter((col) => _findCol(dragColumns, col.name)).length;
}
panel.deselectColumns(dragColumns);
dimension.setColumns(colIdx !== undefined
? [
...dimension.columns.slice(0, colIdx + 1),
...dragColumns,
...dimension.columns.slice(colIdx + 1),
]
: [...dimension.columns, ...dragColumns]);
panel.refreshDimensionsTreeData();
}
return;
}
}, [panel]);
const onDimensionsTreeRowDragMove = useCallback((event) => {
const { overIndex, overNode } = computeDragOverNode(event);
setDimensionsTreeOnHoverIndex(overIndex);
event.api.refreshCells({
rowNodes: overNode ? [overNode] : [],
force: true, // since no data change is happening, we need to force refresh
});
}, []);
const onDimensionsTreeRowDragCancel = useCallback((event) => {
setDimensionsTreeOnHoverIndex(undefined);
}, []);
const onDimensionsTreeRowDragLeave = useCallback((event) => {
setDimensionsTreeOnHoverIndex(undefined);
}, []);
useEffect(() => {
// on load, reset all renaming state for all dimensions
panel.dimensions.forEach((dimension) => dimension.setIsRenaming(false));
// reset transient grid states
setDimensionsTreeOnHoverIndex(undefined);
}, [panel]);
// eslint-disable-next-line no-process-env
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.error = (message, ...agrs) => {
console.debug(`%c ${message}`, 'color: silver'); // eslint-disable-line no-console
};
}
return (_jsxs("div", { className: "h-full w-full select-none p-2", children: [_jsxs("div", { className: "flex h-6", children: [_jsx("div", { className: "flex h-6 items-center text-xl font-medium", children: _jsx(DataCubeIcon.TableDimension, {}) }), _jsxs("div", { className: "ml-1 flex h-6 items-center text-xl font-medium", children: ["Dimensions", _jsx(FormBadge_WIP, {})] })] }), _jsxs("div", { className: "h-[calc(100%_-_24px)] w-full", children: [_jsx("div", { className: "h-10 py-2", children: _jsx("div", { className: "flex h-6 w-full items-center", children: _jsx(FormCheckbox, { label: "Enable multidimensional grid mode", checked: editor.generalProperties.configuration.gridMode ===
DataCubeGridMode.MULTIDIMENSIONAL, onChange: () => {
if (editor.generalProperties.configuration.gridMode ===
DataCubeGridMode.MULTIDIMENSIONAL) {
editor.generalProperties.configuration.setGridMode(DataCubeGridMode.STANDARD);
}
else {
if (
// only suggest converting vertical pivots if no dimensions have been specified
editor.verticalPivots.selector.selectedColumns.length !==
0 &&
panel.dimensions.length === 0) {
view.alertService.alert({
message: `Specified vertical pivots will be ignored in multidimensional grid mode.`,
text: `Multidimensional grid mode will ignore any specified vertical pivots, essentially, vertical pivot columns can be considered as one dimension.\nFor convenience, these vertical pivots can be converted into a dimension, do you want this conversion to happen?`,
type: AlertType.WARNING,
actions: [
{
label: 'Yes',
handler: () => {
const groupByColumns = editor.verticalPivots.selector.selectedColumns;
const newDimension = panel.newDimension();
newDimension.setColumns(panel.availableColumns.filter((column) => _findCol(groupByColumns, column.name)));
editor.verticalPivots.selector.setSelectedColumns([]);
panel.refreshDimensionsTreeData();
editor.generalProperties.configuration.setGridMode(DataCubeGridMode.MULTIDIMENSIONAL);
},
},
{
label: 'No',
handler: () => {
editor.generalProperties.configuration.setGridMode(DataCubeGridMode.MULTIDIMENSIONAL);
},
},
],
windowConfig: {
...DEFAULT_ALERT_WINDOW_CONFIG,
width: 550,
height: 220,
},
});
}
else {
editor.generalProperties.configuration.setGridMode(DataCubeGridMode.MULTIDIMENSIONAL);
}
}
} }) }) }), editor.generalProperties.configuration.gridMode !==
DataCubeGridMode.MULTIDIMENSIONAL && (_jsx(FormAlert, { message: "Multidimensional grid mode is experimental", type: AlertType.WARNING, text: `This is an alternative grid view for DataCube with a different mode of interaction: dimensions (ordered list of columns to group by) are defined and will be used to explore data "top-down". Initially, the grid will show the root aggregation, then user can drill down into each dimension. This grid mode comes with the following limitations:
- Specified vertical pivots will be ignored: essentially, vertical pivot can be considered as one dimension in this mode
- No support for pagination
- Initial view can take longer to materialize if drilldown is deep` })), editor.generalProperties.configuration.gridMode ===
DataCubeGridMode.MULTIDIMENSIONAL && (_jsx("div", { className: "flex h-[calc(100%_-_40px)] w-full", children: _jsxs("div", { className: "data-cube-column-selector flex h-full w-full", children: [_jsxs("div", { className: "h-full w-[calc(50%_-_20px)]", children: [_jsx("div", { className: "flex h-5 items-center text-sm", children: "Available columns:" }), _jsxs("div", { className: "h-[calc(100%_-_20px)] rounded-sm border border-neutral-200", children: [_jsxs("div", { className: "relative h-6 border-b border-neutral-200", children: [_jsx("input", { className: "h-full w-full pl-10 pr-6 placeholder-neutral-400", ref: searchAvailableColumnsInputRef, placeholder: "Search columns...", value: panel.availableColumnsSearchText, onChange: (event) => panel.setAvailableColumnsSearchText(event.target.value), onKeyDown: (event) => {
if (event.code === 'Escape') {
event.stopPropagation();
searchAvailableColumnsInputRef.current?.select();
panel.setAvailableColumnsSearchText('');
}
} }), _jsx("div", { className: "absolute left-0 top-0 flex h-6 w-10 items-center justify-center", children: _jsx(DataCubeIcon.Search, { className: "stroke-[3px] text-lg text-neutral-500" }) }), _jsx("button", { className: "absolute right-0 top-0 flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", disabled: !panel.availableColumnsSearchText, title: "Clear search [Esc]", onClick: () => {
panel.setAvailableColumnsSearchText('');
searchAvailableColumnsInputRef.current?.focus();
}, children: _jsx(DataCubeIcon.X, { className: "text-lg" }) })] }), _jsx("div", { className: "h-[calc(100%_-_24px)]", children: _jsx(AgGridReact, { ...getColumnsSelectorBaseGridProps(),
// Disable managed row-dragging to disallow changing the order of columns
// and to make sure the row data and the available columns state are in sync
rowDragManaged: false, onGridReady: (params) => setAvailableColumnsGridApi(params.api), onSelectionChanged: (event) => {
setSelectedAvailableColumns(event.api
.getSelectedNodes()
.map((node) => node.data)
.filter(isNonNullable));
},
// Using ag-grid quick filter is a cheap way to implement search
quickFilterText: panel.availableColumnsSearchText, rowData: panel.availableColumnsForDisplay, rowClass: "border-t border-b border-1 border-transparent", columnDefs: [
{
field: 'name',
colId: 'name',
flex: 1,
minWidth: 100,
filter: true,
sortable: false,
resizable: false,
suppressHeaderMenuButton: true,
getQuickFilterText: (params) => params.value,
/**
* Support double-click to add all (filtered by search) columns
*/
headerComponent: (params) => (_jsxs("button", { title: "Double-click to add all columns", className: "flex h-full w-full items-center justify-between pl-0.5", onDoubleClick: () => {
// The columns being moved are scoped by the current search
const filteredNodes = getDataForAllFilteredNodes(params.api);
const dimension = panel.dimensions.at(-1);
if (dimension) {
dimension.setColumns([
...dimension.columns,
...filteredNodes,
]);
panel.refreshDimensionsTreeData();
}
else {
confirmPopulateStubDimensionWhenAddingColumns(view, () => {
const newDimension = panel.newDimension();
newDimension.setColumns([
...filteredNodes,
]);
panel.refreshDimensionsTreeData();
});
}
params.api.clearFocusedCell();
}, children: [_jsx("div", { children: `[All Columns]` }), _jsx(AvailableColumnsSearchResultCountBadge, { panel: panel, gridApi: params.api })] })),
cellRenderer: (params) => {
const data = params.data;
if (!data) {
return null;
}
return (_jsx("div", { className: "flex h-full w-full cursor-pointer items-center", title: `[${data.name}]\nDouble-click to add column`, onDoubleClick: () => {
const dimension = panel.dimensions.at(-1);
if (dimension) {
dimension.setColumns([
...dimension.columns,
data,
]);
panel.refreshDimensionsTreeData();
}
else {
confirmPopulateStubDimensionWhenAddingColumns(view, () => {
const newDimension = panel.newDimension();
newDimension.setColumns([data]);
panel.refreshDimensionsTreeData();
});
}
params.api.clearFocusedCell();
}, children: _jsx("div", { className: "h-full flex-1 items-center overflow-hidden overflow-ellipsis whitespace-nowrap pl-2", children: data.name }) }));
},
},
] }) })] })] }), _jsx("div", { className: "flex h-full w-10 items-center justify-center", children: _jsxs("div", { className: "flex flex-col", children: [_jsx("button", { className: "flex items-center justify-center rounded-sm border border-neutral-300 bg-neutral-100 text-neutral-500 hover:bg-neutral-200 disabled:bg-neutral-200 disabled:text-neutral-400", title: "Add selected column(s)",
/**
* Support add selected (filtered by search) columns
* We reset the selection after this operation
*/
onClick: () => {
if (!availableColumnsGridApi ||
selectedAvailableColumns.length === 0) {
return;
}
// The columns being moved are scoped by the current search
const filteredNodes = getDataForAllFilteredNodes(availableColumnsGridApi);
const columnsToMove = selectedAvailableColumns.filter((column) => _findCol(filteredNodes, column.name));
const dimension = panel.dimensions.at(-1);
if (dimension) {
dimension.setColumns([
...dimension.columns,
...columnsToMove,
]);
panel.refreshDimensionsTreeData();
}
else {
confirmPopulateStubDimensionWhenAddingColumns(view, () => {
const newDimension = panel.newDimension();
newDimension.setColumns([...columnsToMove]);
panel.refreshDimensionsTreeData();
});
}
availableColumnsGridApi.clearFocusedCell();
}, disabled: !availableColumnsGridApi ||
selectedAvailableColumns.length === 0, children: _jsx(DataCubeIcon.ChevronRight, { className: "text-2xl" }) }), _jsx("button", { className: "mt-2 flex items-center justify-center rounded-sm border border-neutral-300 bg-neutral-100 text-neutral-500 hover:bg-neutral-200 disabled:bg-neutral-200 disabled:text-neutral-400", title: "Remove selected column(s)",
/**
* Support remove selected (filtered by search) columns
* We reset the selection after this operation
*/
onClick: () => {
if (!dimensionsTreeGridApi ||
selectedSelectedColumns.length === 0) {
return;
}
// The columns being moved are scoped by the current search
const filteredNodes = getDataForAllFilteredNodes(dimensionsTreeGridApi);
const columnsToMove = selectedSelectedColumns.filter((column) => _findCol(filteredNodes, column.name));
panel.deselectColumns(columnsToMove);
panel.refreshDimensionsTreeData();
dimensionsTreeGridApi.clearFocusedCell();
}, disabled: !dimensionsTreeGridApi ||
selectedSelectedColumns.length === 0, children: _jsx(DataCubeIcon.ChevronLeft, { className: "text-2xl" }) })] }) }), _jsxs("div", { className: "data-cube-tree-selector h-full w-[calc(50%_-_20px)]", children: [_jsx("div", { className: "flex h-5 items-center text-sm", children: "Dimensions:" }), _jsxs("div", { className: "h-[calc(100%_-_20px)] rounded-sm border border-neutral-200", children: [_jsxs("div", { className: "flex h-6", children: [_jsxs("div", { className: "relative h-6 w-[calc(100%_-_124px)] border-b border-r border-neutral-200", children: [_jsx("input", { className: "h-full w-full pl-10 pr-6 placeholder-neutral-400", ref: searchDimensionsTreeInputRef, placeholder: "Search...", value: panel.dimensionsTreeSearchText, onChange: (event) => panel.setDimensionsTreeSearchText(event.target.value), onKeyDown: (event) => {
if (event.code === 'Escape') {
event.stopPropagation();
searchDimensionsTreeInputRef.current?.select();
panel.setDimensionsTreeSearchText('');
}
} }), _jsx("div", { className: "absolute left-0 top-0 flex h-6 w-10 items-center justify-center", children: _jsx(DataCubeIcon.Search, { className: "stroke-[3px] text-lg text-neutral-500" }) }), _jsx("button", { className: "absolute right-0 top-0 flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", disabled: !panel.dimensionsTreeSearchText, title: "Clear search [Esc]", onClick: () => {
panel.setDimensionsTreeSearchText('');
searchDimensionsTreeInputRef.current?.focus();
}, children: _jsx(DataCubeIcon.X, { className: "text-lg" }) })] }), _jsxs("div", { className: "flex h-6 items-center border-b border-neutral-200", children: [_jsx("button", { className: "flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", title: "Add new dimension", onClick: () => {
panel.newDimension();
panel.refreshDimensionsTreeData();
}, children: _jsx(DataCubeIcon.Plus, { className: "text-lg" }) }), _jsx("div", { className: "h-3 w-[1px] bg-neutral-200" }), _jsx("button", { className: "flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", title: "Move dimension(s) up", disabled: selectedDimensions.length === 0, onClick: () => {
if (selectedDimensions.length === 0) {
return;
}
const minIdx = Math.min(...selectedDimensions.map((dimension) => panel.dimensions.indexOf(dimension)));
const precedings = [
...panel.dimensions.slice(0, Math.max(minIdx - 1, 0)),
// ensure moving dimensions maintain their relative ordering
...panel.dimensions
.map((dimension) => selectedDimensions.find((dim) => dim.name === dimension.name))
.filter(isNonNullable),
];
panel.setDimensions([
...precedings,
...panel.dimensions.filter((dimension) => !precedings.find((dim) => dim.name === dimension.name)),
]);
panel.refreshDimensionsTreeData();
}, children: _jsx(DataCubeIcon.ChevronUp, { className: "text-lg" }) }), _jsx("div", { className: "h-3 w-[1px] bg-neutral-200" }), _jsx("button", { className: "flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", title: "Move dimension(s) down", disabled: selectedDimensions.length === 0, onClick: () => {
if (selectedDimensions.length === 0) {
return;
}
const maxIdx = Math.max(...selectedDimensions.map((dimension) => panel.dimensions.indexOf(dimension)));
const followings = [
// ensure moving dimensions maintain their relative ordering
...panel.dimensions
.map((dimension) => selectedDimensions.find((dim) => dim.name === dimension.name))
.filter(isNonNullable),
...panel.dimensions.slice(Math.min(maxIdx + 1, panel.dimensions.length) +
1),
];
panel.setDimensions([
...panel.dimensions.filter((dimension) => !followings.find((dim) => dim.name === dimension.name)),
...followings,
]);
panel.refreshDimensionsTreeData();
}, children: _jsx(DataCubeIcon.ChevronDown, { className: "text-lg" }) }), _jsx("div", { className: "h-3 w-[1px] bg-neutral-200" }), _jsx("button", { className: "flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", title: "Remove dimension(s)", disabled: selectedDimensions.length === 0, onClick: () => {
if (selectedDimensions.length === 0) {
return;
}
selectedDimensions.at(0)?.setIsRenaming(true);
}, children: _jsx(DataCubeIcon.Pencil, { className: "text-lg" }) }), _jsx("div", { className: "h-3 w-[1px] bg-neutral-200" }), _jsx("button", { className: "flex h-6 w-6 items-center justify-center text-neutral-500 disabled:text-neutral-300", title: "Rename dimension", disabled: selectedDimensions.length === 0, onClick: () => {
if (selectedDimensions.length === 0) {
return;
}
panel.setDimensions(panel.dimensions.filter((dimension) => !selectedDimensions.find((dim) => dim.name === dimension.name)));
panel.refreshDimensionsTreeData();
}, children: _jsx(DataCubeIcon.Delete, { className: "text-lg" }) })] })] }), _jsx("div", { className: "h-[calc(100%_-_24px)]", children: _jsx(AgGridReact, { ...getColumnsSelectorBaseGridProps(), modules: [AllCommunityModule, TreeDataModule], className: "ag-theme-quartz", treeData: true, treeDataChildrenField: "children", groupDefaultExpanded: -1,
// Disable managed row-dragging to disallow changing