@finos/legend-data-cube
Version:
348 lines • 26.1 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 { 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