@finos/legend-studio
Version:
266 lines • 23.3 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 { Fragment, useState, useRef, useCallback } from 'react';
import { flowResult } from 'mobx';
import { Dialog, ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, createFilter, CustomSelectorInput, BlankPanelPlaceholder, PanelLoadingIndicator, TimesIcon, PlayIcon, FlaskIcon, ResizablePanelSplitterLine, compareLabelFn, DropdownMenu, MenuContent, MenuContentItem, CaretDownIcon, RefreshIcon, RobotIcon, } from '@finos/legend-art';
import { observer } from 'mobx-react-lite';
import { getMappingElementSource, getMappingElementTarget, getMappingElementLabel, } from '../../../../stores/editor-state/element-editor-state/mapping/MappingEditorState.js';
import { useDrop } from 'react-dnd';
import { NewServiceModal } from '../service-editor/NewServiceModal.js';
import { CORE_DND_TYPE, } from '../../../../stores/shared/DnDUtil.js';
import { guaranteeType, uniq } from '@finos/legend-shared';
import { MappingExecutionEmptyInputDataState, MappingExecutionObjectInputDataState, MappingExecutionFlatDataInputDataState, MappingExecutionRelationalInputDataState, } from '../../../../stores/editor-state/element-editor-state/mapping/MappingExecutionState.js';
import { EDITOR_LANGUAGE, ActionAlertActionType, ActionAlertType, useApplicationStore, ExecutionPlanViewer, } from '@finos/legend-application';
import { useEditorStore } from '../../EditorStoreProvider.js';
import { Class, SetImplementation, OperationSetImplementation, getAllClassMappings, RelationalInputType, stub_RawLambda, isStubbed_RawLambda, } from '@finos/legend-graph';
import { StudioTextInputEditor } from '../../../shared/StudioTextInputEditor.js';
import { objectInputData_setData } from '../../../../stores/graphModifier/DSLMapping_GraphModifierHelper.js';
import { flatData_setData } from '../../../../stores/graphModifier/StoreFlatData_GraphModifierHelper.js';
import { relationalInputData_setData, relationalInputData_setInputType, } from '../../../../stores/graphModifier/StoreRelational_GraphModifierHelper.js';
export const ClassMappingSelectorModal = observer((props) => {
const { mappingEditorState, changeClassMapping, hideClassMappingSelectorModal, classMappingFilterFn, } = props;
const editorStore = useEditorStore();
// Class mapping selector
const classMappingSelectorRef = useRef(null);
const filterOption = createFilter({
ignoreCase: true,
ignoreAccents: false,
stringify: (option) => getMappingElementLabel(option.value, editorStore).value,
});
const classMappingOptions = uniq(getAllClassMappings(mappingEditorState.mapping)
.filter((classMapping) => !classMappingFilterFn || classMappingFilterFn(classMapping))
.map((classMapping) => ({
label: getMappingElementLabel(classMapping, editorStore).value,
value: classMapping,
}))
.sort(compareLabelFn));
const handleEnterClassMappingSelectorModal = () => classMappingSelectorRef.current?.focus();
const changeClassMappingOption = (val) => changeClassMapping(val.value);
return (_jsx(Dialog, { open: true, onClose: hideClassMappingSelectorModal, TransitionProps: {
onEnter: handleEnterClassMappingSelectorModal,
}, classes: { container: 'search-modal__container' }, PaperProps: { classes: { root: 'search-modal__inner-container' } }, children: _jsxs("div", { className: "modal search-modal", children: [_jsx("div", { className: "modal__title", children: "Choose a class mapping" }), _jsx(CustomSelectorInput, { ref: classMappingSelectorRef, options: classMappingOptions, onChange: changeClassMappingOption, value: null, placeholder: 'Choose a class mapping...', filterOption: filterOption, isClearable: true })] }) }));
});
export const getRelationalInputTestDataEditorLanguage = (type) => {
switch (type) {
case RelationalInputType.SQL:
return EDITOR_LANGUAGE.SQL;
default:
return EDITOR_LANGUAGE.TEXT;
}
};
const MappingExecutionQueryEditor = observer((props) => {
const { executionState } = props;
const queryState = executionState.queryState;
const mappingEditorState = executionState.mappingEditorState;
const editorStore = useEditorStore();
const applicationStore = useApplicationStore();
const extraQueryEditorActions = editorStore.pluginManager
.getApplicationPlugins()
.flatMap((plugin) => plugin.getExtraMappingExecutionQueryEditorActionConfigurations?.() ?? [])
.map((config) => (_jsx(Fragment, { children: config.renderer(executionState) }, config.key)));
// Class mapping selector
const [openClassMappingSelectorModal, setOpenClassMappingSelectorModal] = useState(false);
const showClassMappingSelectorModal = () => setOpenClassMappingSelectorModal(true);
const hideClassMappingSelectorModal = () => setOpenClassMappingSelectorModal(false);
const changeClassMapping = useCallback((setImplementation) => {
// do all the necessary updates
executionState.setExecutionResultText(undefined);
flowResult(queryState.updateLamba(setImplementation
? editorStore.graphManagerState.graphManager.HACKY__createGetAllLambda(guaranteeType(getMappingElementTarget(setImplementation), Class))
: stub_RawLambda())).catch(applicationStore.alertUnhandledError);
hideClassMappingSelectorModal();
// Attempt to generate data for input data panel as we pick the class mapping:
// - If the source panel is empty right now, automatically try to generate input data:
// - We generate based on the class mapping, if it's concrete
// - If the class mapping is operation, output a warning message
// - If the source panel is non-empty (show modal), show an option to keep current input data
if (setImplementation) {
if (executionState.inputDataState instanceof
MappingExecutionEmptyInputDataState) {
if (setImplementation instanceof OperationSetImplementation) {
applicationStore.notifyWarning(`Can't auto-generate input data for operation class mapping. Please pick a concrete class mapping instead`);
}
else {
executionState.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins()), true);
}
}
else {
editorStore.setActionAlertInfo({
message: 'Mapping execution input data is already set',
prompt: 'Do you want to regenerate the input data?',
type: ActionAlertType.CAUTION,
onEnter: () => editorStore.setBlockGlobalHotkeys(true),
onClose: () => editorStore.setBlockGlobalHotkeys(false),
actions: [
{
label: 'Regenerate',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: () => executionState.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins()), true),
},
{
label: 'Keep my input data',
type: ActionAlertActionType.PROCEED,
default: true,
},
],
});
}
}
// TODO: open query builder
}, [applicationStore, editorStore, executionState, queryState]);
// Drag and Drop
const handleDrop = useCallback((item) => {
changeClassMapping(guaranteeType(item.data, SetImplementation));
}, [changeClassMapping]);
const [{ isDragOver, canDrop }, dropRef] = useDrop(() => ({
accept: CORE_DND_TYPE.MAPPING_EXPLORER_CLASS_MAPPING,
drop: (item) => handleDrop(item),
collect: (monitor) => ({
isDragOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
}),
}), [handleDrop]);
const clearQuery = applicationStore.guardUnhandledError(() => flowResult(executionState.queryState.updateLamba(stub_RawLambda())));
return (_jsxs("div", { className: "panel mapping-execution-builder__query-panel", children: [_jsxs("div", { className: "panel__header", children: [_jsx("div", { className: "panel__header__title", children: _jsx("div", { className: "panel__header__title__label", children: "query" }) }), _jsxs("div", { className: "panel__header__actions", children: [extraQueryEditorActions, _jsx("button", { className: "panel__header__action", tabIndex: -1, onClick: clearQuery, title: 'Clear query', children: _jsx(TimesIcon, {}) })] })] }), !isStubbed_RawLambda(queryState.query) && (_jsx("div", { className: "panel__content", children: _jsx("div", { className: "mapping-execution-builder__query-panel__query", children: _jsx(StudioTextInputEditor, { inputValue: queryState.lambdaString, isReadOnly: true, language: EDITOR_LANGUAGE.PURE, showMiniMap: false }) }) })), isStubbed_RawLambda(queryState.query) && (_jsx("div", { ref: dropRef, className: "panel__content", children: _jsx(BlankPanelPlaceholder, { placeholderText: "Choose a class mapping", onClick: showClassMappingSelectorModal, clickActionType: "add", tooltipText: "Drop a class mapping, or click to choose one to start building the query", dndProps: {
isDragOver: isDragOver,
canDrop: canDrop,
} }) })), openClassMappingSelectorModal && (_jsx(ClassMappingSelectorModal, { mappingEditorState: mappingEditorState, hideClassMappingSelectorModal: hideClassMappingSelectorModal, changeClassMapping: changeClassMapping }))] }));
});
export const MappingExecutionObjectInputDataBuilder = observer((props) => {
const { inputDataState } = props;
// TODO?: handle XML/type
// Input data
const updateInput = (val) => objectInputData_setData(inputDataState.inputData, val);
return (_jsx("div", { className: "panel__content mapping-execution-builder__input-data-panel__content", children: _jsx(StudioTextInputEditor, { language: EDITOR_LANGUAGE.JSON, inputValue: inputDataState.inputData.data, updateInput: updateInput }) }));
});
export const MappingExecutionFlatDataInputDataBuilder = observer((props) => {
const { inputDataState } = props;
// Input data
const updateInput = (val) => flatData_setData(inputDataState.inputData, val);
return (_jsx("div", { className: "panel__content mapping-execution-builder__input-data-panel__content", children: _jsx(StudioTextInputEditor, { language: EDITOR_LANGUAGE.TEXT, inputValue: inputDataState.inputData.data, updateInput: updateInput }) }));
});
/**
* Right now, we always default this to use Local H2 connection.
*/
export const MappingExecutionRelationalInputDataBuilder = observer((props) => {
const { inputDataState } = props;
// Input data
const updateInput = (val) => relationalInputData_setData(inputDataState.inputData, val);
return (_jsx("div", { className: "panel__content mapping-execution-builder__input-data-panel__content", children: _jsx(StudioTextInputEditor, { language: getRelationalInputTestDataEditorLanguage(inputDataState.inputData.inputType), inputValue: inputDataState.inputData.data, updateInput: updateInput }) }));
});
export const MappingExecutionEmptyInputDataBuilder = observer((props) => {
const { changeClassMapping, showClassMappingSelectorModal } = props;
// Drag and Drop
const handleDrop = useCallback((item) => {
changeClassMapping(guaranteeType(item.data, SetImplementation));
}, [changeClassMapping]);
const [{ isDragOver, canDrop }, dropRef] = useDrop(() => ({
accept: CORE_DND_TYPE.MAPPING_EXPLORER_CLASS_MAPPING,
drop: (item) => handleDrop(item),
collect: (monitor) => ({
isDragOver: monitor.isOver({ shallow: true }),
canDrop: monitor.canDrop(),
}),
}), [handleDrop]);
return (_jsx("div", { ref: dropRef, className: "panel__content", children: _jsx(BlankPanelPlaceholder, { placeholderText: "Choose a class mapping", onClick: showClassMappingSelectorModal, clickActionType: "add", tooltipText: "Drop a class mapping, or click to choose one to generate input data", dndProps: {
isDragOver: isDragOver,
canDrop: canDrop,
} }) }));
});
const RelationalMappingExecutionInputDataTypeSelector = observer((props) => {
const { inputDataState } = props;
const changeInputType = (val) => () => {
relationalInputData_setInputType(inputDataState.inputData, val);
};
return (_jsx(DropdownMenu, { className: "mapping-execution-builder__input-data-panel__type-selector", content: _jsx(MenuContent, { children: Object.keys(RelationalInputType).map((mode) => (_jsx(MenuContentItem, { className: "mapping-execution-builder__input-data-panel__type-selector__option", onClick: changeInputType(mode), children: mode }, mode))) }), children: _jsxs("div", { className: "mapping-execution-builder__input-data-panel__type-selector__value", title: "Choose input data type...", children: [_jsx("div", { className: "mapping-execution-builder__input-data-panel__type-selector__value__label", children: inputDataState.inputData.inputType }), _jsx(CaretDownIcon, {})] }) }));
});
export const MappingExecutionInputDataBuilder = observer((props) => {
const { executionState } = props;
const editorStore = useEditorStore();
const mappingEditorState = executionState.mappingEditorState;
const inputDataState = executionState.inputDataState;
// Class mapping selector
const [openClassMappingSelectorModal, setOpenClassMappingSelectorModal] = useState(false);
const showClassMappingSelectorModal = () => setOpenClassMappingSelectorModal(true);
const hideClassMappingSelectorModal = () => setOpenClassMappingSelectorModal(false);
const changeClassMapping = useCallback((setImplementation) => {
executionState.setInputDataStateBasedOnSource(setImplementation
? getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins())
: undefined, true);
executionState.setExecutionResultText(undefined);
hideClassMappingSelectorModal();
}, [executionState, editorStore]);
const classMappingFilterFn = (setImp) => !(setImp instanceof OperationSetImplementation);
// Input data builder
let inputDataBuilder;
if (inputDataState instanceof MappingExecutionEmptyInputDataState) {
inputDataBuilder = (_jsx(MappingExecutionEmptyInputDataBuilder, { inputDataState: inputDataState, showClassMappingSelectorModal: showClassMappingSelectorModal, changeClassMapping: changeClassMapping }));
}
else if (inputDataState instanceof MappingExecutionObjectInputDataState) {
inputDataBuilder = (_jsx(MappingExecutionObjectInputDataBuilder, { inputDataState: inputDataState }));
}
else if (inputDataState instanceof MappingExecutionFlatDataInputDataState) {
inputDataBuilder = (_jsx(MappingExecutionFlatDataInputDataBuilder, { inputDataState: inputDataState }));
}
else if (inputDataState instanceof MappingExecutionRelationalInputDataState) {
inputDataBuilder = (_jsx(MappingExecutionRelationalInputDataBuilder, { inputDataState: inputDataState }));
}
else {
inputDataBuilder = null;
}
// input type builder
let inputTypeSelector;
if (inputDataState instanceof MappingExecutionRelationalInputDataState) {
inputTypeSelector = (_jsx(RelationalMappingExecutionInputDataTypeSelector, { inputDataState: inputDataState }));
}
else {
inputTypeSelector = null;
}
const clearInputData = () => executionState.setInputDataState(new MappingExecutionEmptyInputDataState(mappingEditorState.editorStore, mappingEditorState.mapping, undefined));
return (_jsxs("div", { className: "panel mapping-execution-builder__input-data-panel", children: [_jsxs("div", { className: "panel__header", children: [_jsx("div", { className: "panel__header__title", children: _jsx("div", { className: "panel__header__title__label", children: "input data" }) }), _jsxs("div", { className: "panel__header__actions", children: [inputTypeSelector, _jsx("button", { className: "panel__header__action", tabIndex: -1, onClick: showClassMappingSelectorModal, title: 'Regenerate...', children: _jsx(RefreshIcon, { className: "mapping-execution-builder__icon--refresh" }) }), _jsx("button", { className: "panel__header__action", tabIndex: -1, onClick: clearInputData, title: 'Clear input data', children: _jsx(TimesIcon, {}) })] })] }), inputDataBuilder, openClassMappingSelectorModal && (_jsx(ClassMappingSelectorModal, { mappingEditorState: mappingEditorState, hideClassMappingSelectorModal: hideClassMappingSelectorModal, changeClassMapping: changeClassMapping, classMappingFilterFn: classMappingFilterFn }))] }));
});
export const MappingExecutionBuilder = observer((props) => {
const { executionState } = props;
const mappingEditorState = executionState.mappingEditorState;
const applicationStore = useApplicationStore();
const { queryState, inputDataState } = executionState;
// execute
const generatePlan = applicationStore.guardUnhandledError(() => flowResult(executionState.generatePlan(false)));
const debugPlanGeneration = applicationStore.guardUnhandledError(() => flowResult(executionState.generatePlan(true)));
const execute = applicationStore.guardUnhandledError(() => flowResult(executionState.executeMapping()));
const executionResultText = executionState.executionResultText;
// actions
const promote = applicationStore.guardUnhandledError(() => flowResult(executionState.promoteToTest()));
const promoteToService = () => executionState.setShowServicePathModal(true);
return (_jsxs("div", { className: "mapping-execution-builder", children: [_jsx(PanelLoadingIndicator, { isLoading: executionState.isExecuting || executionState.isGeneratingPlan }), _jsxs("div", { className: "mapping-execution-builder__header", children: [_jsx("div", {}), _jsxs("div", { className: "mapping-execution-builder__header__actions", children: [!mappingEditorState.isReadOnly && (_jsx("button", { className: "mapping-execution-builder__header__action", disabled: isStubbed_RawLambda(queryState.query) ||
!inputDataState.isValid ||
executionState.isExecuting ||
!executionState.executionResultText, onClick: promoteToService, tabIndex: -1, title: "Promote to Service...", children: _jsx(RobotIcon, {}) })), !mappingEditorState.isReadOnly && (_jsx("button", { className: "mapping-execution-builder__header__action", disabled: isStubbed_RawLambda(queryState.query) ||
!inputDataState.isValid ||
executionState.isExecuting ||
!executionState.executionResultText, onClick: promote, tabIndex: -1, title: "Promote to Test", children: _jsx(FlaskIcon, {}) })), _jsxs("button", { className: "mapping-execution-builder__execute-btn", onClick: execute, disabled: isStubbed_RawLambda(queryState.query) ||
!inputDataState.isValid ||
executionState.isGeneratingPlan ||
executionState.isExecuting, tabIndex: -1, children: [_jsxs("div", { className: "mapping-execution-builder__execute-btn__label", children: [_jsx(PlayIcon, { className: "mapping-execution-builder__execute-btn__label__icon" }), _jsx("div", { className: "mapping-execution-builder__execute-btn__label__title", children: "Execute" })] }), _jsx(DropdownMenu, { className: "mapping-execution-builder__execute-btn__dropdown-btn", disabled: isStubbed_RawLambda(queryState.query) ||
!inputDataState.isValid ||
executionState.isGeneratingPlan ||
executionState.isExecuting, content: _jsxs(MenuContent, { children: [_jsx(MenuContentItem, { className: "mapping-execution-builder__execute-btn__option", onClick: generatePlan, children: "Generate Plan" }), _jsx(MenuContentItem, { className: "mapping-execution-builder__execute-btn__option", onClick: debugPlanGeneration, children: "Debug" })] }), menuProps: {
anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
transformOrigin: { vertical: 'top', horizontal: 'right' },
}, children: _jsx(CaretDownIcon, {}) })] })] })] }), _jsx("div", { className: "mapping-execution-builder__content", children: _jsxs(ResizablePanelGroup, { orientation: "horizontal", children: [_jsx(ResizablePanel, { size: 250, minSize: 28, children: _jsx(MappingExecutionQueryEditor, { executionState: executionState }, executionState.queryState.uuid) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: "var(--color-dark-grey-50)" }) }), _jsx(ResizablePanel, { size: 250, minSize: 28, children: _jsx(MappingExecutionInputDataBuilder, { executionState: executionState }, executionState.inputDataState.uuid) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: "var(--color-dark-grey-50)" }) }), _jsx(ResizablePanel, { minSize: 28, children: _jsxs("div", { className: "panel mapping-execution-builder__result-panel", children: [_jsx("div", { className: "panel__header", children: _jsx("div", { className: "panel__header__title", children: _jsx("div", { className: "panel__header__title__label", children: "result" }) }) }), _jsx("div", { className: "panel__content mapping-execution-builder__result-panel__content", children: _jsx(StudioTextInputEditor, { inputValue: executionResultText ?? '', isReadOnly: true, language: EDITOR_LANGUAGE.JSON }) })] }) })] }) }), _jsx(ExecutionPlanViewer, { executionPlanState: executionState.executionPlanState }), _jsx(NewServiceModal, { mapping: mappingEditorState.mapping, close: () => executionState.setShowServicePathModal(false), showModal: executionState.showServicePathModal, promoteToService: (packagePath, name) => flowResult(executionState.promoteToService(packagePath, name)), isReadOnly: mappingEditorState.isReadOnly })] }));
});
//# sourceMappingURL=MappingExecutionBuilder.js.map