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/10/26 00:05:09 UTC

[camel-karavan] 05/07: Kamelet Editor in App for #315

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 cce1dd34b98e62a12afa7499ee8ff18ee6de4902
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Mon Oct 23 20:10:47 2023 -0400

    Kamelet Editor in App for #315
---
 .../src/core/model/IntegrationDefinition.ts        | 10 ++--
 .../main/webui/src/designer/KaravanDesigner.tsx    |  2 -
 .../webui/src/designer/icons/ComponentIcons.tsx    | 23 +++++++++-
 .../main/webui/src/designer/icons/KaravanIcons.tsx |  3 +-
 .../webui/src/designer/route/DslConnections.tsx    |  2 +
 .../main/webui/src/designer/route/DslElement.tsx   | 14 +++++-
 .../main/webui/src/designer/route/DslSelector.tsx  |  3 +-
 .../webui/src/designer/route/RouteDesigner.tsx     | 53 ++++++++++++++--------
 .../src/designer/route/useRouteDesignerHook.tsx    | 42 +++++++++++++++--
 .../src/main/webui/src/designer/utils/CamelUi.tsx  | 12 ++++-
 .../main/webui/src/knowledgebase/eip/EipModal.tsx  | 35 ++++++--------
 .../src/main/webui/src/project/file/FileEditor.tsx |  2 +-
 .../webui/src/project/files/CreateFileModal.tsx    | 51 +++++++++++++++++----
 .../src/main/webui/src/project/files/FilesTab.tsx  |  2 +-
 .../src/project/topology/ProjectTopologyTab.tsx    |  2 +-
 15 files changed, 186 insertions(+), 70 deletions(-)

diff --git a/karavan-core/src/core/model/IntegrationDefinition.ts b/karavan-core/src/core/model/IntegrationDefinition.ts
index fb1c11f3..18d01dc8 100644
--- a/karavan-core/src/core/model/IntegrationDefinition.ts
+++ b/karavan-core/src/core/model/IntegrationDefinition.ts
@@ -73,8 +73,10 @@ export class Spec {
     }
 }
 
+export type KameletTypes =  "sink" | "source" | "action";
+
 export class MetadataLabels {
-    "camel.apache.org/kamelet.type": "sink" | "source" | "action" = 'source'
+    "camel.apache.org/kamelet.type": KameletTypes = 'source'
 
     public constructor(init?: Partial<MetadataLabels>) {
         Object.assign(this, init);
@@ -82,10 +84,10 @@ export class MetadataLabels {
 }
 
 export class MetadataAnnotations {
-    "camel.apache.org/kamelet.support.level:": string = 'Preview';
+    "camel.apache.org/kamelet.support.level": string = 'Preview';
     "camel.apache.org/catalog.version": string = '';
-    "camel.apache.org/kamelet.icon": string = '';
-    "camel.apache.org/provider": string = '';
+    "camel.apache.org/kamelet.icon": string = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000000' viewBox='0 0 32 32' id='icon'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:none;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Eapplication%3C/title%3E%3Cpath d='M16,18H6a2,2,0,0,1-2-2V6A2,2,0,0,1,6,4H16a2,2,0,0,1,2,2V16A2,2,0,0,1,16,18ZM6,6V16H16V6Z' transform='translate(0 0)'/%3E%3Cpath d='M26,12v4H22V12h4m0-2H22a2,2,0,0,0-2,2v4a2,2,0,0,0,2,2h4a2,2,0,0,0,2-2V12a2,2,0,0,0-2-2Z' tran [...]
+    "camel.apache.org/provider": string = 'Custom';
     "camel.apache.org/kamelet.group": string = '';
     "camel.apache.org/kamelet.namespace": string = '';
 
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
index 4b7b3c7a..b1af73c5 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/KaravanDesigner.tsx
@@ -24,8 +24,6 @@ import {
     Tabs,
     TabTitleIcon,
     TabTitleText,
-    Tooltip,
-    TooltipPosition,
 } from '@patternfly/react-core';
 import './karavan.css';
 import {RouteDesigner} from "./route/RouteDesigner";
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/icons/ComponentIcons.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/icons/ComponentIcons.tsx
index a05f5e70..bda35d53 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/icons/ComponentIcons.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/icons/ComponentIcons.tsx
@@ -1165,7 +1165,28 @@ export function ApiIcon() {
     );
 }
 
-export function MonitoringIcon() {
+export function KameletIcon() {
+    return (
+        <svg
+            className="icon" id="icon"
+            xmlns="http://www.w3.org/2000/svg"
+            viewBox="0 0 32 32"
+        >
+            <title>{"application"}</title>
+            <path d="M16 18H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2ZM6 6v10h10V6ZM26 12v4h-4v-4h4m0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2ZM26 22v4h-4v-4h4m0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2ZM16 22v4h-4v-4h4m0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Z" />
+            <path
+                d="M0 0h32v32H0z"
+                data-name="&lt;Transparent Rectangle&gt;"
+                style={{
+                    fill: "none",
+                }}
+            />
+        </svg>
+    )
+}
+
+
+    export function MonitoringIcon() {
     return (
         <svg
             xmlns="http://www.w3.org/2000/svg"
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/icons/KaravanIcons.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/icons/KaravanIcons.tsx
index b7165b93..b0b8042f 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/icons/KaravanIcons.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/icons/KaravanIcons.tsx
@@ -260,7 +260,7 @@ export function CamelIcon(props?: (JSX.IntrinsicAttributes & React.SVGProps<SVGS
     );
 }
 
-export function getDesignerIcon(icon: string) {
+export function getDesignerIcon(icon: string): React.JSX.Element {
     if (icon === 'kamelet') return (
         <svg
             className="top-icon" id="icon"
@@ -429,6 +429,7 @@ export function getDesignerIcon(icon: string) {
             <rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" className="cls-1" width="32"
                   height="32" transform="translate(0 32) rotate(-90)"/>
         </svg>)
+    return <></>;
 }
 
 
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx
index de6a0060..a636339e 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx
@@ -63,6 +63,7 @@ export function DslConnections() {
             .filter(pos => ["FromDefinition"].includes(pos.step.dslName))
             .filter(pos => !TopologyUtils.isElementInternalComponent(pos.step))
             .filter(pos => !(pos.step.dslName === 'FromDefinition' && TopologyUtils.hasInternalUri(pos.step)))
+            .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source'))
             .sort((pos1: DslPosition, pos2: DslPosition) => {
                 const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
                 const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
@@ -142,6 +143,7 @@ export function DslConnections() {
             .filter(pos => pos.step.dslName === 'ToDefinition' && !CamelUi.isActionKamelet(pos.step) && !TopologyUtils.isElementInternalComponent(pos.step))
             .filter(pos => !(outgoingDefinitions.includes(pos.step.dslName) && TopologyUtils.hasInternalUri(pos.step)))
             .filter(pos => pos.step.dslName !== 'SagaDefinition')
+            .filter(pos => !CamelUi.isKameletSink(pos.step))
             .sort((pos1: DslPosition, pos2: DslPosition) => {
                 const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
                 const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslElement.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslElement.tsx
index 305b7859..9e4a8c70 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslElement.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslElement.tsx
@@ -42,7 +42,7 @@ interface Props {
 export function DslElement(props: Props) {
 
     const headerRef = React.useRef<HTMLDivElement>(null);
-    const {selectElement, moveElement, onShowDeleteConfirmation, openSelector} = useRouteDesignerHook();
+    const {selectElement, moveElement, onShowDeleteConfirmation, openSelector, isKamelet, isSourceKamelet, isActionKamelet} = useRouteDesignerHook();
 
     const [integration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow)
 
@@ -241,9 +241,19 @@ export function DslElement(props: Props) {
         )
     }
 
+    function getHeaderText(step: CamelElement): string {
+        if (isKamelet() && step.dslName === 'ToDefinition' && (step as any).uri === 'kamelet:sink') {
+            return "Sink";
+        } else if (isKamelet() && step.dslName === 'FromDefinition' && (step as any).uri === 'kamelet:source') {
+            return "Source";
+        } else {
+            return (step as any).description ? (step as any).description : CamelUi.getElementTitle(props.step);
+        }
+    }
+
     function getHeaderTextWithTooltip(step: CamelElement) {
         const checkRequired = CamelUtil.checkRequired(step);
-        const title = (step as any).description ? (step as any).description : CamelUi.getElementTitle(props.step);
+        const title = getHeaderText(step);
         let className = hasWideChildrenElement() ? "text text-right" : "text text-bottom";
         if (!checkRequired[0]) className = className + " header-text-required";
         if (checkRequired[0]) {
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslSelector.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslSelector.tsx
index 8f815536..a80525ba 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/route/DslSelector.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/route/DslSelector.tsx
@@ -24,7 +24,7 @@ import {
 import '../karavan.css';
 import {CamelUi} from "../utils/CamelUi";
 import {DslMetaModel} from "../utils/DslMetaModel";
-import {useDesignerStore, useSelectorStore} from "../DesignerStore";
+import {useDesignerStore, useIntegrationStore, useSelectorStore} from "../DesignerStore";
 import {shallow} from "zustand/shallow";
 import {useRouteDesignerHook} from "./useRouteDesignerHook";
 
@@ -40,7 +40,6 @@ export function DslSelector (props: Props) {
             [s.showSelector, s.showSteps, s.parentId, s.parentDsl, s.selectorTabIndex, s.setShowSelector, s.setSelectorTabIndex,
                 s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel], shallow)
 
-
     const [dark] = useDesignerStore((s) => [s.dark], shallow)
 
     const {onDslSelect} = useRouteDesignerHook();
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
index 215f334b..c57260db 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
@@ -40,7 +40,8 @@ import {DslElementMoveModal} from "./DslElementMoveModal";
 
 export function RouteDesigner() {
 
-    const {openSelector, createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement} = useRouteDesignerHook();
+    const {openSelector, createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement, onDslSelect,
+        isSourceKamelet, isActionKamelet, isKamelet, isSinkKamelet} = useRouteDesignerHook();
 
     const [integration] = useIntegrationStore((state) => [state.integration], shallow)
     const [showDeleteConfirmation, setPosition, width, height, top, left, hideLogDSL, showMoveConfirmation, setShowMoveConfirmation] =
@@ -104,6 +105,37 @@ export function RouteDesigner() {
         )
     }
 
+    function getGraphButtons() {
+        const routes = CamelUi.getRoutes(integration);
+        const showNewRoute = (isKamelet() && routes.length === 0) || !isKamelet();
+        const showNewRouteConfiguration = !isKamelet();
+        return (
+            <div className="add-flow">
+                {showNewRoute && <Button
+                    variant={routes.length === 0 ? "primary" : "secondary"}
+                    icon={<PlusIcon/>}
+                    onClick={e => {
+                        if (isSinkKamelet() || isActionKamelet()) {
+                            const dsl = CamelUi.getDslMetaModel('FromDefinition');
+                            dsl.uri = 'kamelet:source';
+                            onDslSelect(dsl, '', undefined);
+                        } else {
+                            openSelector(undefined, undefined)
+                        }
+                    }}
+                >
+                    Create route
+                </Button>}
+                {showNewRouteConfiguration && <Button
+                    variant="secondary"
+                    icon={<PlusIcon/>}
+                    onClick={e => createRouteConfiguration()}
+                >
+                    Create configuration
+                </Button>}
+            </div>
+        )
+    }
     function getGraph() {
         const routes = CamelUi.getRoutes(integration);
         const routeConfigurations = CamelUi.getRouteConfigurations(integration);
@@ -129,24 +161,7 @@ export function RouteDesigner() {
                                     step={route}
                                     parent={undefined}/>
                     ))}
-                    <div className="add-flow">
-                        <Button
-                            variant={routes.length === 0 ? "primary" : "secondary"}
-                            icon={<PlusIcon/>}
-                            onClick={e => {
-                                openSelector(undefined, undefined)
-                            }}
-                        >
-                            Create route
-                        </Button>
-                        <Button
-                            variant="secondary"
-                            icon={<PlusIcon/>}
-                            onClick={e => createRouteConfiguration()}
-                        >
-                            Create configuration
-                        </Button>
-                    </div>
+                    {getGraphButtons()}
                 </div>
             </div>)
     }
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx
index fe61d410..f48fc850 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx
@@ -19,7 +19,7 @@ import '../karavan.css';
 import {DslMetaModel} from "../utils/DslMetaModel";
 import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
 import {ChoiceDefinition, FromDefinition, LogDefinition, RouteConfigurationDefinition, RouteDefinition} from "karavan-core/lib/model/CamelDefinition";
-import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition";
+import {CamelElement, MetadataLabels} 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";
@@ -27,6 +27,7 @@ import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
 import {toPng} from 'html-to-image';
 import {useDesignerStore, useIntegrationStore, useSelectorStore} from "../DesignerStore";
 import {shallow} from "zustand/shallow";
+import {v4 as uuidv4} from 'uuid';
 
 export function useRouteDesignerHook () {
 
@@ -46,6 +47,34 @@ export function useRouteDesignerHook () {
         }
     }
 
+    function isKamelet(): boolean {
+        return integration.type === 'kamelet';
+    }
+
+    function isSourceKamelet(): boolean {
+        if (isKamelet()){
+            const m: MetadataLabels | undefined = integration.metadata.labels;
+            return m !== undefined && m["camel.apache.org/kamelet.type"] === 'source';
+        }
+        return false;
+    }
+
+    function isSinkKamelet(): boolean {
+        if (isKamelet()){
+            const m: MetadataLabels | undefined = integration.metadata.labels;
+            return m !== undefined && m["camel.apache.org/kamelet.type"] === 'sink';
+        }
+        return false;
+    }
+
+    function isActionKamelet(): boolean {
+        if (isKamelet()){
+            const m: MetadataLabels | undefined = integration.metadata.labels;
+            return m !== undefined && m["camel.apache.org/kamelet.type"] === 'action';
+        }
+        return false;
+    }
+
     const onShowDeleteConfirmation = (id: string) => {
         let message: string;
         const uuidsToDelete:string [] = [id];
@@ -193,13 +222,17 @@ export function useRouteDesignerHook () {
         setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'kamelet' : 'eip');
     }
 
-    const onDslSelect = (dsl: DslMetaModel, parentId: string, position?: number | undefined) => {
+    function onDslSelect (dsl: DslMetaModel, parentId: string, position?: number | undefined) {
         switch (dsl.dsl) {
             case 'FromDefinition' :
-                const route = CamelDefinitionApi.createRouteDefinition({from: new FromDefinition({uri: dsl.uri})});
+                const nodePrefixId = isKamelet() ? integration.metadata.name : 'route-' + uuidv4().substring(0,3);
+                const route = CamelDefinitionApi.createRouteDefinition({from: new FromDefinition({uri: dsl.uri}), nodePrefixId: nodePrefixId});
                 addStep(route, parentId, position)
                 break;
             case 'ToDefinition' :
+                if (dsl.uri === undefined && isKamelet()) {
+                    dsl.uri = 'kamelet:sink';
+                }
                 const to = CamelDefinitionApi.createStep(dsl.dsl, {uri: dsl.uri});
                 addStep(to, parentId, position)
                 break;
@@ -291,5 +324,6 @@ export function useRouteDesignerHook () {
     }
 
     return { deleteElement, selectElement, moveElement, onShowDeleteConfirmation, onDslSelect, openSelector,
-        createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement}
+        createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement, isKamelet, isSourceKamelet,
+        isActionKamelet, isSinkKamelet}
 }
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
index 6864361b..2b672a8d 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
@@ -51,7 +51,7 @@ import {
     IgniteIcon,
     InfinispanIcon,
     IotIcon,
-    KafkaIcon,
+    KafkaIcon, KameletIcon,
     KubernetesIcon,
     MachineLearningIcon,
     MailIcon,
@@ -93,6 +93,7 @@ import {
 import React from "react";
 import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
 import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
+import {getDesignerIcon} from "../icons/KaravanIcons";
 
 const StepElements: string[] = [
     "AggregateDefinition",
@@ -107,6 +108,7 @@ const StepElements: string[] = [
     // "ErrorHandlerDefinition",
     "FilterDefinition",
     "IdempotentConsumerDefinition",
+    "KameletDefinition",
     "LogDefinition",
     "LoopDefinition",
     "MarshalDefinition",
@@ -306,6 +308,10 @@ export class CamelUi {
         else return false;
     }
 
+    static isKameletSink = (element: CamelElement): boolean => {
+        return element.dslName === 'ToDefinition' && (element as any).uri === 'kamelet:sink';
+    }
+
     static getInternalRouteUris = (integration: Integration, componentName: string, showComponentName: boolean = true): string[] => {
         const result: string[] = [];
         integration.spec.flows?.filter(f => f.dslName === 'RouteDefinition')
@@ -519,7 +525,7 @@ export class CamelUi {
         }
     }
 
-    static getIconForDsl = (dsl: DslMetaModel): JSX.Element => {
+    static getIconForDsl = (dsl: DslMetaModel): React.JSX.Element => {
         if (dsl.dsl && (dsl.dsl === "KameletDefinition" || dsl.navigation === 'kamelet')) {
             return this.getIconFromSource(CamelUi.getKameletIconByName(dsl.name));
         } else if ((dsl.dsl && dsl.dsl === "FromDefinition")
@@ -687,6 +693,8 @@ export class CamelUi {
                 return <ApiIcon/>;
             case 'HeadDefinition' :
                 return <ApiIcon/>;
+            case 'KameletDefinition' :
+                return <KameletIcon/>;
             default:
                 return this.getIconFromSource(CamelUi.getIconSrcForName(dslName))
         }
diff --git a/karavan-web/karavan-app/src/main/webui/src/knowledgebase/eip/EipModal.tsx b/karavan-web/karavan-app/src/main/webui/src/knowledgebase/eip/EipModal.tsx
index d2100a25..902a3ada 100644
--- a/karavan-web/karavan-app/src/main/webui/src/knowledgebase/eip/EipModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/knowledgebase/eip/EipModal.tsx
@@ -15,16 +15,9 @@
  * limitations under the License.
  */
 import React from 'react';
-import {
-    Button,
-    Modal,
-    ActionGroup,
-    Text,
-    CardHeader,
-    Badge, Flex, CardTitle,
-} from '@patternfly/react-core';
+import {ActionGroup, Badge, Button, CardHeader, CardTitle, Flex, Modal, Text,} from '@patternfly/react-core';
 import '../../designer/karavan.css';
-import {Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import {Table, TableText, Tbody, Td, Th, Thead, Tr, WrapModifier} from "@patternfly/react-table";
 import {CamelUi} from "../../designer/utils/CamelUi";
 import {PropertyMeta} from "karavan-core/lib/model/CamelMetadata";
 import {useKnowledgebaseStore} from "../KnowledgebaseStore";
@@ -45,7 +38,7 @@ export function EipModal() {
             isOpen={isModalOpen}
             onClose={() => setModalOpen(false)}
             actions={[
-                <div className="modal-footer">
+                <div className="modal-footer" key="buttons">
                     <ActionGroup className="deploy-buttons">
                         <Button key="cancel" variant="primary"
                                 onClick={e => setModalOpen(false)}>Close</Button>
@@ -53,8 +46,7 @@ export function EipModal() {
                 </div>
             ]}
         >
-            <Flex direction={{default: 'column'}} key={element?.name}
-                  className="kamelet-modal-card">
+            <Flex direction={{default: 'column'}} key={element?.name} className="kamelet-modal-card">
                 <CardHeader actions={{ actions: <><Badge className="badge"
                                                          isRead> {element?.labels}</Badge></>, hasNoOffset: false, className: undefined}} >
                     {element && CamelUi.getIconForDslName(element?.className)}
@@ -67,27 +59,30 @@ export function EipModal() {
                         <Table aria-label="Simple table" variant='compact'>
                             <Thead>
                                 <Tr>
-                                    <Th key='name'>Display Name / Name</Th>
+                                    <Th key='name' width={10}>Name</Th>
+                                    <Th key='label'>Label</Th>
+                                    <Th key='display' width={10}>Display Name</Th>
                                     <Th key='desc'>Description</Th>
                                     <Th key='type'>Type</Th>
-                                    <Th key='label'>Label</Th>
                                 </Tr>
                             </Thead>
                             <Tbody>
                                 {element?.properties.map((p: PropertyMeta, idx: number) => (
                                     <Tr key={idx}>
-                                        <Td key={`${idx}_name`}>
-                                            <div>
-                                                <b>{p.displayName}</b>
-                                                <div>{p.name}</div>
-                                            </div>
+                                        <Td modifier={"fitContent"}>
+                                            {p.name}
+                                        </Td>
+                                        <Td modifier={"fitContent"}>
+                                            <Badge className="badge" isRead>{p.label}</Badge>
+                                        </Td>
+                                        <Td modifier={"fitContent"}>
+                                            {p.displayName}
                                         </Td>
                                         <Td key={`${idx}_desc`}><div>
                                             <div>{p.description}</div>
                                             {p.defaultValue && p.defaultValue.toString().length > 0 && <div>{"Default value: " + p.defaultValue}</div>}
                                         </div></Td>
                                         <Td key={`${idx}_type`}>{p.type}</Td>
-                                        <Td key={`${idx}_label`}>{p.label}</Td>
                                     </Tr>
                                 ))}
                             </Tbody>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx b/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
index 073b3bc3..8271c19f 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/file/FileEditor.tsx
@@ -88,7 +88,7 @@ export function FileEditor (props: Props) {
     const isKameletYaml = file !== undefined && file.name.endsWith(".kamelet.yaml");
     const isIntegration = isCamelYaml && file?.code && CamelDefinitionYaml.yamlIsIntegration(file.code);
     const isProperties = file !== undefined && file.name.endsWith("properties");
-    const showDesigner = isCamelYaml && isIntegration;
+    const showDesigner = (isCamelYaml && isIntegration) || isKameletYaml;
     const showEditor = !showDesigner && !isProperties;
     return (
         <>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx b/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx
index 36dc24ea..1221dc38 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx
@@ -25,16 +25,18 @@ import {
     ToggleGroupItem, ToggleGroup, FormHelperText, HelperText, HelperTextItem, TextInput
 } from '@patternfly/react-core';
 import '../../designer/karavan.css';
-import {Integration} from "karavan-core/lib/model/IntegrationDefinition";
+import {Integration, KameletTypes, MetadataLabels} from "karavan-core/lib/model/IntegrationDefinition";
 import {CamelDefinitionYaml} from "karavan-core/lib/api/CamelDefinitionYaml";
 import {useFileStore, useProjectStore} from "../../api/ProjectStore";
 import {ProjectFile, ProjectFileTypes} from "../../api/ProjectModels";
 import {CamelUi} from "../../designer/utils/CamelUi";
 import {ProjectService} from "../../api/ProjectService";
 import {shallow} from "zustand/shallow";
+import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
 
 interface Props {
-    types: string[]
+    types: string[],
+    isKameletsProject: boolean
 }
 
 export function CreateFileModal (props: Props) {
@@ -43,6 +45,7 @@ export function CreateFileModal (props: Props) {
     const [operation, setFile] = useFileStore((s) => [s.operation, s.setFile], shallow);
     const [name, setName] = useState<string>( '');
     const [fileType, setFileType] = useState<string>();
+    const [kameletType, setKameletType] = useState<KameletTypes>('source');
 
     useEffect(() => {
         if (props.types.length > 0) {
@@ -60,14 +63,28 @@ export function CreateFileModal (props: Props) {
         cleanValues();
     }
 
+    function getCode(): string {
+        if (fileType === 'INTEGRATION') {
+            return CamelDefinitionYaml.integrationToYaml(Integration.createNew(name, 'plain'));
+        } else if (fileType === 'KAMELET') {
+            const kameletName = name + (isKamelet ? '-'+kameletType : '');
+            const integration = Integration.createNew(kameletName, 'kamelet');
+            const meta:MetadataLabels = new MetadataLabels({"camel.apache.org/kamelet.type": kameletType});
+            integration.metadata.labels = meta;
+            console.log(integration);
+            return CamelDefinitionYaml.integrationToYaml(integration);
+        } else {
+            return '';
+        }
+    }
+
     function confirmAndCloseModal () {
         const extension = ProjectFileTypes.filter(value => value.name === fileType)[0].extension;
         const filename = (extension !== 'java') ? fileNameCheck(name) : CamelUi.javaNameFromTitle(name);
-        const code = fileType === 'INTEGRATION'
-            ? CamelDefinitionYaml.integrationToYaml(Integration.createNew(name, 'plain'))
-            : '';
+        const code = getCode();
         if (filename && extension) {
-            const file = new ProjectFile(filename + '.' + extension, project.projectId, code, Date.now());
+            const fullFileName = filename + (isKamelet ? '-'+kameletType : '') + '.' + extension;
+            const file = new ProjectFile(fullFileName, project.projectId, code, Date.now());
             ProjectService.createFile(file);
             cleanValues();
             if (code) {
@@ -82,10 +99,12 @@ export function CreateFileModal (props: Props) {
         return title.replace(/[^0-9a-zA-Z.]+/gi, "-").toLowerCase();
     }
 
+    const isKamelet = props.isKameletsProject;
     const extension = ProjectFileTypes.filter(value => value.name === fileType)[0]?.extension;
     const filename = (extension !== 'java')
         ? fileNameCheck(name)
-        : CamelUi.javaNameFromTitle(name)
+        : CamelUi.javaNameFromTitle(name);
+    const fullFileName = filename + (isKamelet ? '-'+kameletType : '') + '.' + extension;
     return (
         <Modal
             title="Create"
@@ -98,7 +117,7 @@ export function CreateFileModal (props: Props) {
             ]}
         >
             <Form autoComplete="off" isHorizontal className="create-file-form">
-                <FormGroup label="Type" fieldId="type" isRequired>
+                {!isKamelet && <FormGroup label="Type" fieldId="type" isRequired>
                     <ToggleGroup aria-label="Type" isCompact>
                         {ProjectFileTypes.filter(p => props.types.includes(p.name))
                             .map(p => {
@@ -110,12 +129,24 @@ export function CreateFileModal (props: Props) {
                                                         }}/>
                             })}
                     </ToggleGroup>
-                </FormGroup>
+                </FormGroup>}
+                {isKamelet && <FormGroup label="Kamelet Type" fieldId="kameletType" isRequired>
+                    <ToggleGroup aria-label="Kamelet Type">
+                        {['source', 'action', 'sink'].map((type) => {
+                            const title = CamelUtil.capitalizeName(type);
+                            return <ToggleGroupItem key={type} text={title} buttonId={type}
+                                                    isSelected={kameletType === type}
+                                                    onChange={(_, selected) => {
+                                                        setKameletType(type as KameletTypes);
+                                                    }}/>
+                        })}
+                    </ToggleGroup>
+                </FormGroup>}
                 <FormGroup label="Name" fieldId="name" isRequired>
                     <TextInput id="name" aria-label="name" value={name} onChange={(_, value) => setName(value)}/>
                     <FormHelperText  >
                         <HelperText id="helper-text1">
-                            <HelperTextItem variant={'default'}>{filename + '.' + extension}</HelperTextItem>
+                            <HelperTextItem variant={'default'}>{fullFileName}</HelperTextItem>
                         </HelperText>
                     </FormHelperText>
                 </FormGroup>
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/files/FilesTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/files/FilesTab.tsx
index d0396f96..e9dda209 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/files/FilesTab.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/files/FilesTab.tsx
@@ -159,7 +159,7 @@ export function FilesTab () {
                 </Table>
             </div>
             <UploadFileModal projectId={project.projectId}/>
-            <CreateFileModal types={types}/>
+            <CreateFileModal types={types} isKameletsProject={isKameletsProject()}/>
             <DeleteFileModal />
         </PageSection>
     )
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/topology/ProjectTopologyTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/topology/ProjectTopologyTab.tsx
index 935a91e8..8775353f 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/topology/ProjectTopologyTab.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/topology/ProjectTopologyTab.tsx
@@ -49,7 +49,7 @@ export const ProjectTopologyTab: React.FC = () => {
                 onClickCreateButton={() => setFile('create')}
                 onSetFile={(fileName) => selectFile(fileName)}
             />
-            <CreateFileModal types={['INTEGRATION']}/>
+            <CreateFileModal types={['INTEGRATION']} isKameletsProject={false}/>
         </>
     );
 }
\ No newline at end of file