/** * 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 { observer } from 'mobx-react-lite'; import { type Class, type MappingTestSuite, type Store, type DataElement, type EmbeddedData, ModelStore, Database, getMappingCompatibleClasses, isStubbed_RawLambda, stub_RawLambda, MappingTest, PackageableElementExplicitReference, DataElementReference, RelationalCSVData, ModelStoreData, ExternalFormatData, ModelEmbeddedData, } from '@finos/legend-graph'; import { forwardRef, useEffect, useRef, useState } from 'react'; import { BlankPanelPlaceholder, CaretDownIcon, clsx, compareLabelFn, CustomSelectorInput, Dialog, ControlledDropdownMenu, MenuContent, MenuContentItem, Modal, ModalBody, ModalFooter, ModalFooterButton, ModalTitle, PanelContent, PanelFormTextField, PencilIcon, PlusIcon, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, ResizablePanelSplitterLine, RunAllIcon, RunErrorsIcon, type SelectComponent, PanelLoadingIndicator, ContextMenu, ModalHeader, PanelFormSection, PlayIcon, } from '@finos/legend-art'; import { assertErrorThrown, filterByType, prettyCONSTName, returnUndefOnError, uniq, } from '@finos/legend-shared'; import type { StoreTestDataState, MappingTestableState, MappingTestState, MappingTestSuiteState, CreateSuiteState, } from '../../../../stores/editor/editor-state/element-editor-state/mapping/testable/MappingTestableState.js'; import { useApplicationNavigationContext, useApplicationStore, } from '@finos/legend-application'; import { flowResult } from 'mobx'; import { ExecutionPlanViewer, QueryBuilderTextEditorMode, type QueryBuilderState, } from '@finos/legend-query-builder'; import { MappingExecutionQueryBuilderState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingExecutionQueryBuilderState.js'; import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor'; import { CodeEditor } from '@finos/legend-lego/code-editor'; import { atomicTest_setDoc } from '../../../../stores/graph-modifier/Testable_GraphModifierHelper.js'; import { RenameModal, SharedDataElementModal, TestAssertionEditor, } from '../testable/TestableSharedComponents.js'; import { EmbeddedDataEditor } from '../data-editor/EmbeddedDataEditor.js'; import { TESTABLE_RESULT, getTestableResultFromTestResult, getTestableResultFromTestResults, } from '../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js'; import { getTestableResultIcon } from '../../side-bar/testable/GlobalTestRunner.js'; import { EmbeddedDataCreatorFromEmbeddedData, validateTestableId, } from '../../../../stores/editor/utils/TestableUtils.js'; import { getMappingStores } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; import type { DSL_Data_LegendStudioApplicationPlugin_Extension } from '../../../../stores/extensions/DSL_Data_LegendStudioApplicationPlugin_Extension.js'; import { EmbeddedDataType } from '../../../../stores/editor/editor-state/ExternalFormatState.js'; import type { EmbeddedDataTypeOption } from '../../../../stores/editor/editor-state/element-editor-state/data/DataEditorState.js'; import { buildElementOption, getPackageableElementOptionFormatter, type PackageableElementOption, } from '@finos/legend-lego/graph-editor'; import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../../__lib__/LegendStudioApplicationNavigationContext.js'; import { TestExecutionPlanDebugState, TestUnknownDebugState, type TestableDebugTestResultState, } from '../../../../stores/editor/editor-state/element-editor-state/testable/TestableEditorState.js'; interface ClassSelectOption { label: string; value: Class; } const CreateTestSuiteModal = observer( (props: { creatorState: CreateSuiteState }) => { const { creatorState } = props; const mappingTestableState = creatorState.mappingTestableState; const mappingEditorState = mappingTestableState.mappingEditorState; const mapping = mappingEditorState.mapping; const editorStore = mappingEditorState.editorStore; const applicationStore = editorStore.applicationStore; // Class mapping selector const compatibleClasses = getMappingCompatibleClasses( mapping, editorStore.graphManagerState.usableClasses, ); const inputRef = useRef(null); const handleEnter = (): void => inputRef.current?.focus(); const mappedClassOptions = uniq(compatibleClasses) .map((e) => ({ label: e.name, value: e, })) .sort(compareLabelFn); const [selectedClass, setSelectedClass] = useState( compatibleClasses[0], ); const selectedClassOption = selectedClass ? { value: selectedClass, label: selectedClass.name, } : null; // init states const [suiteName, setSuiteName] = useState(undefined); const [testName, setTestName] = useState(undefined); const isValid = selectedClass && suiteName && testName; const changeClassOption = (val: ClassSelectOption | null): void => { if (val?.value) { setSelectedClass(val.value); } else { setSelectedClass(undefined); } }; // model const close = (): void => mappingTestableState.closeCreateModal(); const create = (): void => { if (selectedClass && suiteName && testName) { flowResult( creatorState.createAndAddTestSuite( selectedClass, suiteName, testName, ), ).catch(editorStore.applicationStore.alertUnhandledError); } }; return ( {creatorState.isCreatingSuiteState.message && (
{`${creatorState.isCreatingSuiteState.message}...`}
)} setSuiteName(value ?? '') } errorMessage={validateTestableId(suiteName, undefined)} /> setTestName(value ?? '') } errorMessage={validateTestableId(testName, undefined)} />
Class Mapping
Mapped Class for which you would like to build test suite for
); }, ); const CreateTestModal = observer( (props: { mappingSuiteState: MappingTestSuiteState }) => { const { mappingSuiteState } = props; const mapping = mappingSuiteState.mappingTestableState.mapping; const suite = mappingSuiteState.suite; const testData = suite.tests.filter(filterByType(MappingTest))[0] ?.storeTestData[0]; const editorStore = mappingSuiteState.editorStore; const applicationStore = editorStore.applicationStore; // test name const [id, setId] = useState(undefined); const isValid = id && !id.includes(' '); const errorMessage = validateTestableId( id, suite.tests.map((t) => t.id), ); const mappedClassSelectorRef = useRef(null); // const firstTest = mapp // Class mapping selector const compatibleClasses = getMappingCompatibleClasses( mapping, editorStore.graphManagerState.usableClasses, ); const mappedClassOptions = uniq(compatibleClasses) .map((e) => ({ label: e.name, value: e, })) .sort(compareLabelFn); // class if required const [selectedClass, setSelectedClass] = useState( compatibleClasses[0], ); const changeClassOption = (val: ClassSelectOption | null): void => { setSelectedClass(val?.value); }; const selectedClassOption = selectedClass ? { value: selectedClass, label: selectedClass.name, } : null; // model const close = (): void => mappingSuiteState.setShowModal(false); const create = (): void => { if (id) { mappingSuiteState.addNewTest(id, selectedClass); close(); } }; return ( setId(value ?? '')} errorMessage={errorMessage} /> {!testData && (
Class Mapping
Mapped Class for which you would like to build test suite for
)}
); }, ); interface StoreSelectOption { label: string; value: Store; } const CreateStoreTestDataModal = observer( (props: { mappingTestState: MappingTestState }) => { const { mappingTestState } = props; const mappingTestableDataState = mappingTestState.dataState; const editorStore = mappingTestableDataState.editorStore; const applicationStore = editorStore.applicationStore; const mapping = mappingTestableDataState.mappingTestableState.mapping; const isReadOnly = mappingTestableDataState.mappingTestableState.mappingEditorState .isReadOnly; // options const dataElementOptions = editorStore.graphManagerState.usableDataElements.map(buildElementOption); const extraOptionTypes = editorStore.pluginManager .getApplicationPlugins() .flatMap( (plugin) => ( plugin as DSL_Data_LegendStudioApplicationPlugin_Extension ).getExtraEmbeddedDataTypeOptions?.() ?? [], ); const embeddedOptions = [ ...Object.values(EmbeddedDataType) .filter( (e) => e !== EmbeddedDataType.DATA_ELEMENT || dataElementOptions.length, ) .map((typeOption) => ({ label: prettyCONSTName(typeOption), value: typeOption, })), ...extraOptionTypes, ]; const getDefaultTypeFromStore = ( _store: Store | undefined, ): string | undefined => { if (_store instanceof ModelStore) { return EmbeddedDataType.EXTERNAL_FORMAT_DATA; } else if (_store instanceof Database) { return EmbeddedDataType.RELATIONAL_CSV; } return undefined; }; const compatibleStores = Array.from( getMappingStores( mapping, editorStore.pluginManager.getApplicationPlugins(), ), ); // set states const [selectedStore, setSelectedStore] = useState( compatibleStores[0], ); const [embeddedDataType, setEmbeddedDataType] = useState< string | undefined >(getDefaultTypeFromStore(selectedStore) ?? embeddedOptions[0]?.value); // data reference const [dataElement, setDataElement] = useState( dataElementOptions[0]?.value, ); const selectedDataElement = dataElement ? buildElementOption(dataElement) : null; const onDataElementChange = ( val: PackageableElementOption | null, ): void => { setDataElement(val?.value); }; // data type const onEmbeddedTypeChange = (val: EmbeddedDataTypeOption | null): void => { setEmbeddedDataType(val?.value); }; const embeddedDataTypeOption = embeddedDataType ? { value: embeddedDataType, label: prettyCONSTName(embeddedDataType), } : undefined; // store const mappedStoreRef = useRef(null); const selectedStoreOptions = compatibleStores .map((e) => ({ label: e.name, value: e, })) .sort(compareLabelFn); const selectedStoreOption = selectedStore ? { value: selectedStore, label: selectedStore.name, } : null; const changeStoreOption = (val: StoreSelectOption | null): void => { setSelectedStore(val?.value); const store = val?.value; if (store instanceof ModelStore) { setEmbeddedDataType(EmbeddedDataType.EXTERNAL_FORMAT_DATA); } else if (store instanceof Database) { setEmbeddedDataType(EmbeddedDataType.RELATIONAL_CSV); } }; // validation const isValid = (Boolean(selectedStore) && Boolean(embeddedDataType)) || (embeddedDataType === EmbeddedDataType.DATA_ELEMENT && dataElement); const close = (): void => mappingTestableDataState.setShowModal(false); const create = (): void => { if (selectedStore && embeddedDataType) { mappingTestableDataState.addStoreTestData( selectedStore, embeddedDataType, dataElement, ); close(); } }; return (
Store to add test data
Data Type
Test data type that will be used to mock store (defaulted based on store)
{embeddedDataType === EmbeddedDataType.DATA_ELEMENT && (
Data Element
)}
); }, ); const MappingTestSuiteQueryEditor = observer( (props: { testSuiteState: MappingTestSuiteState; mappingTestableState: MappingTestableState; isReadOnly: boolean; }) => { const { testSuiteState, mappingTestableState, isReadOnly } = props; const testableQueryState = testSuiteState.queryState; const mapping = mappingTestableState.mapping; const editorStore = mappingTestableState.mappingEditorState.editorStore; const applicationStore = useApplicationStore(); // actions const editWithQueryBuilder = (openInTextMode = false): (() => void) => applicationStore.guardUnhandledError(async () => { const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ setupQueryBuilderState: async (): Promise => { const queryBuilderState = new MappingExecutionQueryBuilderState( embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, mapping, editorStore.applicationStore.config.options.queryBuilderConfig, editorStore.editorMode.getSourceInfo(), ); queryBuilderState.initializeWithQuery(testableQueryState.query); if (openInTextMode) { queryBuilderState.textEditorState.openModal( QueryBuilderTextEditorMode.TEXT, ); } return queryBuilderState; }, actionConfigs: [ { key: 'save-query-btn', renderer: ( queryBuilderState: QueryBuilderState, ): React.ReactNode => { const save = applicationStore.guardUnhandledError( async (): Promise => { try { const rawLambda = queryBuilderState.buildQuery(); await flowResult( testableQueryState.updateLamba(rawLambda), ); applicationStore.notificationService.notifySuccess( `Mapping testable query is updated`, ); embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration( undefined, ); } catch (error) { assertErrorThrown(error); applicationStore.notificationService.notifyError( `Can't save query: ${error.message}`, ); } }, ); return ( ); }, }, ], disableCompile: isStubbed_RawLambda(testableQueryState.query), }), ); }); const clearQuery = applicationStore.guardUnhandledError(() => flowResult(testableQueryState.updateLamba(stub_RawLambda())), ); // debug const disableDebug = !testSuiteState.selectTestState; const debugQuery = (): void => { flowResult(testSuiteState.debug()).catch( testSuiteState.editorStore.applicationStore.alertUnhandledError, ); }; const debugState = testSuiteState.selectTestState?.debugTestResultState; const renderDebug = ( val: TestableDebugTestResultState, ): React.ReactNode => { const closeDebug = (): void => testSuiteState.selectTestState?.setDebugState(undefined); if (val instanceof TestExecutionPlanDebugState) { return ( ); } if (val instanceof TestUnknownDebugState) { return (
{`Debug`}
Close
); } return null; }; return (
query
Text Mode Clear Query } menuProps={{ anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, }} >
{!isStubbed_RawLambda(testableQueryState.query) && (
)} {debugState && renderDebug(debugState)}
); }, ); const StoreTestDataEditor = observer( (props: { mappingTestState: MappingTestState; storeTestDataState: StoreTestDataState; }) => { const { mappingTestState, storeTestDataState } = props; const isReadOnly = mappingTestState.mappingTestableState.mappingEditorState.isReadOnly; const dataElements = storeTestDataState.editorStore.graphManagerState.graph.dataElements; const currentData = storeTestDataState.storeTestData.data; const isUsingReference = currentData instanceof DataElementReference; const open = (): void => storeTestDataState.setDataElementModal(true); const close = (): void => storeTestDataState.setDataElementModal(false); const changeToUseMyOwn = (): void => { if (isUsingReference) { const newBare = returnUndefOnError(() => currentData.accept_EmbeddedDataVisitor( new EmbeddedDataCreatorFromEmbeddedData( mappingTestState.editorStore, ), ), ); if (newBare) { storeTestDataState.changeEmbeddedData(newBare); } } }; const sharedDataHandler = (val: DataElement): void => { const dataRef = new DataElementReference(); dataRef.dataElement = PackageableElementExplicitReference.create(val); const dataElementValue = val.data; let embeddedData: EmbeddedData = dataRef; if ( currentData instanceof ModelStoreData && dataElementValue instanceof ExternalFormatData ) { const modelStoreVal = currentData.modelData?.[0]; if (modelStoreVal instanceof ModelEmbeddedData) { const newModelEmbeddedData = new ModelEmbeddedData(); newModelEmbeddedData.model = PackageableElementExplicitReference.create( modelStoreVal.model.value, ); newModelEmbeddedData.data = dataRef; const modelStoreData = new ModelStoreData(); modelStoreData.modelData = [newModelEmbeddedData]; embeddedData = modelStoreData; } } storeTestDataState.changeEmbeddedData(embeddedData); }; const filter = (val: DataElement): boolean => { const dataElementData = val.data; if (currentData instanceof RelationalCSVData) { if (dataElementData instanceof RelationalCSVData) { return true; } return false; } else if (currentData instanceof ModelStoreData) { if ( dataElementData instanceof ExternalFormatData || dataElementData instanceof ModelStoreData ) { return true; } return false; } // TODO add extensions return true; }; return (
input data
{isUsingReference ? ( ) : ( )}
{storeTestDataState.dataElementModal && ( )}
); }, ); const MappingTestEditor = observer( (props: { mappingTestState: MappingTestState }) => { const { mappingTestState } = props; const mappingTest = mappingTestState.test; const mappingTestableDataState = mappingTestState.dataState; const addStoreTestData = (): void => { mappingTestableDataState.setShowModal(true); }; return (
Test Documentation