@finos/legend-application-studio
Version:
Legend Studio application core
267 lines • 20.5 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, 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