UNPKG

@finos/legend-data-cube

Version:
380 lines 22.6 kB
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