@finos/legend-application-studio
Version:
Legend Studio application core
299 lines • 25.9 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { useState, useRef, useCallback } from 'react';
import { flowResult } from 'mobx';
import { Dialog, ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, createFilter, CustomSelectorInput, BlankPanelPlaceholder, PanelLoadingIndicator, TimesIcon, PlayIcon, FlaskIcon, ResizablePanelSplitterLine, compareLabelFn, ControlledDropdownMenu, MenuContent, MenuContentItem, CaretDownIcon, RefreshIcon, RobotIcon, PanelDropZone, PencilIcon, PanelContent, Modal, ModalTitle, clsx, PanelHeaderActionItem, PanelHeader, PanelHeaderActions, Panel, PauseCircleIcon, } from '@finos/legend-art';
import { observer } from 'mobx-react-lite';
import { getMappingElementSource, getMappingElementTarget, getMappingElementLabel, } from '../../../../stores/editor/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/editor/utils/DnDUtils.js';
import { assertErrorThrown, guaranteeType, uniq } from '@finos/legend-shared';
import { MappingExecutionEmptyInputDataState, MappingExecutionObjectInputDataState, MappingExecutionFlatDataInputDataState, MappingExecutionRelationalInputDataState, } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingExecutionState.js';
import { ActionAlertActionType, ActionAlertType, useApplicationStore, } from '@finos/legend-application';
import { useEditorStore } from '../../EditorStoreProvider.js';
import { Class, SetImplementation, OperationSetImplementation, getAllClassMappings, RelationalInputType, stub_RawLambda, isStubbed_RawLambda, } from '@finos/legend-graph';
import { objectInputData_setData } from '../../../../stores/graph-modifier/DSL_Mapping_GraphModifierHelper.js';
import { flatData_setData } from '../../../../stores/graph-modifier/STO_FlatData_GraphModifierHelper.js';
import { relationalInputData_setData, relationalInputData_setInputType, } from '../../../../stores/graph-modifier/STO_Relational_GraphModifierHelper.js';
import { MappingExecutionQueryBuilderState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingExecutionQueryBuilderState.js';
import { ExecutionPlanViewer, } from '@finos/legend-query-builder';
import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor';
import { CodeEditor } from '@finos/legend-lego/code-editor';
export const ClassMappingSelectorModal = observer((props) => {
const { mappingEditorState, changeClassMapping, hideClassMappingSelectorModal, classMappingFilterFn, } = props;
const editorStore = useEditorStore();
const applicationStore = editorStore.applicationStore;
// Class mapping selector
const classMappingSelectorRef = useRef(null);
const filterOption = createFilter({
ignoreCase: true,
ignoreAccents: false,
stringify: (option) => getMappingElementLabel(option.data.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, classes: { container: 'search-modal__container' }, slotProps: {
transition: {
onEnter: handleEnterClassMappingSelectorModal,
},
paper: {
classes: { root: 'search-modal__inner-container' },
},
}, children: _jsxs(Modal, { className: clsx('search-modal', {
'modal--dark': true,
}), children: [_jsx(ModalTitle, { title: "Choose a class mapping" }), _jsx(CustomSelectorInput, { inputRef: classMappingSelectorRef, options: classMappingOptions, onChange: changeClassMappingOption, value: null, placeholder: "Choose a class mapping...", filterOption: filterOption, isClearable: true, darkMode: !applicationStore.layoutService
.TEMPORARY__isLightColorThemeEnabled })] }) }));
});
export const getRelationalInputTestDataEditorLanguage = (type) => {
switch (type) {
case RelationalInputType.SQL:
return CODE_EDITOR_LANGUAGE.SQL;
default:
return CODE_EDITOR_LANGUAGE.TEXT;
}
};
const MappingExecutionQueryEditor = observer((props) => {
const { executionState } = props;
const queryState = executionState.queryState;
const mappingEditorState = executionState.mappingEditorState;
const editorStore = useEditorStore();
const applicationStore = useApplicationStore();
// actions
const editWithQueryBuilder = applicationStore.guardUnhandledError(async () => {
const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState;
await flowResult(embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({
setupQueryBuilderState: async () => {
const queryBuilderState = new MappingExecutionQueryBuilderState(embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, executionState.mappingEditorState.mapping, editorStore.applicationStore.config.options.queryBuilderConfig, editorStore.editorMode.getSourceInfo());
queryBuilderState.initializeWithQuery(executionState.queryState.query);
return queryBuilderState;
},
actionConfigs: [
{
key: 'save-query-btn',
renderer: (queryBuilderState) => {
const save = applicationStore.guardUnhandledError(async () => {
try {
const rawLambda = queryBuilderState.buildQuery();
await flowResult(executionState.queryState.updateLamba(rawLambda));
applicationStore.notificationService.notifySuccess(`Mapping execution query is updated`);
embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration(undefined);
}
catch (error) {
assertErrorThrown(error);
applicationStore.notificationService.notifyError(`Can't save query: ${error.message}`);
}
});
return (_jsx("button", { className: "query-builder__dialog__header__custom-action", tabIndex: -1, onClick: save, children: "Save Query" }));
},
},
],
disableCompile: isStubbed_RawLambda(executionState.queryState.query),
}));
});
// 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.createGetAllRawLambda(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.notificationService.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 {
applicationStore.alertService.setActionAlertInfo({
message: 'Mapping execution input data is already set',
prompt: 'Do you want to regenerate the input data?',
type: ActionAlertType.CAUTION,
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 }, dropConnector] = 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(Panel, { className: "mapping-execution-builder__query-panel", children: [_jsx(PanelHeader, { title: "query", children: _jsxs(PanelHeaderActions, { children: [_jsx(PanelHeaderActionItem, { onClick: editWithQueryBuilder, title: "Edit query...", children: _jsx(PencilIcon, {}) }), _jsx(PanelHeaderActionItem, { onClick: clearQuery, title: "Clear query", children: _jsx(TimesIcon, {}) })] }) }), !isStubbed_RawLambda(queryState.query) && (_jsx(PanelContent, { children: _jsx("div", { className: "mapping-execution-builder__query-panel__query", children: _jsx(CodeEditor, { inputValue: queryState.lambdaString, isReadOnly: true, language: CODE_EDITOR_LANGUAGE.PURE, hideMinimap: true }) }) })), isStubbed_RawLambda(queryState.query) && (_jsx(PanelContent, { children: _jsx(PanelDropZone, { dropTargetConnector: dropConnector, isDragOver: isDragOver, children: _jsx(BlankPanelPlaceholder, { text: "Choose a class mapping", onClick: showClassMappingSelectorModal, clickActionType: "add", tooltipText: "Drop a class mapping, or click to choose one to start building the query", isDropZoneActive: 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(PanelContent, { className: "mapping-execution-builder__input-data-panel__content", children: _jsx(CodeEditor, { language: CODE_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(PanelContent, { className: "mapping-execution-builder__input-data-panel__content", children: _jsx(CodeEditor, { language: CODE_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(PanelContent, { className: "mapping-execution-builder__input-data-panel__content", children: _jsx(CodeEditor, { 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 }, dropConnector] = 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(PanelContent, { children: _jsx(PanelDropZone, { dropTargetConnector: dropConnector, isDragOver: isDragOver, children: _jsx(BlankPanelPlaceholder, { text: "Choose a class mapping", onClick: showClassMappingSelectorModal, clickActionType: "add", tooltipText: "Drop a class mapping, or click to choose one to generate input data", isDropZoneActive: canDrop }) }) }));
});
const RelationalMappingExecutionInputDataTypeSelector = observer((props) => {
const { inputDataState } = props;
const changeInputType = (val) => () => {
relationalInputData_setInputType(inputDataState.inputData, val);
};
return (_jsx(ControlledDropdownMenu, { className: "mapping-execution-builder__input-data-panel__type-selector", title: "Choose input data type...", 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", 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 cancelExecution = applicationStore.guardUnhandledError(() => flowResult(executionState.cancelExecution()));
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, {}) })), _jsx("div", { className: "mapping-execution-builder__action-btn btn__dropdown-combo btn__dropdown-combo--primary", children: executionState.isExecuting ? (_jsx("button", { className: "btn__dropdown-combo__canceler", onClick: cancelExecution, tabIndex: -1, children: _jsxs("div", { className: "btn--dark btn--caution btn__dropdown-combo__canceler__label", children: [_jsx(PauseCircleIcon, { className: "btn__dropdown-combo__canceler__label__icon" }), _jsx("div", { className: "btn__dropdown-combo__canceler__label__title", children: "Stop" })] }) })) : (_jsxs(_Fragment, { children: [_jsxs("button", { className: "btn__dropdown-combo__label", onClick: execute, disabled: isStubbed_RawLambda(queryState.query) ||
!inputDataState.isValid ||
executionState.isGeneratingPlan ||
executionState.isExecuting, tabIndex: -1, children: [_jsx(PlayIcon, { className: "btn__dropdown-combo__label__icon" }), _jsx("div", { className: "btn__dropdown-combo__label__title", children: "Run Query" })] }), _jsx(ControlledDropdownMenu, { className: "btn__dropdown-combo__dropdown-btn", disabled: isStubbed_RawLambda(queryState.query) ||
!inputDataState.isValid ||
executionState.isGeneratingPlan ||
executionState.isExecuting, content: _jsxs(MenuContent, { children: [_jsx(MenuContentItem, { className: "btn__dropdown-combo__option", onClick: generatePlan, children: "Generate Plan" }), _jsx(MenuContentItem, { className: "btn__dropdown-combo__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(Panel, { className: "mapping-execution-builder__result-panel", children: [_jsx(PanelHeader, { title: "result" }), _jsx(PanelContent, { className: "mapping-execution-builder__result-panel__content", children: _jsx(CodeEditor, { inputValue: executionResultText ?? '', isReadOnly: true, language: CODE_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