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/03/17 18:35:39 UTC

[camel-karavan] 01/01: Prototype #693

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

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

commit ba8562ebfd46ec438140666d62c5330a13edbf89
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Fri Mar 17 14:35:25 2023 -0400

    Prototype #693
---
 karavan-designer/src/App.tsx                       |  12 +-
 karavan-designer/src/data/DataMapper.tsx           | 192 +++++++++++++++++++++
 karavan-designer/src/data/DataMapperModel.tsx      | 118 +++++++++++++
 .../src/data/DataMappingConnections.tsx            |  61 +++++++
 karavan-designer/src/data/DataTreeItem.tsx         | 108 ++++++++++++
 karavan-designer/src/data/datamapper.css           | 118 +++++++++++++
 karavan-designer/src/index.css                     |   1 +
 7 files changed, 607 insertions(+), 3 deletions(-)

diff --git a/karavan-designer/src/App.tsx b/karavan-designer/src/App.tsx
index e536a093..b7bba3a9 100644
--- a/karavan-designer/src/App.tsx
+++ b/karavan-designer/src/App.tsx
@@ -34,6 +34,7 @@ import {KaravanIcon} from "./designer/utils/KaravanIcons";
 import './designer/karavan.css';
 import {DesignerPage} from "./DesignerPage";
 import {TemplateApi} from "karavan-core/lib/api/TemplateApi";
+import {DataMapper} from "./data/DataMapper";
 
 class ToastMessage {
     id: string = ''
@@ -76,7 +77,7 @@ interface State {
 class App extends React.Component<Props, State> {
 
     public state: State = {
-        pageId: "designer",
+        pageId: "datamapper",
         alerts: [],
         name: 'example.yaml',
         key: '',
@@ -106,13 +107,13 @@ class App extends React.Component<Props, State> {
             const kamelets: string[] = [];
             data[0].split("\n---\n").map(c => c.trim()).forEach(z => kamelets.push(z));
             KameletApi.saveKamelets(kamelets, true);
-            this.toast("Success", "Loaded " + kamelets.length + " kamelets", 'success');
+            // this.toast("Success", "Loaded " + kamelets.length + " kamelets", 'success');
 
             const jsons: string[] = [];
             JSON.parse(data[1]).forEach((c: any) => jsons.push(JSON.stringify(c)));
             ComponentApi.saveComponents(jsons, true);
 
-            this.toast("Success", "Loaded " + jsons.length + " components", 'success');
+            // this.toast("Success", "Loaded " + jsons.length + " components", 'success');
             this.setState({loaded: true});
 
             TemplateApi.saveTemplate("org.apache.camel.AggregationStrategy", data[2]);
@@ -147,6 +148,7 @@ class App extends React.Component<Props, State> {
             new MenuItem("eip", "Enterprise Integration Patterns", <EipIcon/>),
             new MenuItem("kamelets", "Kamelets", <KameletsIcon/>),
             new MenuItem("components", "Components", <ComponentsIcon/>),
+            new MenuItem("datamapper", "Data Mapper", <ComponentsIcon/>),
         ]
         return (<Flex className="nav-buttons" direction={{default: "column"}} style={{height: "100%"}}
                       spaceItems={{default: "spaceItemsNone"}}>
@@ -196,6 +198,10 @@ class App extends React.Component<Props, State> {
                 return (
                     <EipPage dark={dark}/>
                 )
+            case "datamapper":
+                return (
+                    <DataMapper dark={dark}/>
+                )
         }
     }
 
diff --git a/karavan-designer/src/data/DataMapper.tsx b/karavan-designer/src/data/DataMapper.tsx
new file mode 100644
index 00000000..3796b118
--- /dev/null
+++ b/karavan-designer/src/data/DataMapper.tsx
@@ -0,0 +1,192 @@
+/*
+ * 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, {RefObject} from 'react';
+import {
+    Button,
+    PageSection,
+    PageSectionVariants,
+    Tooltip,
+    TreeView
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import './datamapper.css';
+import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-alt-circle-right-icon';
+import CollapseIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon';
+import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-down-icon';
+import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon";
+import DownloadImageIcon from "@patternfly/react-icons/dist/esm/icons/image-icon";
+import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon";
+import {DataMappingConnections} from "./DataMappingConnections";
+import {TreeViewDataItem} from "@patternfly/react-core/components";
+import {DataTreeItem} from "./DataTreeItem";
+import {ConnectionPoint, ConnectionsRect, Exchange, ExchangeElement} from "./DataMapperModel";
+
+interface Props {
+    dark: boolean
+    tab?: string
+}
+
+interface State {
+    source: any [],
+    target: any [],
+    transformation: any [],
+    activeItems1: any [],
+    activeItems2: any [],
+    ref1: RefObject<HTMLDivElement>,
+    ref2: RefObject<HTMLDivElement>,
+    connections: ConnectionsRect,
+    startingPoint: ConnectionPoint,
+    movingPoint: ConnectionPoint
+}
+
+export class DataMapper extends React.Component<Props, State> {
+
+    state: State = {
+        activeItems1: [],
+        activeItems2: [],
+        source: [new Exchange({defaultExpanded: true})],
+        target: [new Exchange({defaultExpanded: true})],
+        transformation: [new ExchangeElement({id: "xxx", name: "new java.util.Date()", customBadgeContent: "java"})],
+        ref1: React.createRef(),
+        ref2: React.createRef(),
+        connections: {top: 0, left: 0, width: 0, height: 0},
+        startingPoint: new ConnectionPoint(0,0),
+        movingPoint: new ConnectionPoint(2000,2000)
+    }
+
+    ref1: RefObject<HTMLDivElement> = React.createRef();
+    ref2: RefObject<HTMLDivElement> = React.createRef();
+
+    interval: any;
+
+    componentDidMount() {
+        this.onRefresh();
+        this.interval = setInterval(() => this.onRefresh(), 300);
+    }
+
+    componentWillUnmount() {
+        clearInterval(this.interval);
+    }
+
+    onRefresh = () => {
+        const source = this.ref1.current?.children[0]?.children[0]?.getBoundingClientRect();
+        const target = this.ref2.current?.children[0]?.children[0]?.getBoundingClientRect();
+        const sourceTop = source?.top || 0;
+        const sourceLeft = source?.left || 0;
+        const sourceHeight = source?.height || 0;
+        const sourceWidth = source?.width || 0;
+        const targetTop = target?.top || 0;
+        const targetLeft = target?.left || 0;
+        const targetHeight = target?.height || 0;
+        const targetWidth = target?.width || 0;
+        const height = (sourceTop + sourceHeight > targetTop + targetHeight)
+            ? (sourceTop + sourceHeight)
+            : (targetTop + targetHeight);
+        const width = targetLeft + targetWidth - sourceLeft;
+        this.setState({connections: new ConnectionsRect(width, height, sourceTop, sourceLeft)})
+    }
+
+
+    onSelect = (evt: any, treeViewItem: any) => {
+        // Ignore folders for selection
+        if (treeViewItem && !treeViewItem.children) {
+            this.setState({
+                activeItems1: [treeViewItem],
+                activeItems2: [treeViewItem]
+            });
+        }
+    }
+
+    onDragStart = (rect: DOMRect) => {
+        const top = rect.top + (rect.height / 2);
+        const left = rect.left + rect.width;
+        this.setState({startingPoint: new ConnectionPoint(top, left)})
+    }
+
+    onMoving = (clientX: number, clientY: number) => {
+        this.setState({movingPoint: new ConnectionPoint(clientY, clientX)})
+    }
+
+    private onMapElements(source: ExchangeElement, target: ExchangeElement) {
+        this.setState({startingPoint: new ConnectionPoint(0, 0), movingPoint: new ConnectionPoint(0, 0),})
+    }
+
+    convertTreeItem(items: any [], type: 'source' | 'target' | 'transformation'): TreeViewDataItem[] {
+        return items.map((value: any) => this.convertTreeItems(value, type));
+    }
+
+    convertTreeItems(value: any, type: 'source' | 'target' | 'transformation'): TreeViewDataItem {
+        return {
+            id: value.id,
+            name: <DataTreeItem element={value}
+                                type={type}
+                                onDragStart={this.onDragStart}
+                                onMoving={this.onMoving}
+                                onMapElements={(source, target) => this.onMapElements(source, target)}/>,
+            children: value.children ? this.convertTreeItem(value.children, type) : undefined,
+            defaultExpanded: value.id === 'exchange',
+            action: value.id === 'headers' ? <Button variant={"plain"} icon={<AddIcon/>}/> : undefined,
+            customBadgeContent: value.customBadgeContent
+        }
+    }
+
+    render() {
+        const {activeItems1, startingPoint, movingPoint, source, target, transformation, connections} = this.state;
+        return (
+            <PageSection variant={this.props.dark ? PageSectionVariants.darker : PageSectionVariants.light} className="page" isFilled padding={{default: 'noPadding'}}>
+                <DataMappingConnections rect={connections} moving={movingPoint} starting={startingPoint}/>
+                <div className="exchange-mapper">
+                    <div className="exchange-tree-panel">
+                        <div className="data-toolbar">
+                            <div className="panel-header">
+                                Source
+                            </div>
+                        </div>
+                        <div className="exchange-tree-parent source" ref={this.ref1}>
+                            <TreeView data={this.convertTreeItem(source, 'source')} activeItems={activeItems1} hasBadges hasGuides/>
+                        </div>
+                    </div>
+                    <div className="exchange-tree-panel">
+                        <div className="data-toolbar">
+                            <div className="panel-header">
+                                Transformation
+                            </div>
+                            <Tooltip content="Add Transformation" position={"left"}>
+                                <Button variant="plain" icon={<AddIcon/>} onClick={e => {
+                                }}>
+                                </Button>
+                            </Tooltip>
+                        </div>
+                        <div className="exchange-tree-parent transformation">
+                            <TreeView data={transformation} hasSelectableNodes={false}/>
+                        </div>
+                    </div>
+                    <div className="exchange-tree-panel">
+                        <div className="data-toolbar">
+                            <div className="panel-header">
+                                Target
+                            </div>
+                        </div>
+                        <div className="exchange-tree-parent target" ref={this.ref2}>
+                            <TreeView data={this.convertTreeItem(target, 'target')} hasBadges hasGuides/>
+                        </div>
+                    </div>
+                </div>
+            </PageSection>
+        )
+    }
+}
\ No newline at end of file
diff --git a/karavan-designer/src/data/DataMapperModel.tsx b/karavan-designer/src/data/DataMapperModel.tsx
new file mode 100644
index 00000000..34c1854b
--- /dev/null
+++ b/karavan-designer/src/data/DataMapperModel.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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.
+ */
+
+export class ExchangeElement {
+    id: string = ''
+    name: string = ''
+    customBadgeContent: string = ''
+
+    public constructor(init?: Partial<ExchangeElement>) {
+        Object.assign(this, init);
+    }
+}
+
+export class ExchangeHeader extends ExchangeElement {
+
+}
+
+export class ExchangeProperty extends ExchangeElement {
+
+}
+
+export class ExchangeElementWithChildren extends ExchangeElement {
+    children: ExchangeElement[] = []
+    defaultExpanded: boolean = false;
+
+    public constructor(init?: Partial<ExchangeElementWithChildren>) {
+        super(init);
+        Object.assign(this, init);
+    }
+}
+
+export class ExchangeHeaders extends ExchangeElementWithChildren {
+
+    public constructor(init?: Partial<Body>) {
+        super(init);
+        this.name = "Headers";
+        this.id = "headers";
+        this.customBadgeContent = "Map<String, Object>";
+        this.children = Array.from(Array(10).keys()).map(value => new ExchangeHeader({id: "id" + value, name: "header" + value, customBadgeContent:"String"}));
+    }
+}
+
+export class ExchangeProperties extends ExchangeElementWithChildren {
+    public constructor(init?: Partial<Body>) {
+        super(init);
+        this.name = "Properties";
+        this.id = "properties";
+        this.customBadgeContent = "Map<String, Object>";
+    }
+}
+
+export class Body extends ExchangeElement {
+
+    public constructor(init?: Partial<Body>) {
+        super(init);
+        this.name = "Body";
+        this.id = "body";
+        this.customBadgeContent = "Object";
+    }
+}
+
+export class Exchange extends ExchangeElementWithChildren {
+
+    public constructor(init?: Partial<Exchange>) {
+        super(init);
+        this.customBadgeContent = "Exchange";
+        if (init?.name === undefined) {
+            this.name = "Exchange";
+        }
+        if (init?.id === undefined) {
+            this.id = "exchange";
+        }
+        if (!init?.children) {
+            this.children.push(new ExchangeElement({id:"exchangeId", name: "Exchange ID", customBadgeContent: "String"}))
+            this.children.push(new ExchangeHeaders())
+            this.children.push(new ExchangeProperties())
+            this.children.push(new Body())
+        }
+    }
+}
+
+export class ConnectionsRect {
+    width: number = 0
+    height: number = 0
+    top: number = 0
+    left: number = 0
+
+    constructor(width: number, height: number, top: number, left: number) {
+        this.width = width;
+        this.height = height;
+        this.top = top;
+        this.left = left;
+    }
+}
+
+export class ConnectionPoint {
+    top: number = 0
+    left: number = 0
+
+    constructor(top: number, left: number) {
+        this.top = top;
+        this.left = left;
+    }
+}
diff --git a/karavan-designer/src/data/DataMappingConnections.tsx b/karavan-designer/src/data/DataMappingConnections.tsx
new file mode 100644
index 00000000..a0521434
--- /dev/null
+++ b/karavan-designer/src/data/DataMappingConnections.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 './datamapper.css';
+import {ConnectionPoint, ConnectionsRect} from "./DataMapperModel";
+
+interface Props {
+    rect: ConnectionsRect
+    starting: ConnectionPoint
+    moving: ConnectionPoint
+}
+
+interface State {
+    connections:[]
+}
+
+export class DataMappingConnections extends React.Component<Props, State> {
+
+    public state: State = {
+        connections: [],
+    };
+
+    render() {
+        const {starting, moving} = this.props;
+        const {top, left, height, width} = this.props.rect;
+        const startX = starting.left - left;
+        const startY = starting.top - top;
+        const endX = moving.left - left;
+        const endY = moving.top - top;
+        const middleX = (endX + startX) / 2;
+        console.log(`M ${startX},${startY} C ${middleX},${startY} ${middleX},${endY}   ${endX},${endY}`);
+        return (
+            <svg className="data-mapping-connection"
+                style={{width: width, height: height, position: "absolute", left: left, top: top, backgroundColor: "transparent", zIndex:0}}
+                viewBox={"0 0 " + width + " " + height}>
+                <defs>
+                    <marker id="arrowhead" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto" className="arrow">
+                        <polygon points="0 0, 9 3, 0 6"/>
+                    </marker>
+                </defs>
+                {starting.top !==0 && moving.top !== 0
+                    && <path name={"moving"} d={`M ${startX},${startY} C ${middleX},${startY} ${middleX},${endY}   ${endX},${endY}`}
+                             className="path" key={"moving"} markerEnd="url(#arrowhead)"/>}
+            </svg>
+        );
+    }
+}
diff --git a/karavan-designer/src/data/DataTreeItem.tsx b/karavan-designer/src/data/DataTreeItem.tsx
new file mode 100644
index 00000000..52d300db
--- /dev/null
+++ b/karavan-designer/src/data/DataTreeItem.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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, {RefObject} from 'react';
+import './datamapper.css';
+import '../designer/karavan.css';
+import {ExchangeElement} from "./DataMapperModel";
+
+interface Props {
+    element: ExchangeElement
+    type: 'source' | 'target' | 'transformation'
+    onMapElements: (source: ExchangeElement, target: ExchangeElement) => void
+    onDragStart: (rect: DOMRect) => void
+    onMoving: (clientX: number, clientY: number) => void
+}
+
+interface State {
+    isDragging: boolean
+    isDraggedOver: boolean
+}
+
+export class DataTreeItem extends React.Component<Props, State> {
+
+    public state: State = {
+        isDragging: false,
+        isDraggedOver: false
+    }
+
+    ref:RefObject<HTMLDivElement> = React.createRef();
+
+    render() {
+        const {isDragging, isDraggedOver} = this.state;
+        const {element, type} = this.props;
+        const className = "exchange-item" ;
+        return (
+            <div key={"root-" + element.id}
+                 className={className}
+                 ref={this.ref}
+                 style={{
+                     // borderStyle: this.hasBorder() ? "dotted" : "none",
+                     // borderColor: this.isSelected() ? "var(--step-border-color-selected)" : "var(--step-border-color)",
+                     // marginTop: this.isInStepWithChildren() ? "16px" : "8px",
+                     // zIndex: element.dslName === 'ToDefinition' ? 20 : 10,
+                     boxShadow: isDraggedOver && type !== 'source' ? "0px 0px 1px 2px var(--step-border-color-selected)" : "none",
+                 }}
+                 onMouseOver={event => event.stopPropagation()}
+                 // onClick={event => this.selectElement(event)}body
+                 onDragStart={event => {
+                     event.stopPropagation();
+                     event.dataTransfer.setData("text/plain", element.id);
+                     (event.target as any).style.opacity = 1.5;
+                     (event.target as any).style.borderRadius = 16;
+                     this.setState({isDragging: true});
+                     const rect = this.ref.current?.getBoundingClientRect();
+                     if (rect) this.props.onDragStart?.call(this, rect);
+                 }}
+                 onDragEnd={event => {
+                     (event.target as any).style.opacity = '';
+                     this.setState({isDragging: false});
+                 }}
+                 onDragOver={event => {
+                     event.preventDefault();
+                     event.stopPropagation();
+                     if (element.id !== 'exchange') {
+                         this.setState({isDraggedOver: true});
+                     }
+                 }}
+                 onDragEnter={event => {
+                     event.preventDefault();
+                     event.stopPropagation();
+                     if (element.id !== 'exchange') {
+                         this.setState({isDraggedOver: true});
+                     }
+                 }}
+                 onDragLeave={event => {
+                     event.preventDefault();
+                     event.stopPropagation();
+                     this.setState({isDraggedOver: false});
+                 }}
+                 onDrag ={event => {
+                     this.props.onMoving?.call(this, event.nativeEvent.clientX, event.nativeEvent.clientY);
+                 }}
+                 onDrop={event => this.dragElement(event, element)}
+                 draggable={element.id !== 'exchange'}
+            >
+                {element.name}
+            </div>
+        );
+    }
+
+    private dragElement(event: React.DragEvent<HTMLDivElement>, element: ExchangeElement) {
+        this.props.onMapElements?.call(this, this.props.element, element);
+        this.setState({isDraggedOver: false});
+    }
+}
diff --git a/karavan-designer/src/data/datamapper.css b/karavan-designer/src/data/datamapper.css
new file mode 100644
index 00000000..7e624617
--- /dev/null
+++ b/karavan-designer/src/data/datamapper.css
@@ -0,0 +1,118 @@
+.exchange-mapper {
+    display: flex;
+    flex-direction: row;
+    padding: 16px;
+    justify-content: space-between;
+    background-color: transparent;
+}
+
+.exchange-tree-panel {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+}
+
+.exchange-tree-panel .pf-c-tree-view {
+    --pf-c-tree-view__node--indent--base: calc(var(--pf-global--spacer--md) * 2 + var(--pf-c-tree-view__node-toggle-icon--MinWidth));
+    --pf-c-tree-view__node--nested-indent--base: calc(var(--pf-c-tree-view__node--indent--base) - var(--pf-global--spacer--md));
+    --pf-c-tree-view__node--hover--BackgroundColor: transtparent;
+    --pf-c-tree-view__node--focus--BackgroundColor: transtparent;
+}
+
+.exchange-tree-panel .pf-c-tree-view__list-item .pf-c-tree-view__list-item .pf-c-tree-view__list-item {
+    /*--pf-c-tree-view__node--PaddingLeft: calc(var(--pf-c-tree-view__node--nested-indent--base) * 2 + var(--pf-c-tree-view__node--indent--base));*/
+    --pf-c-tree-view__node--PaddingLeft: calc(var(--pf-c-tree-view__node--nested-indent--base) * 2 + var(--pf-c-tree-view__node--indent--base)) ;
+}
+
+.exchange-tree-panel .pf-c-tree-view__node-count {
+    margin-top: auto;
+    margin-bottom: auto;
+}
+.exchange-tree-panel .pf-c-tree-view__node-count .pf-c-badge {
+    font-weight: 100;
+}
+
+/* width */
+.exchange-tree-parent::-webkit-scrollbar {
+    width: 10px;
+}
+/* Track */
+.exchange-tree-parent::-webkit-scrollbar-track {
+    background: #f1f1f1;
+}
+/* Handle */
+.exchange-tree-parent::-webkit-scrollbar-thumb {
+    background: #888;
+}
+/* Handle on hover */
+.exchange-tree-parent::-webkit-scrollbar-thumb:hover {
+    background: #555;
+}
+.exchange-tree-parent {
+    scrollbar-color: #9aa0a6 transparent;
+    scrollbar-width: thin;
+    scroll-behavior: smooth;
+    display: flex;
+    height: 100vh;
+    overflow: auto;
+}
+
+.exchange-tree-parent .pf-c-tree-view {
+    width: 100%;
+    padding: 0;
+    border: 1px solid var(--pf-global--disabled-color--300);
+}
+
+.draggable-element {
+    visibility: hidden;
+}
+
+.pf-c-tree-view__node {
+    padding-top: 0;
+    padding-bottom: 0;
+    padding-right: 0;
+}
+
+.exchange-item {
+    padding: 6px 6px 6px 6px;
+    border-radius: 16px;
+}
+
+.dragging-over {
+    border-radius: 16px;
+}
+
+.karavan .data-mapping-connection .path {
+    stroke: var(--pf-global--Color--200);
+    stroke-width: 1;
+    fill: transparent;
+}
+
+.source .pf-c-tree-view__content:hover .draggable-element,
+.transformation .pf-c-tree-view__content:hover .draggable-element {
+    visibility: visible;
+}
+
+.transformation .pf-c-tree-view__content .pf-c-tree-view__node-text {
+    border-color: var(--pf-global--Color--400);
+    border-radius: 16px;
+    border-width: 1px;
+    border-style: solid;
+    padding: 3px 6px 3px 6px;
+}
+
+.data-toolbar {
+    padding: 3px 6px 3px 6px;
+    display: flex;
+    justify-content: space-between;
+}
+.data-toolbar .pf-c-button {
+    padding: 0;
+}
+
+.panel-header {
+    padding-left: 6px;
+    padding-bottom: 3px;
+    font-size: 14px;
+    font-weight: bold;
+}
\ No newline at end of file
diff --git a/karavan-designer/src/index.css b/karavan-designer/src/index.css
index e6a61da8..8b2fe783 100644
--- a/karavan-designer/src/index.css
+++ b/karavan-designer/src/index.css
@@ -3,6 +3,7 @@ body,
 #root,
 .App {
   height: 100%;
+  font-size: 14px;
 }
 
 #root {