@finos/legend-data-cube
Version:
380 lines • 22.6 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, 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 { editor as monacoEditorAPI } from 'monaco-editor';
import { BasePopover, Checkbox, clsx, cn, DataCubeIcon, DropdownMenu, DropdownMenuItem, HexAlphaColorPicker, HexColorInput, parseColor, TailwindCSSPalette, useForkRef, } from '@finos/legend-art';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import { isString } from '@finos/legend-shared';
import { useDataCube } from '../DataCubeProvider.js';
import { clearMarkers, CODE_EDITOR_THEME, getBaseCodeEditorOptions, getCodeEditorValue, normalizeLineEnding, resetLineNumberGutterWidth, setErrorMarkers, } from '@finos/legend-code-editor';
import { AlertType } from '../../stores/services/DataCubeAlertService.js';
export function FormBadge_WIP() {
return (_jsx("div", { className: "text-2xs ml-1 flex h-2.5 flex-shrink-0 select-none items-center rounded-md bg-sky-500 px-1 font-semibold leading-[10px] text-white", title: "Work In Progress", children: "WIP" }));
}
export function FormBadge_Advanced() {
return (_jsx("div", { className: "text-2xs ml-1 select-none rounded-md bg-amber-500 px-1 py-0.5 font-semibold text-white", title: "Advanced: Becareful when using this feature!", children: "ADV" }));
}
export const FormButton = forwardRef(function FormButton(props, ref) {
const { className, compact, ...otherProps } = props;
return (_jsx("button", { ref: ref, ...otherProps, className: cn('flex h-6 min-w-20 items-center justify-center border border-neutral-400 bg-neutral-300 px-2.5 hover:brightness-95 disabled:cursor-not-allowed disabled:border-neutral-300 disabled:text-neutral-400 disabled:hover:brightness-100', { 'h-5 text-sm': Boolean(compact) }, className) }));
});
export const FormNumberInput = forwardRef(function FormNumberInput(props, ref) {
const { min, max, step, value, setValue, isValid, isDecimal, defaultValue, disabled, className, ...innerProps } = props;
const [inputValue, setInputValue] = useState(value ?? '');
const inputRef = useRef(null);
const handleRef = useForkRef(inputRef, ref);
useEffect(() => {
setInputValue(value ?? '');
}, [value]);
return (_jsx("input", { className: cn('h-5 flex-shrink-0 border border-neutral-400 px-1.5 text-sm disabled:border-neutral-300 disabled:bg-neutral-50 disabled:text-neutral-300', className), ref: handleRef, ...innerProps, inputMode: "numeric", type: "number", min: min, max: max, step: step, value: inputValue, disabled: Boolean(disabled), onKeyDown: (event) => {
if (event.code === 'Escape') {
inputRef.current?.select();
}
else if (event.code === 'ArrowUp') {
event.preventDefault();
if (value !== undefined && step !== undefined) {
const newValue = value + step;
if ((min !== undefined && newValue < min) ||
(max !== undefined && newValue > max)) {
return;
}
setInputValue(newValue.toString());
setValue(newValue);
}
}
else if (event.code === 'ArrowDown') {
event.preventDefault();
if (value !== undefined && step !== undefined) {
const newValue = value - step;
if ((min !== undefined && newValue < min) ||
(max !== undefined && newValue > max)) {
return;
}
setInputValue(newValue.toString());
setValue(newValue);
}
}
}, onChange: (event) => {
const newInputValue = event.target.value;
setInputValue(newInputValue);
const numericValue = isDecimal
? Number(newInputValue)
: parseInt(newInputValue, 10);
if (isNaN(numericValue) ||
(isString(newInputValue) && !newInputValue) || // Explicitly check this case since `Number()` parses empty string as `0`
(isValid !== undefined
? !isValid(numericValue)
: (min !== undefined && numericValue < min) ||
(max !== undefined && numericValue > max))) {
return;
}
setValue(numericValue);
}, onBlur: () => {
const numericValue = isDecimal
? Number(inputValue)
: parseInt(inputValue.toString(), 10);
if (isNaN(numericValue) ||
(isString(inputValue) && !inputValue) || // Explicitly check this case since `Number()` parses empty string as `0`
(isValid !== undefined
? !isValid(numericValue)
: (min !== undefined && numericValue < min) ||
(max !== undefined && numericValue > max))) {
setValue(defaultValue);
setInputValue(defaultValue ?? '');
}
else {
setInputValue(value ?? '');
}
} }));
});
export const FormTextInput = forwardRef(function FormTextInput(props, ref) {
const { className, ...otherProps } = props;
const inputRef = useRef(null);
const handleRef = useForkRef(inputRef, ref);
return (_jsx("input", { ref: handleRef, className: cn('h-5 flex-shrink-0 border border-neutral-400 px-1.5 text-sm disabled:border-neutral-300 disabled:bg-neutral-50 disabled:text-neutral-300', className), onKeyDown: (event) => {
if (event.code === 'Escape') {
inputRef.current?.select();
}
}, ...otherProps }));
});
export function FormCheckbox(props) {
const { label, className, onChange, disabled, ...otherProps } = props;
return (_jsxs(_Fragment, { children: [_jsx(Checkbox, { icon: _jsx(DataCubeIcon.Checkbox, {}), checkedIcon: _jsx(DataCubeIcon.CheckboxSelected, {}), disableRipple: true, classes: {
root: cn(
// Make sure the icons used have consistent stroke width with other components' borders
// and that the left side is offseted to align well with other components
'p-0 text-neutral-600 [&_*]:stroke-[1.5px] -ml-[1px]', className),
checked: 'data-cube-editor-checkbox--checked',
disabled: 'data-cube-editor-checkbox--disabled',
}, onChange: onChange, disabled: disabled, ...otherProps }), Boolean(label) && (_jsx("button", { className: "flex-shrink-0 pl-1 text-sm disabled:text-neutral-300", onClick: onChange, disabled: disabled, tabIndex: -1, children: label }))] }));
}
export const FormDropdownMenuTrigger = forwardRef(function FormDropdownMenuTrigger(props, ref) {
const { children, className, open, showAsPlaceholder, ...otherProps } = props;
return (_jsxs("button", { ref: ref, className: cn('group flex h-5 flex-shrink-0 items-center justify-between border border-neutral-400 bg-white pl-1.5 text-sm disabled:select-none disabled:border-neutral-300 disabled:bg-neutral-50 disabled:text-neutral-300', {
'border-sky-600': Boolean(open),
'bg-sky-100': Boolean(open),
'text-neutral-400': Boolean(showAsPlaceholder),
}, className), ...otherProps, children: [_jsx("div", { className: "w-[calc(100%_-_12px)] overflow-hidden overflow-ellipsis whitespace-nowrap text-left", children: props.children }), _jsx("div", { className: "flex h-full w-3 items-center justify-center text-neutral-500 group-disabled:text-neutral-400", children: _jsx(DataCubeIcon.CaretDown, {}) })] }));
});
export function FormDropdownMenu(props) {
const { className, ...otherProps } = props;
return (_jsx(DropdownMenu, { ...otherProps, menuProps: {
classes: {
paper: 'rounded-none mt-[1px]',
list: cn('p-0 rounded-none border border-neutral-400 bg-white max-h-40 overflow-y-auto', className),
},
} }));
}
export function FormDropdownMenuItem(props) {
const { className, ...otherProps } = props;
return (_jsx(DropdownMenuItem, { className: cn('flex h-5 w-full items-center px-1.5 text-sm hover:bg-neutral-100 focus:bg-sky-600 focus:text-white', className), ...otherProps }));
}
export function FormDropdownMenuItemSeparator() {
return _jsx("div", { className: "my-0.5 h-[1px] w-full bg-neutral-200" });
}
function FormColorPicker(props) {
const { onChange, onClose, defaultColor } = props;
const [color, setColor] = useState(props.color);
const submit = () => {
onChange(
// if color is completely transparent, set it to #00000000
parseColor(color).alpha === 0 ? TailwindCSSPalette.transparent : color);
onClose();
};
return (_jsxs("div", { className: "data-cube-color-picker rounded-none border border-neutral-400 bg-white", children: [_jsx("div", { className: "p-2", children: _jsx(HexAlphaColorPicker, { color: color, onChange: setColor }) }), _jsx("div", { className: "flex justify-center px-2", children: [
'slate',
'neutral',
'red',
'orange',
'amber',
'yellow',
'lime',
'green',
'emerald',
'teal',
'sky',
'blue',
'indigo',
'violet',
'purple',
'fuchsia',
'pink',
'rose',
].map((scale) => (_jsx("div", { className: "mr-0.5 flex flex-col border-[0.5px] border-neutral-300 last:mr-0", children: [
50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950,
].map((level) => (_jsx("button", { className: "h-3 w-3 flex-shrink-0", style: {
background: TailwindCSSPalette[scale][level],
}, onClick: () => {
setColor(TailwindCSSPalette[scale][level]);
}, onDoubleClick: () => {
setColor(TailwindCSSPalette[scale][level]);
submit();
} }, `${scale}-${level}`))) }, scale))) }), _jsx("div", { className: "flex justify-center px-2 pb-1 pt-0.5", children: _jsx("div", { className: "flex", children: [
// Colors from Better Colors - https://clrs.cc/
TailwindCSSPalette.transparent,
'#000000',
'#aaaaaa',
'#dddddd',
'#ffffff',
'#ff4136',
'#ff851b',
'#ffdc00',
'#01ff70',
'#2ecc40',
'#3d9970',
'#39cccc',
'#7fdbff',
'#0074d9',
'#001f3f',
'#b10dc9',
'#f012be',
'#85144b',
].map((_color) => (_jsx("div", { className: cn('mr-0.5 border-[0.5px] border-neutral-300 last:mr-0', {
'data-cube-color-picker--transparent border-neutral-400': _color === TailwindCSSPalette.transparent,
}), children: _jsx("button", { className: "flex h-3 w-3 flex-shrink-0", style: {
background: _color,
}, onClick: () => {
setColor(_color);
}, onDoubleClick: () => {
setColor(_color);
submit();
} }) }, _color))) }) }), _jsx("div", { className: "h-[1px] w-full bg-neutral-200" }), _jsxs("div", { className: "flex h-6 items-center justify-between p-1", children: [_jsxs("div", { className: "flex", children: [_jsx("div", { className: "h-4 w-4 flex-shrink-0 rounded-sm border-[0.5px] border-neutral-300", style: { background: color } }), _jsx(HexColorInput, { className: "ml-0.5 h-4 w-14 border border-neutral-400 px-1 text-sm", color: color, autoFocus: true, tabIndex: 0, onChange: setColor, prefix: "#", alpha: true })] }), _jsxs("div", { className: "flex", children: [_jsx(FormButton, { className: "h-4 w-9", onClick: () => {
onClose();
}, children: "Cancel" }), _jsx(FormButton, { className: "ml-1 h-4 w-9 text-xs", disabled: defaultColor === undefined, onClick: () => {
if (defaultColor !== undefined) {
setColor(defaultColor);
}
}, children: "Reset" }), _jsx(FormButton, { className: "ml-1 h-4 w-9", onClick: () => {
submit();
}, children: "OK" })] })] })] }));
}
export function FormColorPickerButton(props) {
const { color, onChange, className, disabled, defaultColor } = props;
const [anchorEl, setAnchorEl] = useState(null);
return (_jsxs(_Fragment, { children: [_jsx("button", { className: cn('group h-5 w-10 border border-neutral-300 p-[1px] disabled:border-neutral-200', {
'data-cube-color-picker--disabled': Boolean(disabled),
'data-cube-color-picker--transparent': !disabled && color === TailwindCSSPalette.transparent,
}, className), onClick: (event) => {
setAnchorEl(event.currentTarget);
}, disabled: disabled, children: _jsx("div", { className: "h-full w-full group-disabled:hidden", style: {
background: color,
} }) }), Boolean(anchorEl) && (_jsx(BasePopover, { open: Boolean(anchorEl), anchorEl: anchorEl, anchorOrigin: { vertical: 'center', horizontal: 'right' }, transformOrigin: { vertical: 'center', horizontal: 'left' }, onClose: () => setAnchorEl(null), children: _jsx(FormColorPicker, { color: color, onChange: onChange, onClose: () => setAnchorEl(null), defaultColor: defaultColor }) }))] }));
}
export const FormDocumentation = ({ documentationKey, title, className }) => {
const dataCube = useDataCube();
const _documentationService = dataCube.documentationService;
const documentationEntry = dataCube.documentationService.getEntry(documentationKey);
const openDocLink = (event) => {
event.preventDefault();
event.stopPropagation();
const entry = _documentationService.getEntry(documentationKey);
if (entry) {
if (_documentationService.shouldDisplayDocumentationEntry(entry)) {
dataCube.documentationService.setCurrentEntry(entry);
dataCube.documentationService.display.open();
}
else if (entry.url) {
dataCube.navigationService.openLink(entry.url);
}
}
};
if (!documentationEntry ||
(!documentationEntry.url &&
!_documentationService.shouldDisplayDocumentationEntry(documentationEntry))) {
return null;
}
return (_jsx("div", { onClick: openDocLink, title: title ?? documentationEntry.text ?? 'Click to see documentation', className: cn('cursor-pointer text-xl text-sky-500', className), children: _jsx(DataCubeIcon.DocumentationHint, {}) }));
};
export const FormCodeEditor = (props) => {
const { value: inputValue, updateValue, language, title, isReadOnly, hideMinimap, hideGutter, hidePadding, hideActionBar, lineToScroll, extraEditorOptions, error, } = props;
const [editor, setEditor] = useState();
const [isWordWrap, setIsWordWrap] = useState(false);
const onDidChangeModelContentEventDisposer = useRef(undefined);
/**
* NOTE: we want to normalize line ending here since if the original
* input value includes CR '\r' character, it will get normalized, calling
* the updateValue method and cause a rerender. With the way we setup
* `onChange` method, React will warn about `setState` being called in
* `render` method.
* See https://github.com/finos/legend-studio/issues/608
*/
const value = normalizeLineEnding(inputValue);
const ref = useRef(null);
const toggleWordWrap = () => {
const updatedWordWrap = !isWordWrap;
setIsWordWrap(updatedWordWrap);
editor?.updateOptions({
wordWrap: updatedWordWrap ? 'on' : 'off',
});
};
useEffect(() => {
if (!editor && ref.current) {
const element = ref.current;
const _editor = monacoEditorAPI.create(element, {
...getBaseCodeEditorOptions(),
fontSize: 12,
theme: CODE_EDITOR_THEME.GITHUB_LIGHT,
// layout
glyphMargin: !hidePadding,
padding: !hidePadding ? { top: 20, bottom: 20 } : { top: 0, bottom: 0 },
formatOnType: true,
formatOnPaste: true,
});
setEditor(_editor);
}
}, [hidePadding, editor]);
useEffect(() => {
if (editor) {
resetLineNumberGutterWidth(editor);
const model = editor.getModel();
if (model) {
monacoEditorAPI.setModelLanguage(model, language);
}
}
}, [editor, language]);
useEffect(() => {
if (editor && lineToScroll !== undefined) {
editor.revealLineInCenter(lineToScroll);
}
}, [editor, lineToScroll]);
if (editor) {
// dispose the old editor content setter in case the `updateValue` handler changes
// for a more extensive note on this, see `LambdaEditor`
onDidChangeModelContentEventDisposer.current?.dispose();
onDidChangeModelContentEventDisposer.current =
editor.onDidChangeModelContent(() => {
const currentVal = getCodeEditorValue(editor);
if (currentVal !== value) {
updateValue?.(currentVal);
}
});
// Set the text value and editor options
const currentValue = getCodeEditorValue(editor);
if (currentValue !== value) {
editor.setValue(value);
}
editor.updateOptions({
readOnly: Boolean(isReadOnly),
minimap: { enabled: !hideMinimap },
// Hide the line number gutter
// See https://github.com/microsoft/vscode/issues/30795
...(hideGutter
? {
glyphMargin: !hidePadding,
folding: false,
lineNumbers: 'off',
lineDecorationsWidth: 0,
}
: {}),
...(extraEditorOptions ?? {}),
});
const model = editor.getModel();
if (model) {
if (error?.sourceInformation) {
setErrorMarkers(model, [
{
message: error.message,
startLineNumber: error.sourceInformation.startLine,
startColumn: error.sourceInformation.startColumn,
endLineNumber: error.sourceInformation.endLine,
endColumn: error.sourceInformation.endColumn,
},
]);
}
else {
clearMarkers();
}
}
}
// dispose editor
useEffect(() => () => {
if (editor) {
editor.dispose();
onDidChangeModelContentEventDisposer.current?.dispose();
}
}, [editor]);
return (_jsxs("div", { className: "h-full w-full border border-neutral-200", children: [!hideActionBar && (_jsxs("div", { className: "flex h-5 w-full items-center justify-between border-b border-neutral-200 bg-white", children: [_jsx("div", { className: "pl-1 text-sm text-neutral-500", children: title ?? '' }), _jsx("button", { tabIndex: -1, className: cn('flex aspect-square h-5 items-center justify-center text-neutral-400 hover:text-neutral-600', {
'text-neutral-600': isWordWrap,
}), onClick: toggleWordWrap, title: `[${isWordWrap ? 'on' : 'off'}] Toggle word wrap`, children: _jsx(DataCubeIcon.WordWrap, {}) })] })), _jsx("div", { className: cn('relative h-full w-full', {
'code-editor__content--padding': !hidePadding,
'h-[calc(100%_-_20px)]': !hideActionBar,
}), children: _jsx("div", { className: "absolute left-0 top-0 h-full w-full overflow-hidden", ref: ref }) })] }));
};
export function FormAlert(props) {
const { message, type, text, className } = props;
return (_jsxs("div", { className: clsx('flex rounded-sm bg-neutral-200 p-3', className), children: [_jsxs("div", { className: "mr-3", children: [type === AlertType.ERROR && (_jsx(DataCubeIcon.AlertError, { className: "flex-shrink-0 stroke-[0.5px] text-[40px] text-red-500" })), type === AlertType.INFO && (_jsx(DataCubeIcon.AlertInfo, { className: "flex-shrink-0 stroke-[0.5px] text-[40px] text-sky-500" })), type === AlertType.SUCCESS && (_jsx(DataCubeIcon.AlertSuccess, { className: "flex-shrink-0 stroke-[0.5px] text-[40px] text-green-500" })), type === AlertType.WARNING && (_jsx(DataCubeIcon.AlertWarning, { className: "flex-shrink-0 stroke-[0.3px] text-[40px] text-amber-500" }))] }), _jsxs("div", { children: [_jsx("div", { className: "whitespace-break-spaces", children: message }), text !== undefined && (_jsx("div", { className: "mt-1 whitespace-break-spaces text-sm leading-[13px] text-neutral-500", children: text }))] })] }));
}
//# sourceMappingURL=DataCubeFormUtils.js.map