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