UNPKG

@finos/legend-data-cube

Version:
348 lines 26.1 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 { DataCubeIcon, useDropdownMenu } from '@finos/legend-art'; import { AllCommunityModule, } from 'ag-grid-community'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { AgGridReact, } from 'ag-grid-react'; import {} from '../../../stores/view/editor/DataCubeEditorColumnsSelectorState.js'; import { isNonNullable } from '@finos/legend-shared'; import { getDataForAllFilteredNodes, getDataForAllNodes, } from '../../../stores/view/grid/DataCubeGridClientEngine.js'; import { FormDropdownMenu, FormDropdownMenuItem, } from '../../core/DataCubeFormUtils.js'; import { DataCubeQuerySortDirection } from '../../../stores/core/DataCubeQueryEngine.js'; import { _findCol } from '../../../stores/core/model/DataCubeColumn.js'; export const INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT = 20; export function getColumnsSelectorBaseGridProps() { return { modules: [AllCommunityModule], theme: 'legacy', className: 'ag-theme-quartz', animateRows: false, getRowId: (params) => params.data.name, editType: 'fullRow', rowDragMultiRow: true, rowDragEntireRow: true, rowSelection: { mode: 'multiRow', checkboxes: true, headerCheckbox: true, enableClickSelection: true, }, selectionColumnDef: { width: 40, headerClass: 'pl-6', cellClass: 'pl-1.5', rowDrag: true, rowDragText: (params, dragItemCount) => { if (dragItemCount > 1) { return `${dragItemCount} columns`; } return (params.rowNode?.data).name; }, sortable: false, resizable: false, suppressHeaderMenuButton: true, }, suppressMoveWhenRowDragging: true, rowHeight: INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT, headerHeight: INTERNAL__EDITOR_COLUMNS_SELECTOR_ROW_HEIGHT, suppressRowHoverHighlight: false, noRowsOverlayComponent: (params) => { if (params.api.getQuickFilter()) { return (_jsxs("div", { className: "flex items-center border-[1.5px] border-neutral-300 p-2 font-semibold text-neutral-400", children: [_jsx("div", { children: _jsx(DataCubeIcon.WarningCircle, { className: "mr-1 text-lg" }) }), "No match found"] })); } if (params.noColumnsSelectedRenderer) { return params.noColumnsSelectedRenderer(); } return _jsx("div", {}); }, // Show no rows overlay when there are no search results // See https://stackoverflow.com/a/72637410 onModelUpdated: (event) => { if (event.api.getDisplayedRowCount() === 0) { event.api.showNoRowsOverlay(); } else { event.api.hideOverlay(); } }, }; } export function getColumnsSelectorBaseColumnDef() { return { field: 'name', colId: 'name', flex: 1, minWidth: 100, filter: true, sortable: false, resizable: false, suppressHeaderMenuButton: true, getQuickFilterText: (params) => params.value, }; } /** * Move this display to a separate component to avoid re-rendering the header too frequently */ const ColumnsSearchResultCountBadge = observer(function ColumnsSearchResultCountBadge(props) { const { selector, gridApi, scope } = 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}/${scope === 'available' ? selector.availableColumnsForDisplay.length : selector.selectedColumnsForDisplay.length}`, _jsx("span", { className: "hidden", children: scope === 'available' ? // subscribing to the search text to trigger re-render as it changes selector.availableColumnsSearchText : selector.selectedColumnsSearchText })] })); }); export const DataCubeEditorColumnsSelector = observer(function DataCubeEditorColumnsSelector(props) { const { selector, columnLabelRenderer, columnActionRenderer, noColumnsSelectedRenderer, } = props; const [selectedAvailableColumns, setSelectedAvailableColumns] = useState([]); const [selectedSelectedColumns, setSelectedSelectedColumns] = useState([]); const [availableColumnsGridApi, setAvailableColumnsGridApi] = useState(null); const [selectedColumnsGridApi, setSelectedColumnsGridApi] = useState(null); const searchAvailableColumnsInputRef = useRef(null); const searchSelectedColumnsInputRef = useRef(null); const onAvailableColumnsDragStop = 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) .filter(isNonNullable); selector.setSelectedColumns(selector.selectedColumns.filter((column) => !_findCol(columnsToMove, column.name))); }, [selector]); /** * Since we use managed row dragging for selected columns, * we just need to sync the row data with the state. * Dragging (multiple) rows to specific position have been * handled by ag-grid. */ const onSelectedColumnsDragStop = useCallback((params) => { selector.setSelectedColumns(getDataForAllNodes(params.api)); }, [selector]); /** * Since we use managed row dragging for selected columns, * we just need to sync the row data with the state * Dragging (multiple) rows to specific position have been * handled by ag-grid. */ const onSelectedColumnsDragEnd = useCallback( // this event hook is mainly meant for reordering columns within // selected columns table (event) => { if (event.overIndex === -1) { return; } selector.setSelectedColumns(getDataForAllNodes(event.api)); }, [selector]); /** * 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 || !selectedColumnsGridApi) { return; } const selectedColumnsDropZoneParams = !selectedColumnsGridApi.isDestroyed() ? selectedColumnsGridApi.getRowDropZoneParams({ onDragStop: (event) => { onSelectedColumnsDragStop(event); availableColumnsGridApi.clearFocusedCell(); }, }) : undefined; if (selectedColumnsDropZoneParams && !availableColumnsGridApi.isDestroyed()) { availableColumnsGridApi.addRowDropZone(selectedColumnsDropZoneParams); } const availableColumnsDropZoneParams = !availableColumnsGridApi.isDestroyed() ? availableColumnsGridApi.getRowDropZoneParams({ onDragStop: (event) => { onAvailableColumnsDragStop(event); selectedColumnsGridApi.clearFocusedCell(); }, }) : undefined; if (availableColumnsDropZoneParams && !selectedColumnsGridApi.isDestroyed()) { selectedColumnsGridApi.addRowDropZone(availableColumnsDropZoneParams); } }, [ availableColumnsGridApi, selectedColumnsGridApi, onSelectedColumnsDragStop, onAvailableColumnsDragStop, ]); return (_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: selector.availableColumnsSearchText, onChange: (event) => selector.setAvailableColumnsSearchText(event.target.value), onKeyDown: (event) => { if (event.code === 'Escape') { event.stopPropagation(); searchAvailableColumnsInputRef.current?.select(); selector.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: !selector.availableColumnsSearchText, title: "Clear search [Esc]", onClick: () => { selector.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: selector.availableColumnsSearchText, rowData: selector.availableColumnsForDisplay, columnDefs: [ { ...getColumnsSelectorBaseColumnDef(), /** * 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); selector.setSelectedColumns([ ...selector.selectedColumns, ...filteredNodes, ]); params.api.clearFocusedCell(); }, children: [_jsx("div", { children: `[All Columns]` }), _jsx(ColumnsSearchResultCountBadge, { selector: selector, gridApi: params.api, scope: "available" })] })), cellRenderer: (params) => { const data = params.data; if (!data) { return null; } return (_jsxs("div", { className: "flex h-full w-full cursor-pointer items-center", title: `[${data.name}]\nDouble-click to add column`, onDoubleClick: () => { selector.setSelectedColumns([ ...selector.selectedColumns, data, ]); params.api.clearFocusedCell(); }, children: [columnLabelRenderer?.({ selector, column: data, }) ?? (_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", children: columnActionRenderer?.({ selector, column: data, }) ?? null })] })); }, }, ] }) })] })] }), _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)); selector.setSelectedColumns([ ...selector.selectedColumns, ...columnsToMove, ]); 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 (!selectedColumnsGridApi || selectedSelectedColumns.length === 0) { return; } // The columns being moved are scoped by the current search const filteredNodes = getDataForAllFilteredNodes(selectedColumnsGridApi); const columnsToMove = selectedSelectedColumns.filter((column) => _findCol(filteredNodes, column.name)); selector.setSelectedColumns(selector.selectedColumns.filter((column) => !_findCol(columnsToMove, column.name))); selectedColumnsGridApi.clearFocusedCell(); }, disabled: !selectedColumnsGridApi || selectedSelectedColumns.length === 0, children: _jsx(DataCubeIcon.ChevronLeft, { className: "text-2xl" }) })] }) }), _jsxs("div", { className: "h-full w-[calc(50%_-_20px)]", children: [_jsx("div", { className: "flex h-5 items-center text-sm", children: "Selected 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 placeholder-neutral-400", ref: searchSelectedColumnsInputRef, placeholder: "Search columns...", value: selector.selectedColumnsSearchText, onChange: (event) => selector.setSelectedColumnsSearchText(event.target.value), onKeyDown: (event) => { if (event.code === 'Escape') { event.stopPropagation(); selector.setSelectedColumnsSearchText(''); } } }), _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: !selector.selectedColumnsSearchText, title: "Clear search [Esc]", onClick: () => { selector.setSelectedColumnsSearchText(''); searchSelectedColumnsInputRef.current?.focus(); }, children: _jsx(DataCubeIcon.X, { className: "text-lg" }) })] }), _jsx("div", { className: "h-[calc(100%_-_24px)]", children: _jsx(AgGridReact, { ...getColumnsSelectorBaseGridProps(), // NOTE: technically, we don't want to enable managed row-dragging here // but enabling this gives us free row moving management and interaction // comes out of the box from ag-grid, we will just sync the state with // grid row data afterwards to ensure consistency rowDragManaged: true, onRowDragEnd: onSelectedColumnsDragEnd, onGridReady: (params) => setSelectedColumnsGridApi(params.api), onSelectionChanged: (event) => { setSelectedSelectedColumns(event.api .getSelectedNodes() .map((node) => node.data) .filter(isNonNullable)); }, // Using ag-grid quick filter is a cheap way to implement search quickFilterText: selector.selectedColumnsSearchText, noRowsOverlayComponentParams: { noColumnsSelectedRenderer, }, rowData: selector.selectedColumnsForDisplay, columnDefs: [ { ...getColumnsSelectorBaseColumnDef(), /** * Support double-click to remove all (filtered by search) columns */ headerComponent: (params) => (_jsxs("button", { title: "Double-click to remove 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); selector.setSelectedColumns(selector.selectedColumns.filter((column) => !_findCol(filteredNodes, column.name))); params.api.clearFocusedCell(); }, children: [_jsx("div", { children: `[All Columns]` }), _jsx(ColumnsSearchResultCountBadge, { selector: selector, gridApi: params.api, scope: "selected" })] })), cellRenderer: (params) => { const data = params.data; if (!data) { return null; } return (_jsxs("div", { className: "flex h-full w-full cursor-pointer items-center", title: `[${data.name}]\nDouble-click to remove column`, onDoubleClick: () => { selector.setSelectedColumns(selector.selectedColumns.filter((column) => column !== data)); params.api.clearFocusedCell(); }, children: [columnLabelRenderer?.({ selector, column: data, }) ?? (_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", children: columnActionRenderer?.({ selector, column: data, }) ?? null })] })); }, }, ] }) })] })] })] })); }); export const DataCubeEditorColumnsSelectorSortDirectionDropdown = observer((props) => { const { column } = props; const [openDirectionDropdown, closeDirectionDropdown, directionDropdownProps, directionDropdownPropsOpen,] = useDropdownMenu(); return (_jsxs("div", { className: "group relative flex h-full items-center", children: [!directionDropdownPropsOpen && (_jsx("div", { className: "flex h-[18px] w-32 items-center border border-transparent px-2 text-sm text-neutral-400 group-hover:invisible", children: column.direction })), directionDropdownPropsOpen && (_jsxs("div", { className: "flex h-[18px] w-32 items-center justify-between border border-sky-600 bg-sky-50 pl-2 pr-0.5 text-sm", children: [_jsx("div", { children: column.direction }), _jsx("div", { children: _jsx(DataCubeIcon.CaretDown, {}) })] })), _jsxs("button", { className: "invisible absolute right-0 z-10 flex h-[18px] w-32 items-center justify-between border border-neutral-400 pl-2 pr-0.5 text-sm text-neutral-700 group-hover:visible", /** * ag-grid row select event listener is at a deeper layer than this dropdown trigger * so in order to prevent selecting the row while opening the dropdown, we need to stop * the propagation as event capturing is happening, not when it's bubbling. */ onClickCapture: (event) => { event.stopPropagation(); openDirectionDropdown(event); }, onClick: (event) => event.stopPropagation(), children: [_jsx("div", { children: column.direction }), _jsx("div", { children: _jsx(DataCubeIcon.CaretDown, {}) })] }), _jsx(FormDropdownMenu, { className: "w-32", ...directionDropdownProps, children: [ DataCubeQuerySortDirection.ASCENDING, DataCubeQuerySortDirection.DESCENDING, ].map((direction) => (_jsx(FormDropdownMenuItem, { onClick: () => { column.setDirection(direction); closeDirectionDropdown(); }, autoFocus: column.direction === direction, children: direction }, direction))) })] })); }); //# sourceMappingURL=DataCubeEditorColumnsSelector.js.map