UNPKG

@finos/legend-studio

Version:
238 lines 21.7 kB
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 { Fragment, useState, useEffect, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { MAPPING_TEST_EDITOR_TAB_TYPE, TEST_RESULT, MappingTestObjectInputDataState, MappingTestFlatDataInputDataState, MappingTestExpectedOutputAssertionState, MappingTestRelationalInputDataState, } from '../../../../stores/editor-state/element-editor-state/mapping/MappingTestState.js'; import { JsonDiffView } from '../../../shared/DiffView.js'; import { clsx, PanelLoadingIndicator, BlankPanelPlaceholder, TimesIcon, PlayIcon, ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, ResizablePanelSplitterLine, DropdownMenu, MenuContent, MenuContentItem, CaretDownIcon, ErrorIcon, RefreshIcon, WrenchIcon, } from '@finos/legend-art'; import { useDrop } from 'react-dnd'; import { CORE_DND_TYPE, } from '../../../../stores/shared/DnDUtil.js'; import { IllegalStateError, guaranteeType, tryToFormatLosslessJSONString, } from '@finos/legend-shared'; import { EDITOR_LANGUAGE, useApplicationStore, ActionAlertActionType, ActionAlertType, ExecutionPlanViewer, } from '@finos/legend-application'; import { ClassMappingSelectorModal, getRelationalInputTestDataEditorLanguage, } from './MappingExecutionBuilder.js'; import { flowResult } from 'mobx'; import { MappingTestStatusIndicator } from './MappingTestsExplorer.js'; import { getMappingElementSource, getMappingElementTarget, } from '../../../../stores/editor-state/element-editor-state/mapping/MappingEditorState.js'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { Class, SetImplementation, OperationSetImplementation, RelationalInputType, stub_RawLambda, isStubbed_RawLambda, DEPRECATED__validate_MappingTestAssert, } from '@finos/legend-graph'; import { StudioTextInputEditor } from '../../../shared/StudioTextInputEditor.js'; import { flatData_setData } from '../../../../stores/graphModifier/StoreFlatData_GraphModifierHelper.js'; import { relationalInputData_setData, relationalInputData_setInputType, } from '../../../../stores/graphModifier/StoreRelational_GraphModifierHelper.js'; const MappingTestQueryEditor = observer((props) => { const { testState, isReadOnly } = props; const queryState = testState.queryState; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const extraQueryEditorActions = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraMappingTestQueryEditorActionConfigurations?.() ?? []) .map((config) => (_jsx(Fragment, { children: config.renderer(testState, isReadOnly) }, 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 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 (setImplementation) { editorStore.setActionAlertInfo({ message: 'Mapping test 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: () => testState.setInputDataStateBasedOnSource(getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins()), true), }, { label: 'Keep my input data', type: ActionAlertActionType.PROCEED, default: true, }, ], }); } }, [applicationStore, editorStore, testState, 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(testState.queryState.updateLamba(stub_RawLambda()))); return (_jsxs("div", { className: "panel mapping-test-editor__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, disabled: isReadOnly, onClick: clearQuery, title: 'Clear query', children: _jsx(TimesIcon, {}) })] })] }), !isStubbed_RawLambda(queryState.query) && (_jsx("div", { className: "panel__content", children: _jsx("div", { className: "mapping-test-editor__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: testState.mappingEditorState, hideClassMappingSelectorModal: hideClassMappingSelectorModal, changeClassMapping: changeClassMapping }))] })); }); export const MappingTestObjectInputDataBuilder = observer((props) => { const { inputDataState, isReadOnly } = props; // TODO?: handle XML/type // Input data const updateInput = (val) => inputDataState.setData(val); return (_jsx("div", { className: "panel__content mapping-test-editor__input-data-panel__content", children: _jsx(StudioTextInputEditor, { language: EDITOR_LANGUAGE.JSON, inputValue: inputDataState.data, isReadOnly: isReadOnly, updateInput: updateInput }) })); }); export const MappingTestFlatDataInputDataBuilder = observer((props) => { const { inputDataState, isReadOnly } = props; // Input data const updateInput = (val) => flatData_setData(inputDataState.inputData, val); return (_jsx("div", { className: "panel__content mapping-test-editor__input-data-panel__content", children: _jsx(StudioTextInputEditor, { language: EDITOR_LANGUAGE.TEXT, inputValue: inputDataState.inputData.data, isReadOnly: isReadOnly, updateInput: updateInput }) })); }); /** * Right now, we always default this to use Local H2 connection. */ export const MappingTestRelationalInputDataBuilder = observer((props) => { const { inputDataState, isReadOnly } = props; // Input data const updateInput = (val) => relationalInputData_setData(inputDataState.inputData, val); return (_jsx("div", { className: "panel__content mapping-test-editor__input-data-panel__content", children: _jsx(StudioTextInputEditor, { language: getRelationalInputTestDataEditorLanguage(inputDataState.inputData.inputType), inputValue: inputDataState.inputData.data, isReadOnly: isReadOnly, updateInput: updateInput }) })); }); const RelationalMappingTestInputDataTypeSelector = observer((props) => { const { inputDataState, isReadOnly } = props; const changeInputType = (val) => () => { relationalInputData_setInputType(inputDataState.inputData, val); }; return (_jsx(DropdownMenu, { className: "mapping-test-editor__input-data-panel__type-selector", disabled: isReadOnly, content: _jsx(MenuContent, { children: Object.keys(RelationalInputType).map((mode) => (_jsx(MenuContentItem, { className: "mapping-test-editor__input-data-panel__type-selector__option", onClick: changeInputType(mode), children: mode }, mode))) }), children: _jsxs("div", { className: "mapping-test-editor__input-data-panel__type-selector__value", title: "Choose input data type...", children: [_jsx("div", { className: "mapping-test-editor__input-data-panel__type-selector__value__label", children: inputDataState.inputData.inputType }), _jsx(CaretDownIcon, {})] }) })); }); export const MappingTestInputDataBuilder = observer((props) => { const { testState, isReadOnly } = props; const inputDataState = testState.inputDataState; const editorStore = useEditorStore(); // Class mapping selector const [openClassMappingSelectorModal, setOpenClassMappingSelectorModal] = useState(false); const showClassMappingSelectorModal = () => setOpenClassMappingSelectorModal(true); const hideClassMappingSelectorModal = () => setOpenClassMappingSelectorModal(false); const changeClassMapping = useCallback((setImplementation) => { testState.setInputDataStateBasedOnSource(setImplementation ? getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins()) : undefined, true); hideClassMappingSelectorModal(); }, [testState, editorStore]); const classMappingFilterFn = (setImp) => !(setImp instanceof OperationSetImplementation); // input data builder let inputDataBuilder; if (inputDataState instanceof MappingTestObjectInputDataState) { inputDataBuilder = (_jsx(MappingTestObjectInputDataBuilder, { inputDataState: inputDataState, isReadOnly: isReadOnly })); } else if (inputDataState instanceof MappingTestFlatDataInputDataState) { inputDataBuilder = (_jsx(MappingTestFlatDataInputDataBuilder, { inputDataState: inputDataState, isReadOnly: isReadOnly })); } else if (inputDataState instanceof MappingTestRelationalInputDataState) { inputDataBuilder = (_jsx(MappingTestRelationalInputDataBuilder, { inputDataState: inputDataState, isReadOnly: isReadOnly })); } else { inputDataBuilder = null; } // input type let inputTypeSelector; if (inputDataState instanceof MappingTestRelationalInputDataState) { inputTypeSelector = (_jsx(RelationalMappingTestInputDataTypeSelector, { inputDataState: inputDataState, isReadOnly: isReadOnly })); } else { inputTypeSelector = null; } return (_jsxs("div", { className: "panel mapping-test-editor__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, disabled: isReadOnly, onClick: showClassMappingSelectorModal, title: 'Regenerate...', children: _jsx(RefreshIcon, { className: "mapping-test-editor__icon--refresh" }) })] })] }), inputDataBuilder, openClassMappingSelectorModal && (_jsx(ClassMappingSelectorModal, { mappingEditorState: testState.mappingEditorState, hideClassMappingSelectorModal: hideClassMappingSelectorModal, changeClassMapping: changeClassMapping, classMappingFilterFn: classMappingFilterFn }))] })); }); export const MappingTestExpectedOutputAssertionBuilder = observer((props) => { const { testState, assertionState, isReadOnly } = props; const applicationStore = useApplicationStore(); const validationResult = DEPRECATED__validate_MappingTestAssert(testState.test); const isValid = !validationResult; // Expected Result const updateExpectedResult = (val) => { assertionState.setExpectedResult(val); testState.updateAssertion(); }; const formatExpectedResultJSONString = () => assertionState.setExpectedResult(tryToFormatLosslessJSONString(assertionState.expectedResult)); // Actions const regenerateExpectedResult = applicationStore.guardUnhandledError(() => flowResult(testState.regenerateExpectedResult())); return (_jsxs("div", { className: "panel mapping-test-editor__result-panel", children: [_jsxs("div", { className: "panel__header", children: [_jsx("div", { className: "panel__header__title", children: _jsx("div", { className: "panel__header__title__label", children: "expected" }) }), _jsxs("div", { className: "panel__header__actions", children: [_jsx("button", { className: "panel__header__action", disabled: testState.isExecutingTest || isReadOnly, onClick: regenerateExpectedResult, tabIndex: -1, title: 'Regenerate Result', children: _jsx(RefreshIcon, { className: "mapping-test-editor__icon__regenerate-result" }) }), _jsx("button", { className: "panel__header__action", disabled: isReadOnly, tabIndex: -1, onClick: formatExpectedResultJSONString, title: 'Format JSON', children: _jsx(WrenchIcon, {}) })] })] }), _jsxs("div", { className: clsx('panel__content mapping-test-editor__text-editor mapping-test-editor__result-panel__content', { 'panel__content--has-validation-error': !isValid }), children: [!isValid && (_jsx("div", { className: "panel__content__validation-error", title: validationResult.messages.join('\n'), children: _jsx(ErrorIcon, {}) })), _jsx(StudioTextInputEditor, { inputValue: assertionState.expectedResult, updateInput: updateExpectedResult, isReadOnly: isReadOnly, language: EDITOR_LANGUAGE.JSON })] })] })); }); export const MappingTestAssertionBuilder = observer((props) => { const { testState, isReadOnly } = props; const assertionState = testState.assertionState; if (assertionState instanceof MappingTestExpectedOutputAssertionState) { return (_jsx(MappingTestExpectedOutputAssertionBuilder, { testState: testState, assertionState: assertionState, isReadOnly: isReadOnly })); } return null; }); export const MappingTestBuilder = observer((props) => { const { testState, isReadOnly } = props; const applicationStore = useApplicationStore(); // In case we switch out to another tab to do editing on some class, we want to refresh the test state data so that we can detect problem in deep fetch tree useEffect(() => { flowResult(testState.onTestStateOpen()).catch(applicationStore.alertUnhandledError); }, [applicationStore, testState]); return (_jsxs("div", { className: "mapping-test-editor", children: [_jsx(PanelLoadingIndicator, { isLoading: testState.isExecutingTest }), _jsxs(ResizablePanelGroup, { orientation: "horizontal", children: [_jsx(ResizablePanel, { size: 250, minSize: 28, children: _jsx(MappingTestQueryEditor, { testState: testState, isReadOnly: isReadOnly }, testState.queryState.uuid) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: "var(--color-dark-grey-50)" }) }), _jsx(ResizablePanel, { size: 250, minSize: 28, children: _jsx(MappingTestInputDataBuilder, { testState: testState, isReadOnly: isReadOnly }, testState.inputDataState.uuid) }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: "var(--color-dark-grey-50)" }) }), _jsx(ResizablePanel, { minSize: 28, children: _jsx(MappingTestAssertionBuilder, { testState: testState, isReadOnly: isReadOnly }, testState.assertionState.uuid) })] })] })); }); export const MappingTestEditor = observer((props) => { const { testState, isReadOnly } = props; const applicationStore = useApplicationStore(); const selectedTab = testState.selectedTab; const changeTab = (tab) => () => testState.setSelectedTab(tab); // execute const runTest = applicationStore.guardUnhandledError(() => flowResult(testState.runTest())); const executionPlanState = testState.executionPlanState; const generatePlan = applicationStore.guardUnhandledError(() => flowResult(testState.generatePlan(false))); const debugPlanGeneration = applicationStore.guardUnhandledError(() => flowResult(testState.generatePlan(true))); // Test Result let testResult = ''; switch (testState.result) { case TEST_RESULT.NONE: testResult = 'Test did not run'; break; case TEST_RESULT.FAILED: testResult = `Test failed in ${testState.runTime}ms, see comparison (expected <-> actual) below:`; break; case TEST_RESULT.PASSED: testResult = `Test passed in ${testState.runTime}ms`; break; case TEST_RESULT.ERROR: testResult = `Test failed in ${testState.runTime}ms due to error:\n${testState.errorRunningTest?.message ?? '(unknown)'}`; break; default: throw new IllegalStateError('Unknown test result state'); } testResult = testState.isRunningTest ? 'Running test...' : testResult; return (_jsxs("div", { className: "mapping-test-editor", children: [_jsxs("div", { className: "mapping-test-editor__header", children: [_jsx("div", { className: "mapping-test-editor__header__tabs", children: Object.values(MAPPING_TEST_EDITOR_TAB_TYPE).map((tab) => (_jsxs("div", { onClick: changeTab(tab), className: clsx('mapping-test-editor__header__tab', { 'mapping-test-editor__header__tab--active': tab === selectedTab, }), children: [tab === MAPPING_TEST_EDITOR_TAB_TYPE.RESULT && (_jsx("div", { className: "mapping-test-editor__header__tab__test-status-indicator__container", children: _jsx(MappingTestStatusIndicator, { testState: testState }) })), tab] }, tab))) }), _jsx("div", { className: "mapping-test-editor__header__actions", children: _jsxs("button", { className: "mapping-test-editor__execute-btn", onClick: runTest, disabled: testState.isRunningTest || testState.isExecutingTest || testState.isGeneratingPlan, tabIndex: -1, children: [_jsxs("div", { className: "mapping-test-editor__execute-btn__label", children: [_jsx(PlayIcon, { className: "mapping-test-editor__execute-btn__label__icon" }), _jsx("div", { className: "mapping-test-editor__execute-btn__label__title", children: "Run Test" })] }), _jsx(DropdownMenu, { className: "mapping-test-editor__execute-btn__dropdown-btn", disabled: testState.isRunningTest || testState.isExecutingTest || testState.isGeneratingPlan, content: _jsxs(MenuContent, { children: [_jsx(MenuContentItem, { className: "mapping-test-editor__execute-btn__option", onClick: generatePlan, children: "Generate Plan" }), _jsx(MenuContentItem, { className: "mapping-test-editor__execute-btn__option", onClick: debugPlanGeneration, children: "Debug" })] }), menuProps: { anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, }, children: _jsx(CaretDownIcon, {}) })] }) })] }), _jsxs("div", { className: "mapping-test-editor__content", children: [selectedTab === MAPPING_TEST_EDITOR_TAB_TYPE.SETUP && (_jsx(MappingTestBuilder, { testState: testState, isReadOnly: isReadOnly })), selectedTab === MAPPING_TEST_EDITOR_TAB_TYPE.RESULT && (_jsxs("div", { className: "mapping-test-editor__result", children: [_jsx("div", { className: `mapping-test-editor__result__status mapping-test-editor__result__status--${testState.isRunningTest ? 'running' : testState.result.toLowerCase()}`, children: testResult }), testState.result === TEST_RESULT.FAILED && (_jsx(_Fragment, { children: testState.assertionState instanceof MappingTestExpectedOutputAssertionState && (_jsx("div", { className: "mapping-test-editor__result__diff", children: _jsx(JsonDiffView, { from: testState.assertionState.expectedResult, to: testState.testExecutionResultText, lossless: true }) })) }))] }))] }), _jsx(ExecutionPlanViewer, { executionPlanState: executionPlanState })] })); }); //# sourceMappingURL=MappingTestEditor.js.map