UNPKG

@finos/legend-application-studio

Version:
267 lines 20.5 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 { useState, useEffect, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { clsx, BlankPanelPlaceholder, ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, AsteriskIcon, LongArrowAltDownIcon, PencilEditIcon, PanelDropZone, PanelContent, PanelHeaderActionItem, PanelHeaderActions, PanelHeader, } from '@finos/legend-art'; import { CORE_DND_TYPE, } from '../../../../stores/editor/utils/DnDUtils.js'; import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js'; import { InstanceSetImplementationState, MappingElementState, } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingElementState.js'; import { PureInstanceSetImplementationState, } from '../../../../stores/editor/editor-state/element-editor-state/mapping/PureInstanceSetImplementationState.js'; import { guaranteeNonNullable, noop } from '@finos/legend-shared'; import { getMappingElementSource, MappingEditorState, getEmbeddedSetImplementations, } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingEditorState.js'; import { TypeTree } from './TypeTree.js'; import { FlatDataRecordTypeTree } from './FlatDataRecordTypeTree.js'; import { PropertyMappingEditor } from './PropertyMappingsEditor.js'; import { useDrop } from 'react-dnd'; import { FlatDataInstanceSetImplementationState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/FlatDataInstanceSetImplementationState.js'; import { MappingElementDecorationCleaner } from '../../../../stores/editor/editor-state/element-editor-state/mapping/MappingElementDecorator.js'; import { UnsupportedInstanceSetImplementationState } from '../../../../stores/editor/editor-state/element-editor-state/mapping/UnsupportedInstanceSetImplementationState.js'; import { UnsupportedEditorPanel } from '../UnsupportedElementEditor.js'; import { TableOrViewSourceTree } from './relational/TableOrViewSourceTree.js'; import { getSourceElementLabel, InstanceSetImplementationSourceSelectorModal, } from './InstanceSetImplementationSourceSelectorModal.js'; import { flowResult } from 'mobx'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { ActionAlertActionType, useApplicationStore, } from '@finos/legend-application'; import { Class, Type, FlatData, RootFlatDataRecordType, Table, Database, TableAlias, TableExplicitReference, ViewExplicitReference, getAllRecordTypes, getAllClassProperties, PrimitiveType, ConcreteFunctionDefinition, } from '@finos/legend-graph'; import { InlineLambdaEditor } from '@finos/legend-query-builder'; import { RelationTypeTree } from './RelationTypeTree.js'; export const InstanceSetImplementationSourceExplorer = observer((props) => { const { setImplementation, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const [sourceElementToFilter, setSourceElementToFilter] = useState(undefined); const mappingEditorState = editorStore.tabManagerState.getCurrentEditorState(MappingEditorState); const instanceSetImplementationState = mappingEditorState.currentTabState instanceof MappingElementState ? mappingEditorState.currentTabState : undefined; const srcElement = getMappingElementSource(setImplementation, editorStore.pluginManager.getApplicationPlugins()); const sourceLabel = getSourceElementLabel(srcElement); // `null` is when we want to open the modal using the existing source // `undefined` is to close the source modal // any other value to open the source modal using that value as the initial state of the modal const [sourceElementForSourceSelectorModal, setSourceElementForSourceSelectorModal,] = useState(); const CHANGING_SOURCE_ON_EMBEDDED = 'Changing source on mapping with embedded children will delete all its children'; const showSourceSelectorModal = () => { setSourceElementToFilter(undefined); if (!isReadOnly) { const embeddedSetImpls = getEmbeddedSetImplementations(setImplementation); if (!embeddedSetImpls.length) { setSourceElementForSourceSelectorModal(null); } else { applicationStore.alertService.setActionAlertInfo({ message: CHANGING_SOURCE_ON_EMBEDDED, actions: [ { label: 'Continue', handler: () => setSourceElementForSourceSelectorModal(null), type: ActionAlertActionType.PROCEED, }, { label: 'Cancel', }, ], }); } } }; const hideSourceSelectorModal = () => setSourceElementForSourceSelectorModal(undefined); // Drag and Drop const dndType = [ CORE_DND_TYPE.PROJECT_EXPLORER_CLASS, CORE_DND_TYPE.PROJECT_EXPLORER_FLAT_DATA, CORE_DND_TYPE.PROJECT_EXPLORER_DATABASE, CORE_DND_TYPE.PROJECT_EXPLORER_FUNCTION, ]; // smartly analyze the content of the source and automatically assign it or its sub-part // as class mapping source when possible const changeClassMappingSourceDriver = useCallback((droppedPackagableElement) => { if (droppedPackagableElement instanceof Class) { flowResult(mappingEditorState.changeClassMappingSourceDriver(setImplementation, droppedPackagableElement)).catch(applicationStore.alertUnhandledError); } else if (droppedPackagableElement instanceof FlatData) { const allRecordTypes = getAllRecordTypes(droppedPackagableElement); if (allRecordTypes.length === 0) { applicationStore.notificationService.notifyWarning(`Source flat-data store '${droppedPackagableElement.path}' must have at least one action`); return; } if (allRecordTypes.length === 1) { flowResult(mappingEditorState.changeClassMappingSourceDriver(setImplementation, allRecordTypes[0])).catch(applicationStore.alertUnhandledError); } else { setSourceElementForSourceSelectorModal(allRecordTypes[0]); } } else if (droppedPackagableElement instanceof Database) { setSourceElementToFilter(droppedPackagableElement); const relations = droppedPackagableElement.schemas.flatMap((schema) => schema.tables.concat(schema.views)); if (relations.length === 0) { applicationStore.notificationService.notifyWarning(`Source database '${droppedPackagableElement.path}' must have at least one table or view`); return; } const mainTableAlias = new TableAlias(); mainTableAlias.relation = relations[0] instanceof Table ? TableExplicitReference.create(relations[0]) : ViewExplicitReference.create(relations[0]); mainTableAlias.name = mainTableAlias.relation.value.name; if (relations.length === 1) { flowResult(mappingEditorState.changeClassMappingSourceDriver(setImplementation, mainTableAlias)).catch(applicationStore.alertUnhandledError); } else { setSourceElementForSourceSelectorModal(mainTableAlias); } } else if (droppedPackagableElement instanceof ConcreteFunctionDefinition) { flowResult(mappingEditorState.changeClassMappingSourceDriver(setImplementation, droppedPackagableElement)).catch(applicationStore.alertUnhandledError); } }, [applicationStore, mappingEditorState, setImplementation]); const handleDrop = useCallback((item) => { if (!setImplementation._isEmbedded && !isReadOnly) { const embeddedSetImpls = getEmbeddedSetImplementations(setImplementation); const droppedPackagableElement = item.data.packageableElement; if (!embeddedSetImpls.length) { changeClassMappingSourceDriver(droppedPackagableElement); } else { applicationStore.alertService.setActionAlertInfo({ message: CHANGING_SOURCE_ON_EMBEDDED, actions: [ { label: 'Continue', handler: () => changeClassMappingSourceDriver(droppedPackagableElement), type: ActionAlertActionType.PROCEED, }, { label: 'Cancel', }, ], }); } } }, [ changeClassMappingSourceDriver, applicationStore, isReadOnly, setImplementation, ]); const [{ isDragOver, canDrop }, dropConnector] = useDrop(() => ({ accept: dndType, drop: (item) => handleDrop(item), collect: (monitor) => ({ isDragOver: monitor.isOver({ shallow: true }), canDrop: monitor.canDrop(), }), }), [handleDrop]); const isUnsupported = instanceSetImplementationState instanceof UnsupportedInstanceSetImplementationState; if (!(instanceSetImplementationState instanceof InstanceSetImplementationState)) { return null; } const extraInstanceSetImplementationBlockingErrorCheckers = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraInstanceSetImplementationBlockingErrorCheckers?.() ?? []); let hasParseError = false; for (const checker of extraInstanceSetImplementationBlockingErrorCheckers) { const instanceSetImplementationBlockingErrorChecker = checker(instanceSetImplementationState); if (instanceSetImplementationBlockingErrorChecker) { hasParseError = instanceSetImplementationBlockingErrorChecker; } } return (_jsxs("div", { "data-testid": LEGEND_STUDIO_TEST_ID.SOURCE_PANEL, className: clsx('panel source-panel', { backdrop__element: (instanceSetImplementationState instanceof PureInstanceSetImplementationState ? instanceSetImplementationState.hasParserError : false) || (instanceSetImplementationState instanceof FlatDataInstanceSetImplementationState ? instanceSetImplementationState.hasParserError : false) || hasParseError, }), children: [_jsxs(PanelHeader, { children: [_jsxs("div", { className: "panel__header__title source-panel__header__title", children: [_jsx("div", { className: "panel__header__title__label", children: "source" }), _jsx("div", { className: "panel__header__title__content", children: sourceLabel })] }), _jsx(PanelHeaderActions, { children: _jsx(PanelHeaderActionItem, { onClick: showSourceSelectorModal, disabled: isReadOnly || setImplementation._isEmbedded || isUnsupported, title: "Select Source...", children: _jsx(PencilEditIcon, {}) }) })] }), _jsx(PanelContent, { children: _jsxs(PanelDropZone, { dropTargetConnector: dropConnector, isDragOver: isDragOver && !isReadOnly, children: [!isUnsupported && (_jsx(_Fragment, { children: srcElement ? (_jsxs("div", { className: "source-panel__explorer", children: [srcElement instanceof Type && (_jsx(TypeTree, { type: srcElement, selectedType: instanceSetImplementationState.selectedType })), srcElement instanceof RootFlatDataRecordType && (_jsx(FlatDataRecordTypeTree, { recordType: srcElement, selectedType: instanceSetImplementationState.selectedType })), srcElement instanceof TableAlias && (_jsx(TableOrViewSourceTree, { relation: srcElement.relation.value, selectedType: instanceSetImplementationState.selectedType })), srcElement instanceof ConcreteFunctionDefinition && (_jsx(RelationTypeTree, { relation: srcElement, selectedType: instanceSetImplementationState.selectedType, editorStore: editorStore }))] })) : (_jsx(BlankPanelPlaceholder, { text: "Choose a source", onClick: showSourceSelectorModal, clickActionType: "add", tooltipText: "Drop a class mapping source, or click to choose one", isDropZoneActive: canDrop, disabled: isReadOnly, previewText: "No source" })) })), isUnsupported && (_jsx(UnsupportedEditorPanel, { isReadOnly: isReadOnly, text: "Can't display class mapping source in form mode" })), sourceElementForSourceSelectorModal !== undefined && (_jsx(InstanceSetImplementationSourceSelectorModal, { mappingEditorState: mappingEditorState, setImplementation: setImplementation, sourceElementToSelect: sourceElementForSourceSelectorModal, closeModal: hideSourceSelectorModal, sourceElementToFilter: sourceElementToFilter }))] }) })] })); }); const MappingFilterEditor = observer(({ editorStore, filterState, instanceSetImplementationState, isReadOnly, }) => (_jsxs("div", { className: "panel class-mapping-editor__filter-panel", children: [_jsx("div", { className: "panel__header", children: _jsx("div", { className: "panel__header__title", children: _jsx("div", { className: "panel__header__title__content", children: "FILTER" }) }) }), _jsx("div", { className: clsx('property-mapping-editor', { backdrop__element: Boolean(filterState.parserError), }), children: _jsx("div", { className: "class-mapping-filter-editor__content", children: _jsx(InlineLambdaEditor, { className: "class-mapping-filter-editor__element__lambda-editor", disabled: isReadOnly || instanceSetImplementationState.isConvertingTransformLambdaObjects, forceBackdrop: Boolean(filterState.parserError), lambdaEditorState: filterState, expectedType: PrimitiveType.BOOLEAN }) }) })] }))); // Sort by property type/complexity (asc) const typeSorter = (a, b) => (a.genericType.value.rawType instanceof Class ? 1 : 0) - (b.genericType.value.rawType instanceof Class ? 1 : 0); // Sort by requiredness/multiplicity (desc) const requiredStatusSorter = (a, b) => (a.multiplicity.lowerBound > 0 ? 0 : 1) - (b.multiplicity.lowerBound > 0 ? 0 : 1); export const InstanceSetImplementationEditor = observer((props) => { const { setImplementation, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const mappingEditorState = editorStore.tabManagerState.getCurrentEditorState(MappingEditorState); const [sortByRequired, setSortByRequired] = useState(true); const instanceSetImplementationState = guaranteeNonNullable(mappingEditorState.currentTabState instanceof InstanceSetImplementationState ? mappingEditorState.currentTabState : undefined, 'Mapping element state for instance set implementation must be instance set implementation state'); const handleSortChange = () => setSortByRequired(!sortByRequired); // Get properties of supertypes const sortedProperties = getAllClassProperties(setImplementation.class.value) // LEVEL 1: sort properties by name .sort((a, b) => a.name.localeCompare(b.name)) // LEVEL 2: sort by properties by required/type (which ever is not chosen to be the primary sort) .sort(sortByRequired ? typeSorter : requiredStatusSorter) // LEVEL 3: sort by properties by required/type (primary sort) .sort(sortByRequired ? requiredStatusSorter : typeSorter); const isUnsupported = instanceSetImplementationState instanceof UnsupportedInstanceSetImplementationState; const renderFilterEditor = instanceSetImplementationState instanceof PureInstanceSetImplementationState && instanceSetImplementationState.mappingElement.filter; useEffect(() => { if (!isReadOnly) { instanceSetImplementationState.decorate(); } flowResult(instanceSetImplementationState.convertPropertyMappingTransformObjects()).catch(applicationStore.alertUnhandledError); if (instanceSetImplementationState instanceof PureInstanceSetImplementationState && instanceSetImplementationState.mappingElement.filter) { flowResult(instanceSetImplementationState.convertFilter()).catch(applicationStore.alertUnhandledError); } return isReadOnly ? noop() : () => setImplementation.accept_SetImplementationVisitor(new MappingElementDecorationCleaner(editorStore)); }, [ applicationStore, setImplementation, isReadOnly, instanceSetImplementationState, editorStore, ]); useEffect(() => { instanceSetImplementationState.setSelectedType(undefined); }, [instanceSetImplementationState]); return (_jsx("div", { className: "mapping-element-editor__content", children: _jsxs(ResizablePanelGroup, { orientation: "vertical", children: [_jsx(ResizablePanel, { minSize: 300, children: _jsxs(ResizablePanelGroup, { orientation: "horizontal", children: [_jsx(ResizablePanel, { minSize: 300, children: _jsxs("div", { className: "panel class-mapping-editor__property-panel", children: [_jsxs(PanelHeader, { children: [_jsx("div", { className: "panel__header__title", children: _jsx("div", { className: "panel__header__title__content", children: "PROPERTIES" }) }), _jsx(PanelHeaderActions, { children: _jsx("div", { className: "panel__header__action", children: _jsxs("div", { className: `class-mapping-editor__sort-by-required-btn ${sortByRequired ? 'class-mapping-editor__sort-by-required-btn--enabled' : ''}`, onClick: handleSortChange, children: [_jsx(LongArrowAltDownIcon, {}), _jsx(AsteriskIcon, {})] }) }) })] }), _jsxs(PanelContent, { children: [!isReadOnly && !isUnsupported && sortedProperties.map((property) => (_jsx(PropertyMappingEditor, { property: property, instanceSetImplementationState: instanceSetImplementationState, isReadOnly: isReadOnly }, property.name))), isReadOnly && !isUnsupported && sortedProperties // for property without any property mapping in readonly mode, we won't show it .filter((p) => instanceSetImplementationState.propertyMappingStates.filter((pm) => pm.propertyMapping.property.value.name === p.name).length) .map((property) => (_jsx(PropertyMappingEditor, { property: property, instanceSetImplementationState: instanceSetImplementationState, isReadOnly: isReadOnly }, property.name))), isUnsupported && (_jsx(UnsupportedEditorPanel, { isReadOnly: isReadOnly, text: "Can't display class mapping in form mode" }))] })] }) }), _jsx(ResizablePanelSplitter, {}), renderFilterEditor && instanceSetImplementationState.mappingFilterState && (_jsx(ResizablePanel, { size: 330, minSize: 80, children: _jsx(MappingFilterEditor, { editorStore: editorStore, instanceSetImplementationState: instanceSetImplementationState, filterState: instanceSetImplementationState.mappingFilterState, isReadOnly: isReadOnly }) }))] }) }), _jsx(ResizablePanelSplitter, {}), _jsx(ResizablePanel, { size: 300, minSize: 300, children: _jsx(InstanceSetImplementationSourceExplorer, { setImplementation: setImplementation, isReadOnly: isReadOnly }) })] }) })); }); //# sourceMappingURL=InstanceSetImplementationEditor.js.map