UNPKG

@finos/legend-data-cube

Version:
646 lines 61.3 kB
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