/** * 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 { useEffect, useCallback, useState, forwardRef } from 'react'; import { observer } from 'mobx-react-lite'; import { ServiceEditorState } from '../../../../stores/editor/editor-state/element-editor-state/service/ServiceEditorState.js'; import { type ServiceExecutionContextState, SingleServicePureExecutionState, ServicePureExecutionState, MultiServicePureExecutionState, InlineServicePureExecutionState, } from '../../../../stores/editor/editor-state/element-editor-state/service/ServiceExecutionState.js'; import { EmbeddedRuntimeEditor } from '../RuntimeEditor.js'; import { useDrop } from 'react-dnd'; import { CORE_DND_TYPE, type ElementDragSource, type UMLEditorElementDropTarget, } from '../../../../stores/editor/utils/DnDUtils.js'; import { UnsupportedEditorPanel } from '../UnsupportedElementEditor.js'; import { clsx, ResizablePanelGroup, ResizablePanel, ResizablePanelSplitter, ResizablePanelSplitterLine, PURE_MappingIcon, PURE_RuntimeIcon, ErrorIcon, CogIcon, LongArrowRightIcon, ExclamationTriangleIcon, CustomSelectorInput, ContextMenu, KeyIcon, MenuContent, MenuContentItem, Dialog, InputWithInlineValidation, BlankPanelPlaceholder, PlusIcon, ArrowsJoinIcon, ArrowsSplitIcon, PanelDropZone, Panel, PanelContent, ModalTitle, PanelHeaderActionItem, PanelHeaderActions, PanelHeader, } from '@finos/legend-art'; import { ServiceExecutionQueryEditor } from './ServiceExecutionQueryEditor.js'; import { useEditorStore } from '../../EditorStoreProvider.js'; import { type KeyedExecutionParameter, type Runtime, Mapping, RuntimePointer, PackageableRuntime, PackageableElementExplicitReference, validate_PureExecutionMapping, } from '@finos/legend-graph'; import { guaranteeNonNullable } from '@finos/legend-shared'; import { buildElementOption, type PackageableElementOption, } from '@finos/legend-lego/graph-editor'; import { CUSTOM_LABEL } from '../../../../stores/editor/NewElementState.js'; const PureExecutionContextConfigurationEditor = observer( (props: { pureExecutionState: ServicePureExecutionState; executionContextState: ServiceExecutionContextState; }) => { const { executionContextState, pureExecutionState } = props; const executionContext = executionContextState.executionContext; const editorStore = useEditorStore(); const applicationStore = editorStore.applicationStore; const serviceState = editorStore.tabManagerState.getCurrentEditorState(ServiceEditorState); const isReadOnly = serviceState.isReadOnly; // mapping // TODO: this is not generic error handling, as there could be other problems // with mapping, we need to genericize this const isMappingEmpty = validate_PureExecutionMapping( executionContext.mapping.value, ); const mapping = executionContext.mapping.value; const mappingOptions = editorStore.graphManagerState.usableMappings.map(buildElementOption); const noMappingLabel = (
(none)
); const selectedMappingOption = { value: mapping, label: isMappingEmpty ? noMappingLabel : mapping.path, } as PackageableElementOption; const onMappingSelectionChange = ( val: PackageableElementOption, ): void => { if (val.value !== mapping) { executionContextState.setMapping(val.value); pureExecutionState.autoSelectRuntimeOnMappingChange(val.value); } }; const visitMapping = (): void => editorStore.graphEditorMode.openElement(mapping); // runtime const runtime = executionContext.runtime; const isRuntimePointer = runtime instanceof RuntimePointer; const customRuntimeLabel = (
{CUSTOM_LABEL}
); // only show custom runtime option when a runtime pointer is currently selected let runtimeOptions = !isRuntimePointer ? [] : ([{ label: customRuntimeLabel }] as { label: string | React.ReactNode; value: Runtime | undefined; }[]); // NOTE: for now, only include runtime associated with the mapping // TODO?: Should we bring the runtime compatibility check from query to here? const runtimes = editorStore.graphManagerState.graph.runtimes.filter((rt) => rt.runtimeValue.mappings.map((m) => m.value).includes(mapping), ); runtimeOptions = runtimeOptions.concat( runtimes.map((rt) => ({ label: rt.path, value: new RuntimePointer( PackageableElementExplicitReference.create(rt), ), })), ); const runtimePointerWarning = runtime instanceof RuntimePointer && !runtimes.includes(runtime.packageableRuntime.value) // if the runtime does not belong to the chosen mapping ? `runtime is not associated with specified mapping '${mapping.path}'` : undefined; const selectedRuntimeOption = { value: runtime, label: runtime instanceof RuntimePointer ? (
{runtime.packageableRuntime.value.path}
{runtimePointerWarning && (
)}
) : ( customRuntimeLabel ), }; const onRuntimeSelectionChange = (val: { label: string | React.ReactNode; value: Runtime | undefined; }): void => { if (val.value === undefined) { pureExecutionState.useCustomRuntime(); } else if (val.value !== runtime) { executionContextState.setRuntime(val.value); } }; const visitRuntime = (): void => { if (runtime instanceof RuntimePointer) { editorStore.graphEditorMode.openElement( runtime.packageableRuntime.value, ); } }; const openRuntimeEditor = (): void => pureExecutionState.openRuntimeEditor(); // DnD const handleMappingOrRuntimeDrop = useCallback( (item: UMLEditorElementDropTarget): void => { const element = item.data.packageableElement; if (!isReadOnly) { if (element instanceof Mapping) { executionContextState.setMapping(element); pureExecutionState.autoSelectRuntimeOnMappingChange(element); } else if ( element instanceof PackageableRuntime && element.runtimeValue.mappings.map((m) => m.value).includes(mapping) ) { executionContextState.setRuntime( new RuntimePointer( PackageableElementExplicitReference.create(element), ), ); } } }, [isReadOnly, mapping, executionContextState, pureExecutionState], ); const [{ isMappingOrRuntimeDragOver }, dropConnector] = useDrop< ElementDragSource, void, { isMappingOrRuntimeDragOver: boolean } >( () => ({ accept: [ CORE_DND_TYPE.PROJECT_EXPLORER_MAPPING, CORE_DND_TYPE.PROJECT_EXPLORER_RUNTIME, ], drop: (item) => handleMappingOrRuntimeDrop(item), collect: (monitor) => ({ isMappingOrRuntimeDragOver: monitor.isOver({ shallow: true }), }), }), [handleMappingOrRuntimeDrop], ); // close runtime editor as we leave service editor useEffect( () => (): void => pureExecutionState.closeRuntimeEditor(), [executionContextState, pureExecutionState], ); return (
{!isRuntimePointer && ( )} {isRuntimePointer && ( )} pureExecutionState.closeRuntimeEditor()} />
); }, ); export const ChangeExecutionModal = observer( (props: { executionState: ServicePureExecutionState; isReadOnly: boolean; }) => { const { executionState, isReadOnly } = props; const applicationStore = executionState.editorStore.applicationStore; const closeModal = (): void => executionState.setShowChangeExecModal(false); const isChangingToMulti = executionState instanceof SingleServicePureExecutionState; const renderChangeExecution = (): React.ReactNode => { if (executionState instanceof SingleServicePureExecutionState) { const keyValue = executionState.multiExecutionKey; const validationMessage = keyValue === '' ? `Key value can't be empty` : undefined; const onChange: React.ChangeEventHandler = ( event, ) => { executionState.setMultiExecutionKey(event.target.value); }; return ( ); } else if (executionState instanceof MultiServicePureExecutionState) { const currentOption = executionState.singleExecutionKey ? { value: executionState.singleExecutionKey, label: executionState.singleExecutionKey.key, } : undefined; const multiOptions = executionState.keyedExecutionParameters.map( (keyExecutionParameter) => ({ value: keyExecutionParameter, label: keyExecutionParameter.key, }), ); const _onChange = ( val: { label: string; value: KeyedExecutionParameter } | null, ): void => { if (val === null) { executionState.setSingleExecutionKey(undefined); } else if (val.value !== currentOption?.value) { executionState.setSingleExecutionKey(val.value); } }; return ( ); } return null; }; return (
{ event.preventDefault(); executionState.changeExecution(); closeModal(); }} className="modal modal--dark search-modal" >
{`Change to ${isChangingToMulti ? 'multi' : 'single'} execution`}
{renderChangeExecution()}
); }, ); const PureSingleExecutionEditor = observer( (props: { singleExecutionState: SingleServicePureExecutionState }) => { const { singleExecutionState } = props; // close runtime editor as we leave service editor useEffect( () => (): void => singleExecutionState.closeRuntimeEditor(), [singleExecutionState], ); return (
); }, ); const KeyExecutionContextMenu = observer( forwardRef< HTMLDivElement, { multiExecutionState: MultiServicePureExecutionState; keyExecutionParameter: KeyedExecutionParameter; isReadOnly: boolean; } >(function TestContainerContextMenu(props, ref) { const { multiExecutionState, keyExecutionParameter } = props; const rename = (): void => { multiExecutionState.setRenameKey(keyExecutionParameter); }; const remove = (): void => { multiExecutionState.deleteKeyExecutionParameter(keyExecutionParameter); }; const add = (): void => { multiExecutionState.setNewKeyParameterModal(true); }; return ( Rename Delete Create a new key ); }), ); const KeyExecutionItem = observer( (props: { multiExecutionState: MultiServicePureExecutionState; keyExecutionParameter: KeyedExecutionParameter; isReadOnly: boolean; }) => { const { multiExecutionState, keyExecutionParameter, isReadOnly } = props; const [isSelectedFromContextMenu, setIsSelectedFromContextMenu] = useState(false); const isActive = multiExecutionState.selectedExecutionContextState?.executionContext === keyExecutionParameter; const openKeyedExecution = (): void => multiExecutionState.changeKeyedExecutionParameter(keyExecutionParameter); const onContextMenuOpen = (): void => setIsSelectedFromContextMenu(true); const onContextMenuClose = (): void => setIsSelectedFromContextMenu(false); return ( } menuProps={{ elevation: 7 }} onOpen={onContextMenuOpen} onClose={onContextMenuClose} > ); }, ); export const NewExecutionParameterModal = observer( (props: { executionState: MultiServicePureExecutionState; isReadOnly: boolean; }) => { const [keyValue, setKeyValue] = useState(''); const { executionState, isReadOnly } = props; const validationMessage = keyValue === '' ? `Execution context key can't be empty` : executionState.execution.executionParameters?.find( (e) => e.key === keyValue, ) ? 'Execution context key already exists' : undefined; const closeModal = (): void => executionState.setNewKeyParameterModal(false); const onChange: React.ChangeEventHandler = (event) => { setKeyValue(event.target.value); }; return (
{ event.preventDefault(); executionState.addExecutionParameter(keyValue); setKeyValue(''); closeModal(); }} className="modal modal--dark search-modal" >
); }, ); const RenameModal = observer( (props: { val: string; isReadOnly: boolean; setValue: (val: string) => void; showModal: boolean; closeModal: () => void; }) => { const { val, isReadOnly, showModal, closeModal, setValue } = props; const [inputValue, setInputValue] = useState(val); const changeValue: React.ChangeEventHandler = (event) => { setInputValue(event.target.value); }; return (
{ event.preventDefault(); setValue(inputValue); closeModal(); }} className="modal modal--dark search-modal" >
); }, ); const MultiPureExecutionEditor = observer( (props: { multiExecutionState: MultiServicePureExecutionState }) => { const { multiExecutionState } = props; const multiExecution = multiExecutionState.execution; const key = multiExecution.executionKey; const addExecutionKey = (): void => { multiExecutionState.setNewKeyParameterModal(true); }; const editReviewTitle: React.ChangeEventHandler = ( event, ) => { multiExecutionState.setExecutionKey(event.target.value); }; return (
Execution Contexts
{multiExecutionState.keyedExecutionParameters.map( (executionParameter) => ( ), )} {!multiExecutionState.keyedExecutionParameters.length && ( )}
{multiExecutionState.newKeyParameterModal && ( )} {multiExecutionState.renameKey && ( multiExecutionState.setRenameKey(undefined) } setValue={(val: string): void => multiExecutionState.changeKeyValue( guaranteeNonNullable(multiExecutionState.renameKey), val, ) } /> )}
{multiExecutionState.selectedExecutionContextState ? ( ) : ( )}
); }, ); const ServicePureExecutionEditor = observer( (props: { servicePureExecutionState: ServicePureExecutionState; isReadOnly: boolean; }) => { const { servicePureExecutionState, isReadOnly } = props; const serviceState = servicePureExecutionState.serviceEditorState; const isMultiExecution = servicePureExecutionState instanceof MultiServicePureExecutionState; const showChangeExecutionModal = (): void => { if (servicePureExecutionState instanceof MultiServicePureExecutionState) { servicePureExecutionState.setSingleExecutionKey( servicePureExecutionState.keyedExecutionParameters[0], ); } servicePureExecutionState.setShowChangeExecModal(true); }; const renderExecutionEditor = (): React.ReactNode => { if ( servicePureExecutionState instanceof SingleServicePureExecutionState ) { return ( ); } else if ( servicePureExecutionState instanceof MultiServicePureExecutionState ) { return ( ); } return ( ); }; if (servicePureExecutionState instanceof InlineServicePureExecutionState) { return (
); } else { return (
context
{(servicePureExecutionState instanceof SingleServicePureExecutionState || servicePureExecutionState instanceof MultiServicePureExecutionState) && ( )}
{renderExecutionEditor()} {servicePureExecutionState.showChangeExecModal && ( )}
); } }, ); export const ServiceExecutionEditor = observer(() => { const editorStore = useEditorStore(); const serviceState = editorStore.tabManagerState.getCurrentEditorState(ServiceEditorState); const executionState = serviceState.executionState; const isReadOnly = serviceState.isReadOnly; if (executionState instanceof ServicePureExecutionState) { return ( ); } return ( ); });