You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2023/01/25 15:30:19 UTC

[camel-karavan] 02/03: Apply last changes to app

This is an automated email from the ASF dual-hosted git repository.

marat pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit 910e43799a1c9ac67e843f6cf116e495a75cb732
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Mon Jan 23 11:47:04 2023 -0500

    Apply last changes to app
---
 .../main/webui/src/designer/KaravanDesigner.tsx    |  11 +-
 .../main/webui/src/designer/route/DslElement.tsx   |  36 +-
 .../webui/src/designer/route/RouteDesigner.tsx     | 354 ++-----------------
 .../src/designer/route/RouteDesignerLogic.tsx      | 383 +++++++++++++++++++++
 .../src/main/webui/src/designer/utils/EventBus.ts  |  14 +
 5 files changed, 446 insertions(+), 352 deletions(-)

diff --git a/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx b/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
index e7f975b8..a2556b2a 100644
--- a/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
+++ b/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
@@ -44,7 +44,6 @@ interface State {
     integration: Integration
     key: string
     propertyOnly: boolean
-    routeDesignerRef?: any
 }
 
 export class KaravanInstance {
@@ -78,7 +77,6 @@ export class KaravanDesigner extends React.Component<Props, State> {
         integration: this.getIntegration(this.props.yaml, this.props.filename),
         key: "",
         propertyOnly: false,
-        routeDesignerRef: React.createRef(),
     }
 
     componentDidMount() {
@@ -117,12 +115,6 @@ export class KaravanDesigner extends React.Component<Props, State> {
         )
     }
 
-    downloadImage(){
-        if(this.state.routeDesignerRef){
-            this.state.routeDesignerRef.current.integrationImageDownload();
-         }
-    }
-
     render() {
         const tab = this.state.tab;
         return (
@@ -134,8 +126,7 @@ export class KaravanDesigner extends React.Component<Props, State> {
                 </Tabs>
                     {tab === 'routes' && <RouteDesigner integration={this.state.integration}
                                                         onSave={(integration, propertyOnly) => this.save(integration, propertyOnly)}
-                                                        dark={this.props.dark}
-                                                        ref={this.state.routeDesignerRef}/>}
+                                                        dark={this.props.dark}/>}
                     {tab === 'rest' && <RestDesigner integration={this.state.integration}
                                                      onSave={(integration, propertyOnly) => this.save(integration, propertyOnly)}
                                                      dark={this.props.dark}/>}
diff --git a/karavan-app/src/main/webui/src/designer/route/DslElement.tsx b/karavan-app/src/main/webui/src/designer/route/DslElement.tsx
index 07222d84..f9cd9498 100644
--- a/karavan-app/src/main/webui/src/designer/route/DslElement.tsx
+++ b/karavan-app/src/main/webui/src/designer/route/DslElement.tsx
@@ -41,7 +41,7 @@ interface Props {
     selectElement: any
     openSelector: (parentId: string | undefined, parentDsl: string | undefined, showSteps: boolean, position?: number | undefined) => void
     moveElement: (source: string, target: string, asChild: boolean) => void
-    selectedUuid: string
+    selectedUuid: string []
     inSteps: boolean
     position: number
 }
@@ -51,7 +51,6 @@ interface State {
     showMoveConfirmation: boolean
     moveElements: [string | undefined, string | undefined]
     tabIndex: string | number
-    selectedUuid: string
     isDragging: boolean
     isDraggedOver: boolean
 }
@@ -63,29 +62,16 @@ export class DslElement extends React.Component<Props, State> {
         showMoveConfirmation: false,
         moveElements: [undefined, undefined],
         tabIndex: 0,
-        selectedUuid: this.props.selectedUuid,
         isDragging: false,
         isDraggedOver: false,
     };
 
-    handleKeyDown = (event: React.KeyboardEvent) =>{
-        // event.preventDefault();
-        // console.log(event);
-        // let charCode = String.fromCharCode(event.which).toLowerCase();
-        // if((event.ctrlKey || event.metaKey) && charCode === 's') {
-        //     alert("CTRL+S Pressed");
-        // }else if((event.ctrlKey || event.metaKey) && charCode === 'c') {
-        //     alert("CTRL+C Pressed");
-        // }else if((event.ctrlKey || event.metaKey) && charCode === 'v') {
-        //     alert("CTRL+V Pressed");
-        // }
-    }
-
-    componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) => {
-        if (prevState.selectedUuid !== this.props.selectedUuid) {
-            this.setState({selectedUuid: this.props.selectedUuid});
-        }
-    }
+    //
+    // componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) => {
+    //     if (prevState.selectedUuid !== this.props.selectedUuid) {
+    //         this.setState({selectedUuid: this.props.selectedUuid});
+    //     }
+    // }
 
     openSelector = (evt: React.MouseEvent, showSteps: boolean = true, isInsert: boolean = false) => {
         evt.stopPropagation();
@@ -139,7 +125,7 @@ export class DslElement extends React.Component<Props, State> {
     }
 
     isSelected = (): boolean => {
-        return this.state.selectedUuid === this.props.step.uuid
+        return this.props.selectedUuid.includes(this.props.step.uuid);
     }
 
     hasBorder = (): boolean => {
@@ -365,7 +351,7 @@ export class DslElement extends React.Component<Props, State> {
                                 deleteElement={this.props.deleteElement}
                                 selectElement={this.props.selectElement}
                                 moveElement={this.props.moveElement}
-                                selectedUuid={this.state.selectedUuid}
+                                selectedUuid={this.props.selectedUuid}
                                 inSteps={child.name === 'steps'}
                                 position={index}
                                 step={element}
@@ -386,7 +372,7 @@ export class DslElement extends React.Component<Props, State> {
 
     getAddStepButton() {
         const {integration, step, selectedUuid} = this.props;
-        const hideAddButton = step.dslName === 'StepDefinition' && !CamelDisplayUtil.isStepDefinitionExpanded(integration, step.uuid, selectedUuid);
+        const hideAddButton = step.dslName === 'StepDefinition' && !CamelDisplayUtil.isStepDefinitionExpanded(integration, step.uuid, selectedUuid.at(0));
         if (hideAddButton) return (<></>)
         else return (
             <Tooltip position={"bottom"}
@@ -497,8 +483,6 @@ export class DslElement extends React.Component<Props, State> {
                  }}
                  onDrop={event => this.dragElement(event, element)}
                  draggable={!this.isNotDraggable()}
-                 // tabIndex={0}
-                 onKeyDown={this.handleKeyDown}
             >
                 {this.getElementHeader()}
                 {this.getChildElements()}
diff --git a/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx b/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
index 87584dc1..dc0ab805 100644
--- a/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
+++ b/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
@@ -25,21 +25,14 @@ import {
 } from '@patternfly/react-core';
 import '../karavan.css';
 import {DslSelector} from "./DslSelector";
-import {DslMetaModel} from "../utils/DslMetaModel";
 import {DslProperties} from "./DslProperties";
-import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
-import {FromDefinition, RouteConfigurationDefinition, RouteDefinition} from "karavan-core/lib/model/CamelDefinition";
 import {CamelElement, Integration} from "karavan-core/lib/model/IntegrationDefinition";
-import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt";
-import {CamelDefinitionApi} from "karavan-core/lib/api/CamelDefinitionApi";
 import {DslConnections} from "./DslConnections";
 import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon";
 import {DslElement} from "./DslElement";
-import {EventBus} from "../utils/EventBus";
-import {CamelUi, RouteToCreate} from "../utils/CamelUi";
-import {findDOMNode} from "react-dom";
+import {CamelUi} from "../utils/CamelUi";
 import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
-import {toPng} from 'html-to-image';
+import {RouteDesignerLogic} from "./RouteDesignerLogic";
 
 interface Props {
     onSave?: (integration: Integration, propertyOnly: boolean) => void
@@ -47,7 +40,8 @@ interface Props {
     dark: boolean
 }
 
-interface State {
+export interface RouteDesignerState {
+    logic: RouteDesignerLogic
     integration: Integration
     selectedStep?: CamelElement
     showSelector: boolean
@@ -57,13 +51,13 @@ interface State {
     parentDsl?: string
     selectedPosition?: number
     showSteps: boolean
-    selectedUuid: string []
+    selectedUuids: string []
     key: string
     width: number
     height: number
     top: number
     left: number
-    clipboardStep?: CamelElement
+    clipboardSteps: CamelElement[]
     shiftKeyPressed?: boolean
     ref?: any
     printerRef?: any
@@ -71,16 +65,18 @@ interface State {
     selectorTabIndex?: string | number
 }
 
-export class RouteDesigner extends React.Component<Props, State> {
+export class RouteDesigner extends React.Component<Props, RouteDesignerState> {
 
-    public state: State = {
+    public state: RouteDesignerState = {
+        logic: new RouteDesignerLogic(this),
         integration: CamelDisplayUtil.setIntegrationVisibility(this.props.integration, undefined),
         showSelector: false,
         showDeleteConfirmation: false,
         deleteMessage: '',
         parentId: '',
         showSteps: true,
-        selectedUuid: [],
+        selectedUuids: [],
+        clipboardSteps: [],
         key: "",
         width: 1000,
         height: 1000,
@@ -92,286 +88,41 @@ export class RouteDesigner extends React.Component<Props, State> {
     };
 
     componentDidMount() {
-        window.addEventListener('resize', this.handleResize);
-        window.addEventListener('keydown', this.handleKeyDown);
-        window.addEventListener('keyup', this.handleKeyUp);
-        const element = findDOMNode(this.state.ref.current)?.parentElement?.parentElement;
-        const checkResize = (mutations: any) => {
-            const el = mutations[0].target;
-            const w = el.clientWidth;
-            const isChange = mutations.map((m: any) => `${m.oldValue}`).some((prev: any) => prev.indexOf(`width: ${w}px`) === -1);
-            if (isChange) this.setState({key: Math.random().toString()});
-        }
-        if (element) {
-            const observer = new MutationObserver(checkResize);
-            observer.observe(element, {attributes: true, attributeOldValue: true, attributeFilter: ['style']});
-        }
+        this.state.logic.componentDidMount();
     }
 
     componentWillUnmount() {
-        window.removeEventListener('resize', this.handleResize);
-        window.removeEventListener('keydown', this.handleKeyDown);
-        window.removeEventListener('keyup', this.handleKeyUp);
+        this.state.logic.componentWillUnmount();
     }
 
     handleResize = (event: any) => {
-        this.setState({key: Math.random().toString()});
+        return this.state.logic.handleResize(event);
     }
 
     handleKeyDown = (event: KeyboardEvent) => {
-        const {integration, selectedUuid, clipboardStep} = this.state;
-        const selectedUUID = selectedUuid.at(0);
-        if ((event.shiftKey)) {
-            this.setState({shiftKeyPressed: true});
-        }
-        if (window.document.hasFocus() && window.document.activeElement && selectedUUID) {
-            if (['BODY', 'MAIN'].includes(window.document.activeElement.tagName)) {
-                let charCode = String.fromCharCode(event.which).toLowerCase();
-                if ((event.ctrlKey || event.metaKey) && charCode === 'c') {
-                    const selectedElement = CamelDefinitionApiExt.findElementInIntegration(integration, selectedUUID);
-                    this.saveToClipboard(selectedElement);
-                } else if ((event.ctrlKey || event.metaKey) && charCode === 'v') {
-                    if (clipboardStep?.dslName === 'FromDefinition') {
-                        const clone = CamelUtil.cloneStep(clipboardStep, true);
-                        const route = CamelDefinitionApi.createRouteDefinition({from: clone});
-                        this.addStep(route, '', 0)
-                    } else {
-                        const meta = CamelDefinitionApiExt.findElementMetaInIntegration(integration, selectedUUID);
-                        if (clipboardStep && meta.parentUuid) {
-                            const clone = CamelUtil.cloneStep(clipboardStep, true);
-                            this.addStep(clone, meta.parentUuid, meta.position);
-                        }
-                    }
-                }
-            }
-        } else {
-            if (event.repeat) {
-                window.dispatchEvent(event);
-            }
-        }
+        return this.state.logic.handleKeyDown(event);
     }
 
     handleKeyUp = (event: KeyboardEvent) => {
-        this.setState({shiftKeyPressed: false});
-        if (event.repeat) {
-            window.dispatchEvent(event);
-        }
+        return this.state.logic.handleKeyUp(event);
     }
 
-    componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) => {
-        if (prevState.key !== this.state.key) {
-            this.props.onSave?.call(this, this.state.integration, this.state.propertyOnly);
-        }
-    }
-
-    saveToClipboard = (step?: CamelElement): void => {
-        this.setState({clipboardStep: step, key: Math.random().toString()});
-    }
-
-    onPropertyUpdate = (element: CamelElement, newRoute?: RouteToCreate) => {
-        if (newRoute) {
-            let i = CamelDefinitionApiExt.updateIntegrationRouteElement(this.state.integration, element);
-            const f = CamelDefinitionApi.createFromDefinition({uri: newRoute.componentName + ":" + newRoute.name})
-            const r = CamelDefinitionApi.createRouteDefinition({from: f, id: newRoute.name})
-            i = CamelDefinitionApiExt.addStepToIntegration(i, r, '');
-            const clone = CamelUtil.cloneIntegration(i);
-            this.setState(prevState => ({
-                integration: clone,
-                key: Math.random().toString(),
-                showSelector: false,
-                selectedStep: element,
-                propertyOnly: false,
-                selectedUuid: [element.uuid],
-            }));
-        } else {
-            const clone = CamelUtil.cloneIntegration(this.state.integration);
-            const i = CamelDefinitionApiExt.updateIntegrationRouteElement(clone, element);
-            this.setState({integration: i, propertyOnly: true, key: Math.random().toString()});
-        }
-    }
-
-    showDeleteConfirmation = (id: string) => {
-        let message: string;
-        let ce: CamelElement;
-        let isRouteConfiguration: boolean = false;
-        ce = CamelDefinitionApiExt.findElementInIntegration(this.state.integration, id)!;
-        if (ce.dslName === 'FromDefinition') { // Get the RouteDefinition for this.  Use its uuid.
-            let flows = this.state.integration.spec.flows!;
-            for (let i = 0; i < flows.length; i++) {
-                if (flows[i].dslName === 'RouteDefinition') {
-                    let routeDefinition: RouteDefinition = flows[i];
-                    if (routeDefinition.from.uuid === id) {
-                        id = routeDefinition.uuid;
-                        break;
-                    }
-                }
-            }
-            message = 'Deleting the first element will delete the entire route!';
-        } else if (ce.dslName === 'RouteDefinition') {
-            message = 'Delete route?';
-        } else if (ce.dslName === 'RouteConfigurationDefinition') {
-            message = 'Delete route configuration?';
-            isRouteConfiguration = true;
-        } else {
-            message = 'Delete element from route?';
-        }
-        this.setState(prevState => ({
-            showSelector: false,
-            showDeleteConfirmation: true,
-            deleteMessage: message,
-            selectedUuid: [id],
-        }));
-    }
-
-    deleteElement = () => {
-        const id = this.state.selectedUuid.at(0);
-        if (id) {
-            const i = CamelDefinitionApiExt.deleteStepFromIntegration(this.state.integration, id);
-            this.setState(prevState => ({
-                integration: i,
-                showSelector: false,
-                showDeleteConfirmation: false,
-                deleteMessage: '',
-                key: Math.random().toString(),
-                selectedStep: undefined,
-                propertyOnly: false,
-                selectedUuid: [id],
-            }));
-            const el = new CamelElement("");
-            el.uuid = id;
-            EventBus.sendPosition("delete", el, undefined, new DOMRect(), new DOMRect(), 0);
-        }
-    }
-
-    selectElement = (element: CamelElement) => {
-        console.log(this.state.shiftKeyPressed, element);
-        const i = CamelDisplayUtil.setIntegrationVisibility(this.state.integration, element.uuid);
-        this.setState(prevState => ({
-            integration: i,
-            selectedStep: element,
-            showSelector: false,
-            selectedUuid: [element.uuid],
-        }));
-    }
-
-    unselectElement = (evt: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
-        if ((evt.target as any).dataset.click === 'FLOWS') {
-            evt.stopPropagation()
-            const i = CamelDisplayUtil.setIntegrationVisibility(this.state.integration, undefined);
-            this.setState(prevState => ({
-                integration: i,
-                selectedStep: undefined,
-                showSelector: false,
-                selectedPosition: undefined,
-                selectedUuid: [],
-            }));
-        }
-    }
-
-    openSelector = (parentId: string | undefined, parentDsl: string | undefined, showSteps: boolean = true, position?: number | undefined, selectorTabIndex?: string | number) => {
-        this.setState({
-            showSelector: true,
-            parentId: parentId || '',
-            parentDsl: parentDsl,
-            showSteps: showSteps,
-            selectedPosition: position,
-            selectorTabIndex: selectorTabIndex
-        })
-    }
-
-    closeDslSelector = () => {
-        this.setState({showSelector: false})
-    }
-
-    onDslSelect = (dsl: DslMetaModel, parentId: string, position?: number | undefined) => {
-        switch (dsl.dsl) {
-            case 'FromDefinition' :
-                const route = CamelDefinitionApi.createRouteDefinition({from: new FromDefinition({uri: dsl.uri})});
-                this.addStep(route, parentId, position)
-                break;
-            case 'ToDefinition' :
-                const to = CamelDefinitionApi.createStep(dsl.dsl, {uri: dsl.uri});
-                this.addStep(to, parentId, position)
-                break;
-            case 'ToDynamicDefinition' :
-                const toD = CamelDefinitionApi.createStep(dsl.dsl, {uri: dsl.uri});
-                this.addStep(toD, parentId, position)
-                break;
-            case 'KameletDefinition' :
-                const kamelet = CamelDefinitionApi.createStep(dsl.dsl, {name: dsl.name});
-                this.addStep(kamelet, parentId, position)
-                break;
-            default:
-                const step = CamelDefinitionApi.createStep(dsl.dsl, undefined);
-                this.addStep(step, parentId, position)
-                break;
-        }
-    }
-
-    createRouteConfiguration = () => {
-        const clone = CamelUtil.cloneIntegration(this.state.integration);
-        const routeConfiguration = new RouteConfigurationDefinition();
-        const i = CamelDefinitionApiExt.addRouteConfigurationToIntegration(clone, routeConfiguration);
-        this.setState(prevState => ({
-            integration: i,
-            propertyOnly: false,
-            key: Math.random().toString(),
-            selectedStep: routeConfiguration,
-            selectedUuid: [routeConfiguration.uuid],
-        }));
-    }
-
-    addStep = (step: CamelElement, parentId: string, position?: number | undefined) => {
-        const i = CamelDefinitionApiExt.addStepToIntegration(this.state.integration, step, parentId, position);
-        const clone = CamelUtil.cloneIntegration(i);
-        EventBus.sendPosition("clean", step, undefined, new DOMRect(), new DOMRect(), 0);
-        this.setState(prevState => ({
-            integration: clone,
-            key: Math.random().toString(),
-            showSelector: false,
-            selectedStep: step,
-            propertyOnly: false,
-            selectedUuid: [step.uuid],
-        }));
-    }
-
-    onIntegrationUpdate = (i: Integration) => {
-        this.setState({integration: i, propertyOnly: false, showSelector: false, key: Math.random().toString()});
-    }
-
-    moveElement = (source: string, target: string, asChild: boolean) => {
-        const i = CamelDefinitionApiExt.moveRouteElement(this.state.integration, source, target, asChild);
-        const clone = CamelUtil.cloneIntegration(i);
-        const selectedStep = CamelDefinitionApiExt.findElementInIntegration(clone, source);
-        this.setState(prevState => ({
-            integration: clone,
-            key: Math.random().toString(),
-            showSelector: false,
-            selectedStep: selectedStep,
-            propertyOnly: false,
-            selectedUuid: [source],
-        }));
-    }
-
-    onResizePage(el: HTMLDivElement | null) {
-        const rect = el?.getBoundingClientRect();
-        if (el && rect && (el.scrollWidth !== this.state.width || el.scrollHeight !== this.state.height || rect.top !== this.state.top || rect.left !== this.state.left)) {
-            this.setState({width: el.scrollWidth, height: el.scrollHeight, top: rect.top, left: rect.left})
-        }
+    componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<RouteDesignerState>, snapshot?: any) => {
+        return this.state.logic.componentDidUpdate(prevState, snapshot);
     }
 
     getSelectorModal() {
         return (
             <DslSelector
                 isOpen={this.state.showSelector}
-                onClose={() => this.closeDslSelector()}
+                onClose={() => this.state.logic.closeDslSelector()}
                 dark={this.props.dark}
                 parentId={this.state.parentId}
                 parentDsl={this.state.parentDsl}
                 showSteps={this.state.showSteps}
                 position={this.state.selectedPosition}
                 tabIndex={this.state.selectorTabIndex}
-                onDslSelect={this.onDslSelect}/>)
+                onDslSelect={this.state.logic.onDslSelect}/>)
     }
 
     getDeleteConfirmation() {
@@ -382,7 +133,7 @@ export class RouteDesigner extends React.Component<Props, State> {
             isOpen={this.state.showDeleteConfirmation}
             onClose={() => this.setState({showDeleteConfirmation: false})}
             actions={[
-                <Button key="confirm" variant="primary" onClick={e => this.deleteElement()}>Delete</Button>,
+                <Button key="confirm" variant="primary" onClick={e => this.state.logic.deleteElement()}>Delete</Button>,
                 <Button key="cancel" variant="link"
                         onClick={e => this.setState({showDeleteConfirmation: false})}>Cancel</Button>
             ]}
@@ -401,60 +152,31 @@ export class RouteDesigner extends React.Component<Props, State> {
                 <DslProperties ref={this.state.ref}
                                integration={this.state.integration}
                                step={this.state.selectedStep}
-                               onIntegrationUpdate={this.onIntegrationUpdate}
-                               onPropertyUpdate={this.onPropertyUpdate}
+                               onIntegrationUpdate={this.state.logic.onIntegrationUpdate}
+                               onPropertyUpdate={this.state.logic.onPropertyUpdate}
                                isRouteDesigner={true}
                                dark={this.props.dark}/>
             </DrawerPanelContent>
         )
     }
 
-    downloadIntegrationImage(dataUrl: string) {
-        const a = document.createElement('a');
-        a.setAttribute('download', 'karavan-routes.png');
-        a.setAttribute('href', dataUrl);
-        a.click();
-    }
-
-    integrationImageDownloadFilter = (node: HTMLElement) => {
-        const exclusionClasses = ['add-flow'];
-        return !exclusionClasses.some(classname => {
-            return node.classList === undefined ? false : node.classList.contains(classname);
-        });
-    }
-
-    integrationImageDownload() {
-        if (this.state.printerRef.current === null) {
-            return
-        }
-        toPng(this.state.printerRef.current, {
-            style: {overflow: 'hidden'}, cacheBust: true, filter: this.integrationImageDownloadFilter,
-            height: this.state.height, width: this.state.width, backgroundColor: this.props.dark ? "black" : "white"
-        }).then(v => {
-            toPng(this.state.printerRef.current, {
-                style: {overflow: 'hidden'}, cacheBust: true, filter: this.integrationImageDownloadFilter,
-                height: this.state.height, width: this.state.width, backgroundColor: this.props.dark ? "black" : "white"
-            }).then(this.downloadIntegrationImage);
-        })
-    }
-
     getGraph() {
-        const {selectedUuid, integration, key, width, height, top, left} = this.state;
+        const {selectedUuids, integration, key, width, height, top, left} = this.state;
         const routes = CamelUi.getRoutes(integration);
         const routeConfigurations = CamelUi.getRouteConfigurations(integration);
         return (
             <div ref={this.state.printerRef} className="graph">
                 <DslConnections height={height} width={width} top={top} left={left} integration={integration}/>
-                <div className="flows" data-click="FLOWS" onClick={event => this.unselectElement(event)}
-                     ref={el => this.onResizePage(el)}>
+                <div className="flows" data-click="FLOWS" onClick={event => this.state.logic.unselectElement(event)}
+                     ref={el => this.state.logic.onResizePage(el)}>
                     {routeConfigurations?.map((routeConfiguration, index: number) => (
                         <DslElement key={routeConfiguration.uuid + key}
                                     integration={integration}
-                                    openSelector={this.openSelector}
-                                    deleteElement={this.showDeleteConfirmation}
-                                    selectElement={this.selectElement}
-                                    moveElement={this.moveElement}
-                                    selectedUuid={selectedUuid.at(0) || ''}
+                                    openSelector={this.state.logic.openSelector}
+                                    deleteElement={this.state.logic.showDeleteConfirmation}
+                                    selectElement={this.state.logic.selectElement}
+                                    moveElement={this.state.logic.moveElement}
+                                    selectedUuid={selectedUuids}
                                     inSteps={false}
                                     position={index}
                                     step={routeConfiguration}
@@ -463,11 +185,11 @@ export class RouteDesigner extends React.Component<Props, State> {
                     {routes?.map((route: any, index: number) => (
                         <DslElement key={route.uuid + key}
                                     integration={integration}
-                                    openSelector={this.openSelector}
-                                    deleteElement={this.showDeleteConfirmation}
-                                    selectElement={this.selectElement}
-                                    moveElement={this.moveElement}
-                                    selectedUuid={selectedUuid.at(0) || ''}
+                                    openSelector={this.state.logic.openSelector}
+                                    deleteElement={this.state.logic.showDeleteConfirmation}
+                                    selectElement={this.state.logic.selectElement}
+                                    moveElement={this.state.logic.moveElement}
+                                    selectedUuid={selectedUuids}
                                     inSteps={false}
                                     position={index}
                                     step={route}
@@ -477,12 +199,12 @@ export class RouteDesigner extends React.Component<Props, State> {
                         <Button
                             variant={routes.length === 0 ? "primary" : "secondary"}
                             icon={<PlusIcon/>}
-                            onClick={e => this.openSelector(undefined, undefined)}>Create route
+                            onClick={e => this.state.logic.openSelector(undefined, undefined)}>Create route
                         </Button>
                         <Button
                             variant="secondary"
                             icon={<PlusIcon/>}
-                            onClick={e => this.createRouteConfiguration()}>Create configuration
+                            onClick={e => this.state.logic.createRouteConfiguration()}>Create configuration
                         </Button>
                     </div>
                 </div>
diff --git a/karavan-app/src/main/webui/src/designer/route/RouteDesignerLogic.tsx b/karavan-app/src/main/webui/src/designer/route/RouteDesignerLogic.tsx
new file mode 100644
index 00000000..18b2ce3c
--- /dev/null
+++ b/karavan-app/src/main/webui/src/designer/route/RouteDesignerLogic.tsx
@@ -0,0 +1,383 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 React from 'react';
+import '../karavan.css';
+import {DslMetaModel} from "../utils/DslMetaModel";
+import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
+import {FromDefinition, RouteConfigurationDefinition, RouteDefinition} from "karavan-core/lib/model/CamelDefinition";
+import {CamelElement, Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt";
+import {CamelDefinitionApi} from "karavan-core/lib/api/CamelDefinitionApi";
+import {Command, EventBus} from "../utils/EventBus";
+import {RouteToCreate} from "../utils/CamelUi";
+import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
+import {toPng} from 'html-to-image';
+import {RouteDesigner, RouteDesignerState} from "./RouteDesigner";
+import {findDOMNode} from "react-dom";
+import {Subscription} from "rxjs";
+
+export class RouteDesignerLogic {
+
+    routeDesigner: RouteDesigner
+    commandSub?: Subscription
+
+    constructor(routeDesigner: RouteDesigner) {
+        this.routeDesigner = routeDesigner;
+    }
+
+    componentDidMount() {
+        window.addEventListener('resize', this.routeDesigner.handleResize);
+        window.addEventListener('keydown', this.routeDesigner.handleKeyDown);
+        window.addEventListener('keyup', this.routeDesigner.handleKeyUp);
+        const element = findDOMNode(this.routeDesigner.state.ref.current)?.parentElement?.parentElement;
+        const checkResize = (mutations: any) => {
+            const el = mutations[0].target;
+            const w = el.clientWidth;
+            const isChange = mutations.map((m: any) => `${m.oldValue}`).some((prev: any) => prev.indexOf(`width: ${w}px`) === -1);
+            if (isChange) this.routeDesigner.setState({key: Math.random().toString()});
+        }
+        if (element) {
+            const observer = new MutationObserver(checkResize);
+            observer.observe(element, {attributes: true, attributeOldValue: true, attributeFilter: ['style']});
+        }
+        this.commandSub = EventBus.onCommand()?.subscribe((command: Command) => this.onCommand(command));
+    }
+
+    componentWillUnmount() {
+        window.removeEventListener('resize', this.routeDesigner.handleResize);
+        window.removeEventListener('keydown', this.routeDesigner.handleKeyDown);
+        window.removeEventListener('keyup', this.routeDesigner.handleKeyUp);
+        this.commandSub?.unsubscribe();
+    }
+
+    handleResize = (event: any) => {
+        this.routeDesigner.setState({key: Math.random().toString()});
+    }
+
+    handleKeyDown = (event: KeyboardEvent) => {
+        if ((event.shiftKey)) {
+            this.routeDesigner.setState({shiftKeyPressed: true});
+        }
+        if (window.document.hasFocus() && window.document.activeElement) {
+            if (['BODY', 'MAIN'].includes(window.document.activeElement.tagName)) {
+                let charCode = String.fromCharCode(event.which).toLowerCase();
+                if ((event.ctrlKey || event.metaKey) && charCode === 'c') {
+                    this.copyToClipboard();
+                } else if ((event.ctrlKey || event.metaKey) && charCode === 'v') {
+                    this.pasteFromClipboard();
+                }
+            }
+        } else {
+            if (event.repeat) {
+                window.dispatchEvent(event);
+            }
+        }
+    }
+
+    handleKeyUp = (event: KeyboardEvent) => {
+        this.routeDesigner.setState({shiftKeyPressed: false});
+        if (event.repeat) {
+            window.dispatchEvent(event);
+        }
+    }
+
+    componentDidUpdate = (prevState: Readonly<RouteDesignerState>, snapshot?: any) => {
+        if (prevState.key !== this.routeDesigner.state.key) {
+            this.routeDesigner.props.onSave?.call(this, this.routeDesigner.state.integration, this.routeDesigner.state.propertyOnly);
+        }
+    }
+
+    copyToClipboard = (): void => {
+        const {integration, selectedUuids} = this.routeDesigner.state;
+        const steps: CamelElement[] = []
+        selectedUuids.forEach(selectedUuid => {
+            const selectedElement = CamelDefinitionApiExt.findElementInIntegration(integration, selectedUuid);
+            if (selectedElement) {
+                steps.push(selectedElement);
+            }
+        })
+        if (steps.length >0) {
+            this.routeDesigner.setState(prevState => ({
+                key: Math.random().toString(),
+                clipboardSteps: [...steps]
+            }));
+        }
+    }
+    pasteFromClipboard = (): void => {
+        const {integration, selectedUuids, clipboardSteps} = this.routeDesigner.state;
+        if (clipboardSteps.length === 1 && clipboardSteps[0]?.dslName === 'FromDefinition') {
+            const clone = CamelUtil.cloneStep(clipboardSteps[0], true);
+            const route = CamelDefinitionApi.createRouteDefinition({from: clone});
+            this.addStep(route, '', 0)
+        } else if (clipboardSteps.length === 1 && clipboardSteps[0]?.dslName === 'RouteDefinition') {
+            const clone = CamelUtil.cloneStep(clipboardSteps[0], true);
+            this.addStep(clone, '', 0)
+        } else if (selectedUuids.length === 1) {
+            const targetMeta = CamelDefinitionApiExt.findElementMetaInIntegration(integration, selectedUuids[0]);
+            clipboardSteps.reverse().forEach(clipboardStep => {
+                if (clipboardStep && targetMeta.parentUuid) {
+                    const clone = CamelUtil.cloneStep(clipboardStep, true);
+                    this.addStep(clone, targetMeta.parentUuid, targetMeta.position);
+                }
+            })
+        }
+    }
+
+    onCommand = (command: Command) => {
+        switch (command.command){
+            case "downloadImage": this.integrationImageDownload()
+        }
+    }
+
+    onPropertyUpdate = (element: CamelElement, newRoute?: RouteToCreate) => {
+        if (newRoute) {
+            let i = CamelDefinitionApiExt.updateIntegrationRouteElement(this.routeDesigner.state.integration, element);
+            const f = CamelDefinitionApi.createFromDefinition({uri: newRoute.componentName + ":" + newRoute.name})
+            const r = CamelDefinitionApi.createRouteDefinition({from: f, id: newRoute.name})
+            i = CamelDefinitionApiExt.addStepToIntegration(i, r, '');
+            const clone = CamelUtil.cloneIntegration(i);
+            this.routeDesigner.setState(prevState => ({
+                integration: clone,
+                key: Math.random().toString(),
+                showSelector: false,
+                selectedStep: element,
+                propertyOnly: false,
+                selectedUuids: [element.uuid]
+            }));
+        } else {
+            const clone = CamelUtil.cloneIntegration(this.routeDesigner.state.integration);
+            const i = CamelDefinitionApiExt.updateIntegrationRouteElement(clone, element);
+            this.routeDesigner.setState({integration: i, propertyOnly: true, key: Math.random().toString()});
+        }
+    }
+
+    showDeleteConfirmation = (id: string) => {
+        let message: string;
+        let ce: CamelElement;
+        let isRouteConfiguration: boolean = false;
+        ce = CamelDefinitionApiExt.findElementInIntegration(this.routeDesigner.state.integration, id)!;
+        if (ce.dslName === 'FromDefinition') { // Get the RouteDefinition for this.routeDesigner.  Use its uuid.
+            let flows = this.routeDesigner.state.integration.spec.flows!;
+            for (let i = 0; i < flows.length; i++) {
+                if (flows[i].dslName === 'RouteDefinition') {
+                    let routeDefinition: RouteDefinition = flows[i];
+                    if (routeDefinition.from.uuid === id) {
+                        id = routeDefinition.uuid;
+                        break;
+                    }
+                }
+            }
+            message = 'Deleting the first element will delete the entire route!';
+        } else if (ce.dslName === 'RouteDefinition') {
+            message = 'Delete route?';
+        } else if (ce.dslName === 'RouteConfigurationDefinition') {
+            message = 'Delete route configuration?';
+            isRouteConfiguration = true;
+        } else {
+            message = 'Delete element from route?';
+        }
+        this.routeDesigner.setState(prevState => ({
+            showSelector: false,
+            showDeleteConfirmation: true,
+            deleteMessage: message,
+            selectedUuids: [id],
+        }));
+    }
+
+    deleteElement = () => {
+        const id = this.routeDesigner.state.selectedUuids.at(0);
+        if (id) {
+            const i = CamelDefinitionApiExt.deleteStepFromIntegration(this.routeDesigner.state.integration, id);
+            this.routeDesigner.setState(prevState => ({
+                integration: i,
+                showSelector: false,
+                showDeleteConfirmation: false,
+                deleteMessage: '',
+                key: Math.random().toString(),
+                selectedStep: undefined,
+                propertyOnly: false,
+                selectedUuids: [id],
+            }));
+            const el = new CamelElement("");
+            el.uuid = id;
+            EventBus.sendPosition("delete", el, undefined, new DOMRect(), new DOMRect(), 0);
+        }
+    }
+
+    selectElement = (element: CamelElement) => {
+        const {shiftKeyPressed, selectedUuids, integration} = this.routeDesigner.state;
+        let canNotAdd: boolean = false;
+        if (shiftKeyPressed) {
+            const hasFrom = selectedUuids.map(e => CamelDefinitionApiExt.findElementInIntegration(integration, e)?.dslName === 'FromDefinition').filter(r => r).length > 0;
+            canNotAdd = hasFrom || (selectedUuids.length > 0 && element.dslName === 'FromDefinition');
+        }
+        const add = shiftKeyPressed && !selectedUuids.includes(element.uuid);
+        const remove = shiftKeyPressed && selectedUuids.includes(element.uuid);
+        const i = CamelDisplayUtil.setIntegrationVisibility(this.routeDesigner.state.integration, element.uuid);
+        this.routeDesigner.setState((prevState: RouteDesignerState) => {
+            if (remove) {
+                const index = prevState.selectedUuids.indexOf(element.uuid);
+                prevState.selectedUuids.splice(index, 1);
+            } else if (add && !canNotAdd) {
+                prevState.selectedUuids.push(element.uuid);
+            }
+            const uuid: string = prevState.selectedUuids.includes(element.uuid) ? element.uuid : prevState.selectedUuids.at(0) || '';
+            const selectedElement = shiftKeyPressed ? CamelDefinitionApiExt.findElementInIntegration(integration, uuid) : element;
+            return {
+                integration: i,
+                selectedStep: selectedElement,
+                showSelector: false,
+                selectedUuids: shiftKeyPressed ? [...prevState.selectedUuids] : [element.uuid],
+            }
+        });
+    }
+
+    unselectElement = (evt: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
+        if ((evt.target as any).dataset.click === 'FLOWS') {
+            evt.stopPropagation()
+            const i = CamelDisplayUtil.setIntegrationVisibility(this.routeDesigner.state.integration, undefined);
+            this.routeDesigner.setState(prevState => ({
+                integration: i,
+                selectedStep: undefined,
+                showSelector: false,
+                selectedPosition: undefined,
+                selectedUuids: [],
+            }));
+        }
+    }
+
+    openSelector = (parentId: string | undefined, parentDsl: string | undefined, showSteps: boolean = true, position?: number | undefined, selectorTabIndex?: string | number) => {
+        this.routeDesigner.setState({
+            showSelector: true,
+            parentId: parentId || '',
+            parentDsl: parentDsl,
+            showSteps: showSteps,
+            selectedPosition: position,
+            selectorTabIndex: selectorTabIndex
+        })
+    }
+
+    closeDslSelector = () => {
+        this.routeDesigner.setState({showSelector: false})
+    }
+
+    onDslSelect = (dsl: DslMetaModel, parentId: string, position?: number | undefined) => {
+        switch (dsl.dsl) {
+            case 'FromDefinition' :
+                const route = CamelDefinitionApi.createRouteDefinition({from: new FromDefinition({uri: dsl.uri})});
+                this.addStep(route, parentId, position)
+                break;
+            case 'ToDefinition' :
+                const to = CamelDefinitionApi.createStep(dsl.dsl, {uri: dsl.uri});
+                this.addStep(to, parentId, position)
+                break;
+            case 'ToDynamicDefinition' :
+                const toD = CamelDefinitionApi.createStep(dsl.dsl, {uri: dsl.uri});
+                this.addStep(toD, parentId, position)
+                break;
+            case 'KameletDefinition' :
+                const kamelet = CamelDefinitionApi.createStep(dsl.dsl, {name: dsl.name});
+                this.addStep(kamelet, parentId, position)
+                break;
+            default:
+                const step = CamelDefinitionApi.createStep(dsl.dsl, undefined);
+                this.addStep(step, parentId, position)
+                break;
+        }
+    }
+
+    createRouteConfiguration = () => {
+        const clone = CamelUtil.cloneIntegration(this.routeDesigner.state.integration);
+        const routeConfiguration = new RouteConfigurationDefinition();
+        const i = CamelDefinitionApiExt.addRouteConfigurationToIntegration(clone, routeConfiguration);
+        this.routeDesigner.setState(prevState => ({
+            integration: i,
+            propertyOnly: false,
+            key: Math.random().toString(),
+            selectedStep: routeConfiguration,
+            selectedUuids: [routeConfiguration.uuid],
+        }));
+    }
+
+    addStep = (step: CamelElement, parentId: string, position?: number | undefined) => {
+        const i = CamelDefinitionApiExt.addStepToIntegration(this.routeDesigner.state.integration, step, parentId, position);
+        const clone = CamelUtil.cloneIntegration(i);
+        EventBus.sendPosition("clean", step, undefined, new DOMRect(), new DOMRect(), 0);
+        this.routeDesigner.setState(prevState => ({
+            integration: clone,
+            key: Math.random().toString(),
+            showSelector: false,
+            selectedStep: step,
+            propertyOnly: false,
+            selectedUuids: [step.uuid],
+        }));
+    }
+
+    onIntegrationUpdate = (i: Integration) => {
+        this.routeDesigner.setState({integration: i, propertyOnly: false, showSelector: false, key: Math.random().toString()});
+    }
+
+    moveElement = (source: string, target: string, asChild: boolean) => {
+        const i = CamelDefinitionApiExt.moveRouteElement(this.routeDesigner.state.integration, source, target, asChild);
+        const clone = CamelUtil.cloneIntegration(i);
+        const selectedStep = CamelDefinitionApiExt.findElementInIntegration(clone, source);
+        this.routeDesigner.setState(prevState => ({
+            integration: clone,
+            key: Math.random().toString(),
+            showSelector: false,
+            selectedStep: selectedStep,
+            propertyOnly: false,
+            selectedUuids: [source],
+        }));
+    }
+
+    onResizePage(el: HTMLDivElement | null) {
+        const rect = el?.getBoundingClientRect();
+        if (el && rect && (el.scrollWidth !== this.routeDesigner.state.width || el.scrollHeight !== this.routeDesigner.state.height || rect.top !== this.routeDesigner.state.top || rect.left !== this.routeDesigner.state.left)) {
+            this.routeDesigner.setState({width: el.scrollWidth, height: el.scrollHeight, top: rect.top, left: rect.left})
+        }
+    }
+
+    downloadIntegrationImage(dataUrl: string) {
+        const a = document.createElement('a');
+        a.setAttribute('download', 'karavan-routes.png');
+        a.setAttribute('href', dataUrl);
+        a.click();
+    }
+
+    integrationImageDownloadFilter = (node: HTMLElement) => {
+        const exclusionClasses = ['add-flow'];
+        return !exclusionClasses.some(classname => {
+            return node.classList === undefined ? false : node.classList.contains(classname);
+        });
+    }
+
+    integrationImageDownload() {
+        if (this.routeDesigner.state.printerRef.current === null) {
+            return
+        }
+        toPng(this.routeDesigner.state.printerRef.current, {
+            style: {overflow: 'hidden'}, cacheBust: true, filter: this.integrationImageDownloadFilter,
+            height: this.routeDesigner.state.height, width: this.routeDesigner.state.width, backgroundColor: this.routeDesigner.props.dark ? "black" : "white"
+        }).then(v => {
+            toPng(this.routeDesigner.state.printerRef.current, {
+                style: {overflow: 'hidden'}, cacheBust: true, filter: this.integrationImageDownloadFilter,
+                height: this.routeDesigner.state.height, width: this.routeDesigner.state.width, backgroundColor: this.routeDesigner.props.dark ? "black" : "white"
+            }).then(this.downloadIntegrationImage);
+        })
+    }
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/designer/utils/EventBus.ts b/karavan-app/src/main/webui/src/designer/utils/EventBus.ts
index 2df867f4..aeae6ff7 100644
--- a/karavan-app/src/main/webui/src/designer/utils/EventBus.ts
+++ b/karavan-app/src/main/webui/src/designer/utils/EventBus.ts
@@ -48,6 +48,17 @@ export class DslPosition {
     }
 }
 
+const commands = new Subject<Command>();
+export class Command {
+    command: string;
+    data: any;
+
+    constructor(command: string, data: any) {
+        this.command = command;
+        this.data = data;
+    }
+}
+
 export const EventBus = {
     sendPosition: (command: "add" | "delete" | "clean",
                    step: CamelElement,
@@ -58,4 +69,7 @@ export const EventBus = {
                    inSteps: boolean = false,
                    isSelected: boolean = false) => positions.next(new DslPosition(command, step, parent, rect, headerRect, position, inSteps, isSelected)),
     onPosition: () => positions.asObservable(),
+
+    sendCommand: (command: string, data?: any) => commands.next(new Command(command, data)),
+    onCommand: () => commands.asObservable(),
 }