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:17 UTC

[camel-karavan] branch main updated (504673cf -> e9177143)

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

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


    from 504673cf Copy/Paste Route
     new 8392b52d Apply last changes to space
     new 910e4379 Apply last changes to app
     new e9177143 Fix #632

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../quarkus-kubernetes-application.properties      |   2 +-
 .../quarkus-openshift-application.properties       |   2 +-
 .../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      |   0
 .../src/main/webui/src/designer/utils/EventBus.ts  |  14 +
 karavan-demo/aggregator/application.properties     |   2 +-
 .../quarkus/application.properties                 |   2 +-
 .../postman/project/application.properties         |   2 +-
 .../rest-service/quarkus/application.properties    |   2 +-
 karavan-space/src/designer/KaravanDesigner.tsx     |  11 +-
 karavan-space/src/designer/route/RouteDesigner.tsx | 370 ++-------------------
 .../src/designer/route/RouteDesignerLogic.tsx      |   0
 karavan-space/src/designer/utils/EventBus.ts       |  14 +
 karavan-vscode/CHANGELOG.md                        |   2 +
 karavan-vscode/package.json                        |   6 +-
 17 files changed, 120 insertions(+), 710 deletions(-)
 copy {karavan-designer => karavan-app/src/main/webui}/src/designer/route/RouteDesignerLogic.tsx (100%)
 copy {karavan-designer => karavan-space}/src/designer/route/RouteDesignerLogic.tsx (100%)


[camel-karavan] 01/03: Apply last changes to space

Posted by ma...@apache.org.
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 8392b52d10535ebde3c7ff84672a01466875a1cb
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Mon Jan 23 11:45:05 2023 -0500

    Apply last changes to space
---
 karavan-space/src/designer/KaravanDesigner.tsx     |  11 +-
 karavan-space/src/designer/route/RouteDesigner.tsx | 370 ++-------------------
 .../{RouteDesigner.tsx => RouteDesignerLogic.tsx}  | 346 ++++++-------------
 karavan-space/src/designer/utils/EventBus.ts       |  14 +
 4 files changed, 142 insertions(+), 599 deletions(-)

diff --git a/karavan-space/src/designer/KaravanDesigner.tsx b/karavan-space/src/designer/KaravanDesigner.tsx
index e7f975b8..a2556b2a 100644
--- a/karavan-space/src/designer/KaravanDesigner.tsx
+++ b/karavan-space/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-space/src/designer/route/RouteDesigner.tsx b/karavan-space/src/designer/route/RouteDesigner.tsx
index 67779963..dc0ab805 100644
--- a/karavan-space/src/designer/route/RouteDesigner.tsx
+++ b/karavan-space/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
@@ -71,9 +65,10 @@ 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,
@@ -93,315 +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, selectedUuids, clipboardSteps} = this.state;
-        if ((event.shiftKey)) {
-            this.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') {
-                    const steps: CamelElement[] = []
-                    selectedUuids.forEach(selectedUuid => {
-                        const selectedElement = CamelDefinitionApiExt.findElementInIntegration(integration, selectedUuid);
-                        if (selectedElement) {
-                            steps.push(selectedElement);
-                        }
-                    })
-                    this.saveToClipboard(steps);
-                } else if ((event.ctrlKey || event.metaKey) && charCode === 'v') {
-                    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 (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);
-                            }
-                        })
-                    }
-                }
-            }
-        } 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 = (steps: CamelElement[]): void => {
-        if (steps.length >0) {
-            this.setState(prevState => ({
-                key: Math.random().toString(),
-                clipboardSteps: [...steps]
-            }));
-        }
-    }
-
-    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,
-                selectedUuids: [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,
-            selectedUuids: [id],
-        }));
-    }
-
-    deleteElement = () => {
-        const id = this.state.selectedUuids.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,
-                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.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.state.integration, element.uuid);
-        this.setState((prevState: State) => {
-            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.state.integration, undefined);
-            this.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.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,
-            selectedUuids: [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,
-            selectedUuids: [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,
-            selectedUuids: [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() {
@@ -412,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>
             ]}
@@ -431,43 +152,14 @@ 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 {selectedUuids, integration, key, width, height, top, left} = this.state;
         const routes = CamelUi.getRoutes(integration);
@@ -475,15 +167,15 @@ export class RouteDesigner extends React.Component<Props, State> {
         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}
+                                    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}
@@ -493,10 +185,10 @@ 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}
+                                    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}
@@ -507,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-space/src/designer/route/RouteDesigner.tsx b/karavan-space/src/designer/route/RouteDesignerLogic.tsx
similarity index 50%
copy from karavan-space/src/designer/route/RouteDesigner.tsx
copy to karavan-space/src/designer/route/RouteDesignerLogic.tsx
index 67779963..18b2ce3c 100644
--- a/karavan-space/src/designer/route/RouteDesigner.tsx
+++ b/karavan-space/src/designer/route/RouteDesignerLogic.tsx
@@ -15,141 +15,70 @@
  * limitations under the License.
  */
 import React from 'react';
-import {
-    Drawer,
-    DrawerPanelContent,
-    DrawerContent,
-    DrawerContentBody,
-    Button, Modal,
-    PageSection,
-} 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 {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";
 
-interface Props {
-    onSave?: (integration: Integration, propertyOnly: boolean) => void
-    integration: Integration
-    dark: boolean
-}
-
-interface State {
-    integration: Integration
-    selectedStep?: CamelElement
-    showSelector: boolean
-    showDeleteConfirmation: boolean
-    deleteMessage: string
-    parentId: string
-    parentDsl?: string
-    selectedPosition?: number
-    showSteps: boolean
-    selectedUuids: string []
-    key: string
-    width: number
-    height: number
-    top: number
-    left: number
-    clipboardSteps: CamelElement[]
-    shiftKeyPressed?: boolean
-    ref?: any
-    printerRef?: any
-    propertyOnly: boolean
-    selectorTabIndex?: string | number
-}
+export class RouteDesignerLogic {
 
-export class RouteDesigner extends React.Component<Props, State> {
+    routeDesigner: RouteDesigner
+    commandSub?: Subscription
 
-    public state: State = {
-        integration: CamelDisplayUtil.setIntegrationVisibility(this.props.integration, undefined),
-        showSelector: false,
-        showDeleteConfirmation: false,
-        deleteMessage: '',
-        parentId: '',
-        showSteps: true,
-        selectedUuids: [],
-        clipboardSteps: [],
-        key: "",
-        width: 1000,
-        height: 1000,
-        top: 0,
-        left: 0,
-        ref: React.createRef(),
-        printerRef: React.createRef(),
-        propertyOnly: false,
-    };
+    constructor(routeDesigner: RouteDesigner) {
+        this.routeDesigner = routeDesigner;
+    }
 
     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;
+        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.setState({key: Math.random().toString()});
+            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.handleResize);
-        window.removeEventListener('keydown', this.handleKeyDown);
-        window.removeEventListener('keyup', this.handleKeyUp);
+        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.setState({key: Math.random().toString()});
+        this.routeDesigner.setState({key: Math.random().toString()});
     }
 
     handleKeyDown = (event: KeyboardEvent) => {
-        const {integration, selectedUuids, clipboardSteps} = this.state;
         if ((event.shiftKey)) {
-            this.setState({shiftKeyPressed: true});
+            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') {
-                    const steps: CamelElement[] = []
-                    selectedUuids.forEach(selectedUuid => {
-                        const selectedElement = CamelDefinitionApiExt.findElementInIntegration(integration, selectedUuid);
-                        if (selectedElement) {
-                            steps.push(selectedElement);
-                        }
-                    })
-                    this.saveToClipboard(steps);
+                    this.copyToClipboard();
                 } else if ((event.ctrlKey || event.metaKey) && charCode === 'v') {
-                    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 (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);
-                            }
-                        })
-                    }
+                    this.pasteFromClipboard();
                 }
             }
         } else {
@@ -160,35 +89,68 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     handleKeyUp = (event: KeyboardEvent) => {
-        this.setState({shiftKeyPressed: false});
+        this.routeDesigner.setState({shiftKeyPressed: false});
         if (event.repeat) {
             window.dispatchEvent(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);
+    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);
         }
     }
 
-    saveToClipboard = (steps: CamelElement[]): void => {
+    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.setState(prevState => ({
+            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.state.integration, element);
+            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.setState(prevState => ({
+            this.routeDesigner.setState(prevState => ({
                 integration: clone,
                 key: Math.random().toString(),
                 showSelector: false,
@@ -197,9 +159,9 @@ export class RouteDesigner extends React.Component<Props, State> {
                 selectedUuids: [element.uuid]
             }));
         } else {
-            const clone = CamelUtil.cloneIntegration(this.state.integration);
+            const clone = CamelUtil.cloneIntegration(this.routeDesigner.state.integration);
             const i = CamelDefinitionApiExt.updateIntegrationRouteElement(clone, element);
-            this.setState({integration: i, propertyOnly: true, key: Math.random().toString()});
+            this.routeDesigner.setState({integration: i, propertyOnly: true, key: Math.random().toString()});
         }
     }
 
@@ -207,9 +169,9 @@ export class RouteDesigner extends React.Component<Props, State> {
         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!;
+        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];
@@ -228,7 +190,7 @@ export class RouteDesigner extends React.Component<Props, State> {
         } else {
             message = 'Delete element from route?';
         }
-        this.setState(prevState => ({
+        this.routeDesigner.setState(prevState => ({
             showSelector: false,
             showDeleteConfirmation: true,
             deleteMessage: message,
@@ -237,10 +199,10 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     deleteElement = () => {
-        const id = this.state.selectedUuids.at(0);
+        const id = this.routeDesigner.state.selectedUuids.at(0);
         if (id) {
-            const i = CamelDefinitionApiExt.deleteStepFromIntegration(this.state.integration, id);
-            this.setState(prevState => ({
+            const i = CamelDefinitionApiExt.deleteStepFromIntegration(this.routeDesigner.state.integration, id);
+            this.routeDesigner.setState(prevState => ({
                 integration: i,
                 showSelector: false,
                 showDeleteConfirmation: false,
@@ -257,7 +219,7 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     selectElement = (element: CamelElement) => {
-        const {shiftKeyPressed, selectedUuids, integration} = this.state;
+        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;
@@ -265,8 +227,8 @@ export class RouteDesigner extends React.Component<Props, State> {
         }
         const add = shiftKeyPressed && !selectedUuids.includes(element.uuid);
         const remove = shiftKeyPressed && selectedUuids.includes(element.uuid);
-        const i = CamelDisplayUtil.setIntegrationVisibility(this.state.integration, element.uuid);
-        this.setState((prevState: State) => {
+        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);
@@ -287,8 +249,8 @@ export class RouteDesigner extends React.Component<Props, State> {
     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 => ({
+            const i = CamelDisplayUtil.setIntegrationVisibility(this.routeDesigner.state.integration, undefined);
+            this.routeDesigner.setState(prevState => ({
                 integration: i,
                 selectedStep: undefined,
                 showSelector: false,
@@ -299,7 +261,7 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     openSelector = (parentId: string | undefined, parentDsl: string | undefined, showSteps: boolean = true, position?: number | undefined, selectorTabIndex?: string | number) => {
-        this.setState({
+        this.routeDesigner.setState({
             showSelector: true,
             parentId: parentId || '',
             parentDsl: parentDsl,
@@ -310,7 +272,7 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     closeDslSelector = () => {
-        this.setState({showSelector: false})
+        this.routeDesigner.setState({showSelector: false})
     }
 
     onDslSelect = (dsl: DslMetaModel, parentId: string, position?: number | undefined) => {
@@ -339,10 +301,10 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     createRouteConfiguration = () => {
-        const clone = CamelUtil.cloneIntegration(this.state.integration);
+        const clone = CamelUtil.cloneIntegration(this.routeDesigner.state.integration);
         const routeConfiguration = new RouteConfigurationDefinition();
         const i = CamelDefinitionApiExt.addRouteConfigurationToIntegration(clone, routeConfiguration);
-        this.setState(prevState => ({
+        this.routeDesigner.setState(prevState => ({
             integration: i,
             propertyOnly: false,
             key: Math.random().toString(),
@@ -352,10 +314,10 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     addStep = (step: CamelElement, parentId: string, position?: number | undefined) => {
-        const i = CamelDefinitionApiExt.addStepToIntegration(this.state.integration, step, parentId, position);
+        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.setState(prevState => ({
+        this.routeDesigner.setState(prevState => ({
             integration: clone,
             key: Math.random().toString(),
             showSelector: false,
@@ -366,14 +328,14 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     onIntegrationUpdate = (i: Integration) => {
-        this.setState({integration: i, propertyOnly: false, showSelector: false, key: Math.random().toString()});
+        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.state.integration, source, target, asChild);
+        const i = CamelDefinitionApiExt.moveRouteElement(this.routeDesigner.state.integration, source, target, asChild);
         const clone = CamelUtil.cloneIntegration(i);
         const selectedStep = CamelDefinitionApiExt.findElementInIntegration(clone, source);
-        this.setState(prevState => ({
+        this.routeDesigner.setState(prevState => ({
             integration: clone,
             key: Math.random().toString(),
             showSelector: false,
@@ -385,60 +347,11 @@ export class RouteDesigner extends React.Component<Props, State> {
 
     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})
+        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})
         }
     }
 
-    getSelectorModal() {
-        return (
-            <DslSelector
-                isOpen={this.state.showSelector}
-                onClose={() => this.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}/>)
-    }
-
-    getDeleteConfirmation() {
-        let htmlContent: string = this.state.deleteMessage;
-        return (<Modal
-            className="modal-delete"
-            title="Confirmation"
-            isOpen={this.state.showDeleteConfirmation}
-            onClose={() => this.setState({showDeleteConfirmation: false})}
-            actions={[
-                <Button key="confirm" variant="primary" onClick={e => this.deleteElement()}>Delete</Button>,
-                <Button key="cancel" variant="link"
-                        onClick={e => this.setState({showDeleteConfirmation: false})}>Cancel</Button>
-            ]}
-            onEscapePress={e => this.setState({showDeleteConfirmation: false})}>
-            <div>
-                {htmlContent}
-            </div>
-        </Modal>)
-    }
-
-    getPropertiesPanel() {
-        return (
-            <DrawerPanelContent onResize={width => this.setState({key: Math.random().toString(1)})}
-                                style={{transform: "initial"}} isResizable hasNoBorder defaultSize={'400px'}
-                                maxSize={'800px'} minSize={'300px'}>
-                <DslProperties ref={this.state.ref}
-                               integration={this.state.integration}
-                               step={this.state.selectedStep}
-                               onIntegrationUpdate={this.onIntegrationUpdate}
-                               onPropertyUpdate={this.onPropertyUpdate}
-                               isRouteDesigner={true}
-                               dark={this.props.dark}/>
-            </DrawerPanelContent>
-        )
-    }
-
     downloadIntegrationImage(dataUrl: string) {
         const a = document.createElement('a');
         a.setAttribute('download', 'karavan-routes.png');
@@ -454,84 +367,17 @@ export class RouteDesigner extends React.Component<Props, State> {
     }
 
     integrationImageDownload() {
-        if (this.state.printerRef.current === null) {
+        if (this.routeDesigner.state.printerRef.current === null) {
             return
         }
-        toPng(this.state.printerRef.current, {
+        toPng(this.routeDesigner.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"
+            height: this.routeDesigner.state.height, width: this.routeDesigner.state.width, backgroundColor: this.routeDesigner.props.dark ? "black" : "white"
         }).then(v => {
-            toPng(this.state.printerRef.current, {
+            toPng(this.routeDesigner.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"
+                height: this.routeDesigner.state.height, width: this.routeDesigner.state.width, backgroundColor: this.routeDesigner.props.dark ? "black" : "white"
             }).then(this.downloadIntegrationImage);
         })
     }
-
-    getGraph() {
-        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)}>
-                    {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={selectedUuids}
-                                    inSteps={false}
-                                    position={index}
-                                    step={routeConfiguration}
-                                    parent={undefined}/>
-                    ))}
-                    {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={selectedUuids}
-                                    inSteps={false}
-                                    position={index}
-                                    step={route}
-                                    parent={undefined}/>
-                    ))}
-                    <div className="add-flow">
-                        <Button
-                            variant={routes.length === 0 ? "primary" : "secondary"}
-                            icon={<PlusIcon/>}
-                            onClick={e => this.openSelector(undefined, undefined)}>Create route
-                        </Button>
-                        <Button
-                            variant="secondary"
-                            icon={<PlusIcon/>}
-                            onClick={e => this.createRouteConfiguration()}>Create configuration
-                        </Button>
-                    </div>
-                </div>
-            </div>)
-    }
-
-    render() {
-        return (
-            <PageSection className="dsl-page" isFilled padding={{default: 'noPadding'}}>
-                <div className="dsl-page-columns">
-                    <Drawer isExpanded isInline>
-                        <DrawerContent panelContent={this.getPropertiesPanel()}>
-                            <DrawerContentBody>{this.getGraph()}</DrawerContentBody>
-                        </DrawerContent>
-                    </Drawer>
-                </div>
-                {this.getSelectorModal()}
-                {this.getDeleteConfirmation()}
-            </PageSection>
-        );
-    }
 }
\ No newline at end of file
diff --git a/karavan-space/src/designer/utils/EventBus.ts b/karavan-space/src/designer/utils/EventBus.ts
index 2df867f4..aeae6ff7 100644
--- a/karavan-space/src/designer/utils/EventBus.ts
+++ b/karavan-space/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(),
 }


[camel-karavan] 03/03: Fix #632

Posted by ma...@apache.org.
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 e9177143f6216a0c33f58277f0a3e6b21e2038d9
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Wed Jan 25 10:30:08 2023 -0500

    Fix #632
---
 .../resources/snippets/quarkus-kubernetes-application.properties    | 2 +-
 .../resources/snippets/quarkus-openshift-application.properties     | 2 +-
 karavan-demo/aggregator/application.properties                      | 2 +-
 karavan-demo/expose-pod-service/quarkus/application.properties      | 2 +-
 karavan-demo/postman/project/application.properties                 | 2 +-
 karavan-demo/rest-service/quarkus/application.properties            | 2 +-
 karavan-vscode/CHANGELOG.md                                         | 2 ++
 karavan-vscode/package.json                                         | 6 +++---
 8 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/karavan-app/src/main/resources/snippets/quarkus-kubernetes-application.properties b/karavan-app/src/main/resources/snippets/quarkus-kubernetes-application.properties
index eb73f218..c3fdc3a8 100644
--- a/karavan-app/src/main/resources/snippets/quarkus-kubernetes-application.properties
+++ b/karavan-app/src/main/resources/snippets/quarkus-kubernetes-application.properties
@@ -3,7 +3,7 @@ camel.karavan.project-name={projectName}
 camel.karavan.project-description={projectDescription}
 camel.jbang.gav=org.camel.karavan.demo:{projectId}:1
 camel.jbang.runtime=quarkus
-camel.jbang.quarkusVersion=2.13.5.Final
+camel.jbang.quarkusVersion=2.16.0.Final
 camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-kubernetes,mvn:io.quarkus:quarkus-container-image-jib
 camel.health.enabled=true
 camel.health.exposure-level=full
diff --git a/karavan-app/src/main/resources/snippets/quarkus-openshift-application.properties b/karavan-app/src/main/resources/snippets/quarkus-openshift-application.properties
index 31de7653..f3fac222 100644
--- a/karavan-app/src/main/resources/snippets/quarkus-openshift-application.properties
+++ b/karavan-app/src/main/resources/snippets/quarkus-openshift-application.properties
@@ -3,7 +3,7 @@ camel.karavan.project-name={projectName}
 camel.karavan.project-description={projectDescription}
 camel.jbang.gav=org.camel.karavan.demo:{projectId}:1
 camel.jbang.runtime=quarkus
-camel.jbang.quarkusVersion=2.13.5.Final
+camel.jbang.quarkusVersion=2.16.0.Final
 camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-openshift,mvn:io.quarkus:quarkus-container-image-jib
 camel.health.enabled=true
 camel.health.exposure-level=full
diff --git a/karavan-demo/aggregator/application.properties b/karavan-demo/aggregator/application.properties
index 397a47f5..4222006a 100644
--- a/karavan-demo/aggregator/application.properties
+++ b/karavan-demo/aggregator/application.properties
@@ -4,7 +4,7 @@ camel.jbang.project-name=Aggregator
 camel.jbang.project-description=Aggregator
 camel.jbang.gav=org.camel.karavan.demo:aggregator:1.0.0
 camel.jbang.runtime=quarkus
-camel.jbang.quarkusVersion=2.12.2.Final
+camel.jbang.quarkusVersion=2.16.0.Final
 camel.jbang.dependencies=mvn:io.quarkus:quarkus-container-image-jib,mvn:org.apache.camel.quarkus:camel-quarkus-microprofile-health,camel:file
 camel.health.enabled=true
 camel.health.exposure-level=full
diff --git a/karavan-demo/expose-pod-service/quarkus/application.properties b/karavan-demo/expose-pod-service/quarkus/application.properties
index 5a72a54c..057e9f40 100644
--- a/karavan-demo/expose-pod-service/quarkus/application.properties
+++ b/karavan-demo/expose-pod-service/quarkus/application.properties
@@ -3,7 +3,7 @@ camel.karavan.project-name=NodePortExpose
 camel.karavan.project-description=Example to show the way to expose POD port via NodePort service
 camel.jbang.gav=org.camel.karavan.demo:nodeportexpose:1
 camel.jbang.runtime=quarkus
-camel.jbang.quarkusVersion=2.13.5.Final
+camel.jbang.quarkusVersion=2.16.0.Final
 camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-kubernetes,mvn:io.quarkus:quarkus-container-image-jib
 camel.health.enabled=true
 camel.health.exposure-level=full
diff --git a/karavan-demo/postman/project/application.properties b/karavan-demo/postman/project/application.properties
index 35ba31fb..d0ba2d28 100644
--- a/karavan-demo/postman/project/application.properties
+++ b/karavan-demo/postman/project/application.properties
@@ -3,7 +3,7 @@ camel.jbang.project-name=postman
 camel.jbang.project-description=Postman Demo
 camel.jbang.gav=org.camel.karavan.demo:postman:1.0.0
 camel.jbang.runtime=quarkus
-camel.jbang.quarkusVersion=2.12.2.Final
+camel.jbang.quarkusVersion=2.16.0.Final
 camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-container-image-jib,mvn:io.quarkus:quarkus-openshift
 camel.health.enabled=true
 camel.health.exposure-level=full
diff --git a/karavan-demo/rest-service/quarkus/application.properties b/karavan-demo/rest-service/quarkus/application.properties
index 340eeef9..6e396053 100644
--- a/karavan-demo/rest-service/quarkus/application.properties
+++ b/karavan-demo/rest-service/quarkus/application.properties
@@ -3,7 +3,7 @@ camel.karavan.project-name=OpenApi
 camel.karavan.project-description=Example to show Openapi usage
 camel.jbang.gav=org.camel.karavan.demo:openapi:1
 camel.jbang.runtime=quarkus
-camel.jbang.quarkusVersion=2.13.5.Final
+camel.jbang.quarkusVersion=2.16.0.Final
 camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-kubernetes,mvn:io.quarkus:quarkus-container-image-jib
 camel.health.enabled=true
 camel.health.exposure-level=full
diff --git a/karavan-vscode/CHANGELOG.md b/karavan-vscode/CHANGELOG.md
index 99cd77a3..163cc15c 100644
--- a/karavan-vscode/CHANGELOG.md
+++ b/karavan-vscode/CHANGELOG.md
@@ -7,6 +7,8 @@
 3. Quarkus and Spring Boot in cloud-native mode
 4. Kamelets 3.20.0
 5. Route Configuration DSL
+6. Quarkus 2.16.0.Final 
+7. Spring Boot 2.7.7
 
 ## 3.18.5
 1. First preview of Karavan Operator 
diff --git a/karavan-vscode/package.json b/karavan-vscode/package.json
index 35547b24..e82b0085 100644
--- a/karavan-vscode/package.json
+++ b/karavan-vscode/package.json
@@ -196,7 +196,7 @@
             "camel.karavan.project-description=$NAME",
             "camel.karavan.target=$TARGET",
             "camel.jbang.gav=$GAV",
-            "camel.jbang.quarkusVersion=2.15.3.Final",
+            "camel.jbang.quarkusVersion=2.16.0.Final",
             "camel.jbang.runtime=$RUNTIME",
             "camel.jbang.exportDir=.export"
           ],
@@ -237,7 +237,7 @@
             "type": "string"
           },
           "default": [
-            "camel.jbang.quarkusVersion=2.15.3.Final",
+            "camel.jbang.quarkusVersion=2.16.0.Final",
             "camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-openshift,mvn:io.quarkus:quarkus-container-image-jib",
             "# image build properties",
             "quarkus.container-image.build=true",
@@ -275,7 +275,7 @@
             "type": "string"
           },
           "default": [
-            "camel.jbang.quarkusVersion=2.15.3.Final",
+            "camel.jbang.quarkusVersion=2.16.0.Final",
             "camel.jbang.dependencies=camel:microprofile-health,mvn:io.quarkus:quarkus-container-image-jib,mvn:io.quarkus:quarkus-kubernetes",
             "# image build properties",
             "quarkus.container-image.build=true",


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

Posted by ma...@apache.org.
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(),
 }