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


The following commit(s) were added to refs/heads/main by this push:
     new 685d76cc ROute Templates in Topology
685d76cc is described below

commit 685d76ccabf3ee5eb75923d3644c4e6e608447f1
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Tue Nov 26 17:08:24 2024 -0500

    ROute Templates in Topology
---
 .../src/main/webui/src/designer/DesignerStore.ts   |   6 ++
 .../src/main/webui/src/designer/karavan.css        |  19 ----
 .../webui/src/designer/property/DslProperties.tsx  |   3 +-
 .../property/property/ComponentPropertyField.tsx   |   6 +-
 .../property/property/DslPropertyField.tsx         |   5 +-
 .../webui/src/designer/route/RouteDesigner.tsx     |  24 ++++-
 .../src/designer/route/element/DslElement.css      |  18 ++++
 .../src/designer/route/element/DslElement.tsx      |  17 ++--
 .../designer/route/element/DslElementHeader.tsx    |  22 +++--
 .../route/element/RouteTemplateElement.css         |  64 +++++++++++++
 .../route/element/RouteTemplateElement.tsx         | 106 +++++++++++++++++++++
 .../src/designer/route/useRouteDesignerHook.tsx    |  29 +++++-
 .../src/main/webui/src/designer/utils/CamelUi.tsx  |   8 ++
 .../src/main/webui/src/topology/CustomGroup.tsx    |  10 +-
 .../src/main/webui/src/topology/CustomNode.tsx     |  16 +++-
 .../src/main/webui/src/topology/TopologyApi.tsx    |   5 +-
 .../src/main/webui/src/topology/TopologyLegend.tsx |  44 +++++++++
 .../src/main/webui/src/topology/TopologyTab.tsx    |   2 +
 .../src/main/webui/src/topology/topology.css       |  50 +++++++++-
 19 files changed, 398 insertions(+), 56 deletions(-)

diff --git a/karavan-app/src/main/webui/src/designer/DesignerStore.ts 
b/karavan-app/src/main/webui/src/designer/DesignerStore.ts
index e207c546..cd610c0f 100644
--- a/karavan-app/src/main/webui/src/designer/DesignerStore.ts
+++ b/karavan-app/src/main/webui/src/designer/DesignerStore.ts
@@ -110,6 +110,8 @@ interface SelectorStateState {
     deleteSelectedToggle: (label: string) => void;
     routeId?: string;
     setRouteId: (routeId: string) => void;
+    isRouteTemplate?: boolean;
+    setIsRouteTemplate: (isRouteTemplate: boolean) => void;
 }
 
 export const useSelectorStore = createWithEqualityFn<SelectorStateState>((set) 
=> ({
@@ -117,6 +119,7 @@ export const useSelectorStore = 
createWithEqualityFn<SelectorStateState>((set) =
     deleteMessage: '',
     parentId: '',
     showSteps: true,
+    isRouteTemplate: false,
     selectedToggles: ['eip', 'components', 'kamelets'],
     addSelectedToggle: (toggle: string) => {
         set(state => ({
@@ -149,6 +152,9 @@ export const useSelectorStore = 
createWithEqualityFn<SelectorStateState>((set) =
     setRouteId: (routeId: string) => {
         set({routeId: routeId})
     },
+    setIsRouteTemplate: (isRouteTemplate: boolean) => {
+        set({isRouteTemplate: isRouteTemplate})
+    },
 }), shallow)
 
 
diff --git a/karavan-app/src/main/webui/src/designer/karavan.css 
b/karavan-app/src/main/webui/src/designer/karavan.css
index 74b64c36..870cb9a4 100644
--- a/karavan-app/src/main/webui/src/designer/karavan.css
+++ b/karavan-app/src/main/webui/src/designer/karavan.css
@@ -388,21 +388,6 @@
     gap: 6px;
 }
 
-.karavan .dsl-page .flows .step-element {
-    align-items: center;
-    text-align: center;
-    display: flex;
-    flex-direction: column;
-    width: fit-content;
-    border-color: var(--pf-v5-global--Color--200);
-    border-radius: 42px;
-    border-width: 1px;
-    min-width: 120px;
-    padding: 3px 4px 4px 4px;
-    margin: 3px auto 0 auto;
-    position: relative;
-}
-
 .karavan {
     --step-border-color: var(--pf-v5-global--Color--200);
     --step-border-color-selected: var(--pf-v5-global--active-color--100);
@@ -449,10 +434,6 @@
 }
 
 .element-builder:hover .add-button,
-.karavan .step-element:hover .add-element-button,
-.karavan .step-element:hover .add-button {
-    visibility: visible;
-}
 
 .dsl-gallery {
     margin-top: 16px;
diff --git a/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx 
b/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx
index fb52dbc9..9511b1f8 100644
--- a/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx
+++ b/karavan-app/src/main/webui/src/designer/property/DslProperties.tsx
@@ -97,7 +97,8 @@ export function DslProperties(props: Props) {
             .filter((p: PropertyMeta) => p.name !== 'uri') // do not show uri
             // .filer((p: PropertyMeta) => (showAdvanced && 
p.label.includes('advanced')) || (!showAdvanced && 
!p.label.includes('advanced')))
             .filter((p: PropertyMeta) => !p.isObject || (p.isObject && 
!CamelUi.dslHasSteps(p.type)) || (p.name === 'onWhen'))
-            .filter((p: PropertyMeta) => !(dslName === 'RestDefinition' && 
['get', 'post', 'put', 'patch', 'delete', 'head'].includes(p.name)));
+            .filter((p: PropertyMeta) => !(dslName === 'RestDefinition' && 
['get', 'post', 'put', 'patch', 'delete', 'head'].includes(p.name)))
+            .filter((p: PropertyMeta) => !(dslName === 
'RouteTemplateDefinition' && ['route', 'beans'].includes(p.name)));
         // .filter((p: PropertyMeta) => dslName && !(['RestDefinition', 
'GetDefinition', 'PostDefinition', 'PutDefinition', 'PatchDefinition', 
'DeleteDefinition', 'HeadDefinition'].includes(dslName) && ['param', 
'responseMessage'].includes(p.name))) // TODO: configure this properties
     }
 
diff --git 
a/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx
 
b/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx
index eed5d12a..27e22780 100644
--- 
a/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx
+++ 
b/karavan-app/src/main/webui/src/designer/property/property/ComponentPropertyField.tsx
@@ -269,7 +269,7 @@ export function ComponentPropertyField(props: Props) {
                            type={property.secret && !showPassword ? "password" 
: "text"}
                            autoComplete="off"
                            id={id} name={id}
-                           value={textValue !== undefined ? textValue : 
property.defaultValue}
+                           value={(textValue !== undefined ? textValue : 
property.defaultValue) || ''}
                            onBlur={_ => parametersChanged(property.name, 
textValue, property.kind === 'path')}
                            onChange={(_, v) => {
                                setTextValue(v);
@@ -325,7 +325,7 @@ export function ComponentPropertyField(props: Props) {
                         type={(property.secret ? "password" : "text")}
                         autoComplete="off"
                         id={id} name={id}
-                        value={textValue !== undefined ? textValue : 
property.defaultValue}
+                        value={(textValue !== undefined ? textValue : 
property.defaultValue) || ''}
                         onBlur={_ => parametersChanged(property.name, 
textValue, property.kind === 'path')}
                         onChange={(_, v) => {
                             setTextValue(v);
@@ -396,7 +396,7 @@ export function ComponentPropertyField(props: Props) {
                         name={property.name + "-placeholder"}
                         type="text"
                         aria-label="placeholder"
-                        value={!isValueBoolean ? textValue?.toString() : 
undefined}
+                        value={!isValueBoolean ? textValue?.toString() : ''}
                         onBlur={_ => onParametersChange(property.name, 
textValue)}
                         onChange={(_, v) => {
                             setTextValue(v);
diff --git 
a/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx
 
b/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx
index c8346056..5331f8fb 100644
--- 
a/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx
+++ 
b/karavan-app/src/main/webui/src/designer/property/property/DslPropertyField.tsx
@@ -205,7 +205,7 @@ export function DslPropertyField(props: Props) {
     }
 
     function isParameter(property: PropertyMeta): boolean {
-        return property.name === 'parameters' && property.description === 
'parameters';
+        return property.name === 'parameters' || property.description === 
'parameters';
     }
 
     function getLabel(property: PropertyMeta, value: any, isKamelet: boolean) {
@@ -230,7 +230,7 @@ export function DslPropertyField(props: Props) {
             )
         }
         if (isParameter(property)) {
-            return isKamelet ? "Kamelet properties:" : "Component properties:";
+            return isKamelet ? "Kamelet properties:" : isRouteTemplate ? 
"Parameters:" : "Component properties:";
         } else if (!["ExpressionDefinition"].includes(property.type)) {
             return (
                 <div style={{display: "flex", flexDirection: 'row', 
alignItems: 'center', gap: '3px'}}>
@@ -1084,6 +1084,7 @@ export function DslPropertyField(props: Props) {
 
     const element = props.element;
     const isKamelet = CamelUtil.isKameletComponent(element);
+    const isRouteTemplate = element?.dslName === 'RouteTemplateDefinition';
     const property: PropertyMeta = props.property;
     const value = props.value;
     const isVariable = getIsVariable();
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 c75a6640..6898bd85 100644
--- a/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
+++ b/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx
@@ -37,11 +37,12 @@ import {Command, EventBus} from "../utils/EventBus";
 import useMutationsObserver from "./useDrawerMutationsObserver";
 import {DeleteConfirmation} from "./DeleteConfirmation";
 import {DslElementMoveModal} from "./element/DslElementMoveModal";
+import {RouteTemplateElement} from "./element/RouteTemplateElement";
 
 export function RouteDesigner() {
 
     const {openSelector, createRouteConfiguration, onCommand, unselectElement, 
onDslSelect,
-        isSourceKamelet, isActionKamelet, isKamelet, isSinkKamelet} = 
useRouteDesignerHook();
+        isSourceKamelet, isActionKamelet, isKamelet, isSinkKamelet, 
createRouteTemplate} = useRouteDesignerHook();
 
     const [integration] = useIntegrationStore((state) => [state.integration], 
shallow)
     const [showDeleteConfirmation, setPosition, width, height, top, left, 
showMoveConfirmation, setShowMoveConfirmation] =
@@ -130,12 +131,23 @@ export function RouteDesigner() {
                 >
                     Create configuration
                 </Button>}
+                {<Button
+                    variant="secondary"
+                    icon={<PlusIcon/>}
+                    onClick={evt => {
+                        evt.stopPropagation();
+                        openSelector(undefined, undefined, undefined, 
undefined, true);
+                    }}
+                >
+                    Create template
+                </Button>}
             </div>
         )
     }
     function getGraph() {
         const routes = CamelUi.getRoutes(integration);
         const routeConfigurations = 
CamelUi.getRouteConfigurations(integration);
+        const routeTemplates = CamelUi.getRouteTemplates(integration);
         return (
             <div className="graph" ref={printerRef}>
                 <DslConnections/>
@@ -154,6 +166,16 @@ export function RouteDesigner() {
                                     inStepsLength={array.length}
                                     parent={undefined}/>
                     ))}
+                    {routeTemplates?.map((routeTemplate, index: number, array) 
=> (
+                        <RouteTemplateElement key={routeTemplate.uuid}
+                                    inSteps={false}
+                                    position={index}
+                                    step={routeTemplate}
+                                    nextStep={undefined}
+                                    prevStep={undefined}
+                                    inStepsLength={array.length}
+                                    parent={undefined}/>
+                    ))}
                     {routes?.map((route: any, index: number, array) => {
                         return (
                             <DslElement key={route.uuid}
diff --git 
a/karavan-app/src/main/webui/src/designer/route/element/DslElement.css 
b/karavan-app/src/main/webui/src/designer/route/element/DslElement.css
index 1b431837..4705b76a 100644
--- a/karavan-app/src/main/webui/src/designer/route/element/DslElement.css
+++ b/karavan-app/src/main/webui/src/designer/route/element/DslElement.css
@@ -14,6 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+.karavan .dsl-page .flows .step-element {
+    align-items: center;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    width: fit-content;
+    border-color: var(--pf-v5-global--Color--200);
+    border-radius: 42px;
+    border-width: 1px;
+    min-width: 120px;
+    padding: 3px 4px 4px 4px;
+    margin: 3px auto 0 auto;
+    position: relative;
+}
+.karavan .dsl-page .flows .step-element:hover .add-element-button,
+.karavan .dsl-page .flows .step-element:hover .add-button {
+    visibility: visible;
+}
 
 .karavan .dsl-page .flows .step-element .header-route {
     display: block;
diff --git 
a/karavan-app/src/main/webui/src/designer/route/element/DslElement.tsx 
b/karavan-app/src/main/webui/src/designer/route/element/DslElement.tsx
index b11c693d..cf5e4200 100644
--- a/karavan-app/src/main/webui/src/designer/route/element/DslElement.tsx
+++ b/karavan-app/src/main/webui/src/designer/route/element/DslElement.tsx
@@ -243,17 +243,12 @@ export function DslElement(props: Props) {
         const hideAddButton = step.dslName === 'StepDefinition' && 
!CamelDisplayUtil.isStepDefinitionExpanded(integration, step.uuid, 
selectedUuids.at(0));
         if (hideAddButton) return (<></>)
         else return (
-                <Tooltip position={"left"}
-                         content={<div>{"Add step to " + 
CamelDisplayUtil.getTitle(step)}</div>}
-                >
-                    <button type="button"
-                            aria-label="Add"
-                            onClick={e => onOpenSelector(e)}
-                            className={isAddStepButtonLeft() ? "add-button 
add-button-left" : "add-button add-button-bottom"}>
-                        <AddElementIcon/>
-                    </button>
-
-                </Tooltip>
+            <button type="button"
+                    aria-label="Add"
+                    onClick={e => onOpenSelector(e)}
+                    className={isAddStepButtonLeft() ? "add-button 
add-button-left" : "add-button add-button-bottom"}>
+                <AddElementIcon/>
+            </button>
         )
     }
 
diff --git 
a/karavan-app/src/main/webui/src/designer/route/element/DslElementHeader.tsx 
b/karavan-app/src/main/webui/src/designer/route/element/DslElementHeader.tsx
index 612b6932..aae10161 100644
--- a/karavan-app/src/main/webui/src/designer/route/element/DslElementHeader.tsx
+++ b/karavan-app/src/main/webui/src/designer/route/element/DslElementHeader.tsx
@@ -80,7 +80,7 @@ export function DslElementHeader(props: Props) {
     }
 
     function isWide(): boolean {
-        return ['RouteConfigurationDefinition', 'RouteDefinition', 
'ChoiceDefinition', 'MulticastDefinition',
+        return ['RouteConfigurationDefinition', 'RouteTemplateDefinition', 
'RouteDefinition', 'ChoiceDefinition', 'MulticastDefinition',
             'LoadBalanceDefinition', 'TryDefinition', 
'CircuitBreakerDefinition']
             .includes(props.step.dslName);
     }
@@ -163,6 +163,10 @@ export function DslElementHeader(props: Props) {
             classes.push('header-route')
             classes.push('header-bottom-line')
             classes.push(isElementSelected() ? 'header-bottom-selected' : 
'header-bottom-not-selected')
+        } else if (step.dslName === 'RouteTemplateDefinition') {
+            classes.push('header-route')
+            classes.push('header-bottom-line')
+            classes.push(isElementSelected() ? 'header-bottom-selected' : 
'header-bottom-not-selected')
         } else if (step.dslName === 'RouteConfigurationDefinition') {
             classes.push('header-route')
             if (hasElements(step)) {
@@ -181,39 +185,43 @@ export function DslElementHeader(props: Props) {
         const step: CamelElement = props.step;
         const parent = props.parent;
         const inRouteConfiguration = parent !== undefined && parent.dslName 
=== 'RouteConfigurationDefinition';
-        const showAddButton = !['CatchDefinition', 
'RouteDefinition'].includes(step.dslName) && availableModels.length > 0;
+        const showAddButton = !['CatchDefinition', 'RouteTemplateDefinition', 
'RouteDefinition'].includes(step.dslName) && availableModels.length > 0;
         const showInsertButton =
-            !['FromDefinition', 'RouteConfigurationDefinition', 
'RouteDefinition', 'CatchDefinition', 'FinallyDefinition', 'WhenDefinition', 
'OtherwiseDefinition'].includes(step.dslName)
+            !['FromDefinition', 'RouteConfigurationDefinition', 
'RouteTemplateDefinition', 'RouteDefinition', 'CatchDefinition', 
'FinallyDefinition', 'WhenDefinition', 
'OtherwiseDefinition'].includes(step.dslName)
             && !inRouteConfiguration;
+        const showDeleteButton = !('RouteDefinition' === step.dslName && 
'RouteTemplateDefinition' === parent?.dslName);
         const headerClasses = getHeaderClasses();
         const childrenInfo = getChildrenInfo(props.step) || [];
         const hasWideChildrenElement = getHasWideChildrenElement(childrenInfo)
         return (
             <div className={"dsl-element " + headerClasses} 
style={getHeaderStyle()} ref={props.headerRef}>
-                {!['RouteConfigurationDefinition', 
'RouteDefinition'].includes(props.step.dslName) &&
+                {!['RouteConfigurationDefinition', 'RouteTemplateDefinition', 
'RouteDefinition'].includes(props.step.dslName) &&
                     <div
                         className={getHeaderIconClasses()}
                         style={isWide() ? {width: ""} : {}}>
                         {CamelUi.getIconForElement(step)}
                     </div>
                 }
-                {'RouteDefinition' === step.dslName&&
+                {'RouteDefinition' === step.dslName &&
                     <div className={"route-icons"}>
                         {(step as any).autoStartup !== false && 
<AutoStartupIcon/>}
                         {(step as any).errorHandler !== undefined && 
<ErrorHandlerIcon/>}
                     </div>
                 }
-                {'RouteConfigurationDefinition' === step.dslName&&
+                {'RouteConfigurationDefinition' === step.dslName &&
                     <div className={"route-icons"}>
                         {(step as any).errorHandler !== undefined && 
<ErrorHandlerIcon/>}
                     </div>
                 }
+                {'RouteTemplateDefinition' === step.dslName &&
+                    <div style={{height: '10px'}}></div>
+                }
                 <div className={hasWideChildrenElement ? "header-text" : ""}>
                     {hasWideChildrenElement && <div className="spacer"/>}
                     {getHeaderTextWithTooltip(step, hasWideChildrenElement)}
                 </div>
                 {showInsertButton && getInsertElementButton()}
-                {getDeleteButton()}
+                {showDeleteButton && getDeleteButton()}
                 {showAddButton && getAddElementButton()}
             </div>
         )
diff --git 
a/karavan-app/src/main/webui/src/designer/route/element/RouteTemplateElement.css
 
b/karavan-app/src/main/webui/src/designer/route/element/RouteTemplateElement.css
new file mode 100644
index 00000000..a31b7c51
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/designer/route/element/RouteTemplateElement.css
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+.karavan .dsl-page .flows .route-template-element {
+    align-items: center;
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    width: fit-content;
+    border-color: var(--pf-v5-global--Color--200);
+    border-radius: 42px;
+    border-width: 1px;
+    min-width: 120px;
+    padding: 3px 4px 4px 4px;
+    margin: 3px auto 0 auto;
+    position: relative;
+}
+.karavan .dsl-page .flows .route-template-element:hover .add-element-button,
+.karavan .dsl-page .flows .route-template-element:hover .add-button {
+    visibility: visible;
+}
+
+.karavan .dsl-page .flows .route-template-element .header .delete-button {
+    position: absolute;
+    top: -11px;
+    line-height: 1;
+    border: 0;
+    padding: 0;
+    margin: 0 0 0 10px;
+    background: transparent;
+    color: #909090;
+    visibility: hidden;
+}
+
+.karavan .dsl-page .flows .route-template-element .header:hover .delete-button,
+.karavan .dsl-page .flows .route-template-element .header-route:hover 
.delete-button {
+    visibility: visible;
+}
+
+.karavan .dsl-page .flows .route-template-element .delete-button {
+    position: absolute;
+    top: 5px;
+    right: 5px;
+    line-height: 1;
+    border: 0;
+    padding: 0;
+    margin: 0;
+    background: transparent;
+    color: #909090;
+    visibility: hidden;
+}
diff --git 
a/karavan-app/src/main/webui/src/designer/route/element/RouteTemplateElement.tsx
 
b/karavan-app/src/main/webui/src/designer/route/element/RouteTemplateElement.tsx
new file mode 100644
index 00000000..1aeace1e
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/designer/route/element/RouteTemplateElement.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 './RouteTemplateElement.css';
+import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition";
+import {ChildElement, CamelDefinitionApiExt} from 
"karavan-core/lib/api/CamelDefinitionApiExt";
+import {useDesignerStore} from "../../DesignerStore";
+import {shallow} from "zustand/shallow";
+import {useRouteDesignerHook} from "../useRouteDesignerHook";
+import {DslElementHeader} from "./DslElementHeader";
+import {DslElement} from "./DslElement";
+import {RouteDefinition, RouteTemplateDefinition} from 
"karavan-core/lib/model/CamelDefinition";
+
+interface Props {
+    step: CamelElement,
+    parent: CamelElement | undefined,
+    nextStep: CamelElement | undefined,
+    prevStep: CamelElement | undefined,
+    inSteps: boolean
+    position: number
+    inStepsLength: number
+}
+
+export function RouteTemplateElement(props: Props) {
+
+    const headerRef = React.useRef<HTMLDivElement>(null);
+    const {
+        selectElement,
+        moveElement,
+        onShowDeleteConfirmation,
+        openSelector,
+        isKamelet,
+        isSourceKamelet,
+        isActionKamelet
+    } = useRouteDesignerHook();
+
+    const [selectedUuids, selectedStep, showMoveConfirmation, 
setShowMoveConfirmation, setMoveElements] =
+        useDesignerStore((s) =>
+            [s.selectedUuids, s.selectedStep, s.showMoveConfirmation, 
s.setShowMoveConfirmation, s.setMoveElements], shallow)
+
+
+    function onSelectElement(evt: React.MouseEvent) {
+        evt.stopPropagation();
+        selectElement(props.step);
+    }
+
+    function isElementSelected(): boolean {
+        return selectedUuids.includes(props.step.uuid);
+    }
+
+    function isInStepWithChildren() {
+        const step: CamelElement = props.step;
+        const children = 
CamelDefinitionApiExt.getElementChildrenDefinition(step.dslName);
+        return children.filter((c: ChildElement) => c.name === 'steps' || 
c.multiple).length > 0 && props.inSteps;
+    }
+
+    const element: RouteTemplateDefinition = (props.step as 
RouteTemplateDefinition);
+    const route: RouteDefinition | undefined = element.route;
+    const className = "route-template-element";
+    return (
+        <div key={"root" + element.uuid}
+             className={className}
+             style={{
+                 borderStyle: "dashed",
+                 borderColor: isElementSelected() ? 
"var(--step-border-color-selected)" : "var(--step-border-color)",
+                 marginTop: isInStepWithChildren() ? "16px" : "8px",
+                 zIndex: 10,
+             }}
+             onMouseOver={event => event.stopPropagation()}
+             onClick={event => onSelectElement(event)}
+             draggable={false}
+        >
+            <DslElementHeader headerRef={headerRef}
+                              step={element}
+                              parent={props.parent}
+                              nextStep={props.nextStep}
+                              prevStep={props.prevStep}
+                              inSteps={props.inSteps}
+                              isDragging={false}
+                              position={props.position}/>
+            {route && <DslElement key={route.uuid}
+                         inSteps={false}
+                         position={0}
+                         step={route}
+                         nextStep={undefined}
+                         prevStep={undefined}
+                         inStepsLength={0}
+                         parent={element}/>}
+        </div>
+    )
+}
diff --git 
a/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx 
b/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx
index 5ed8199b..62cda4bb 100644
--- a/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx
+++ b/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx
@@ -45,8 +45,9 @@ export function useRouteDesignerHook() {
         [s.selectedUuids, s.clipboardSteps, s.shiftKeyPressed,
             s.setShowDeleteConfirmation, s.setDeleteMessage, s.selectedStep, 
s.setSelectedStep, s.setSelectedUuids, s.setClipboardSteps, 
s.setShiftKeyPressed,
             s.width, s.height, s.dark], shallow)
-    const [setParentId, setShowSelector, setSelectorTabIndex, setParentDsl, 
setShowSteps, setSelectedPosition, routeId, setRouteId] = useSelectorStore((s) 
=>
-        [s.setParentId, s.setShowSelector, s.setSelectorTabIndex, 
s.setParentDsl, s.setShowSteps, s.setSelectedPosition, s.routeId, 
s.setRouteId], shallow)
+    const [setParentId, setShowSelector, setSelectorTabIndex, setParentDsl, 
setShowSteps, setSelectedPosition, routeId, setRouteId, isRouteTemplate, 
setIsRouteTemplate] = useSelectorStore((s) =>
+        [s.setParentId, s.setShowSelector, s.setSelectorTabIndex, 
s.setParentDsl, s.setShowSteps, s.setSelectedPosition, s.routeId, s.setRouteId,
+        s.isRouteTemplate, s.setIsRouteTemplate], shallow)
 
     function onCommand(command: Command, printerRef: 
React.MutableRefObject<HTMLDivElement | null>) {
         switch (command.command) {
@@ -102,6 +103,8 @@ export function useRouteDesignerHook() {
             message = 'Deleting the first element will delete the entire 
route!';
         } else if (ce.dslName === 'RouteDefinition') {
             message = 'Delete route?';
+        } else if (ce.dslName === 'RouteTemplateDefinition') {
+            message = 'Delete route template?';
         } else if (ce.dslName === 'RouteConfigurationDefinition') {
             message = 'Delete route configuration?';
         } else {
@@ -222,12 +225,13 @@ export function useRouteDesignerHook() {
         }
     }
 
-    const openSelector = (parentId: string | undefined, parentDsl: string | 
undefined, showSteps: boolean = true, position?: number | undefined) => {
+    const openSelector = (parentId: string | undefined, parentDsl: string | 
undefined, showSteps: boolean = true, position?: number | undefined, 
routeTemplate?: boolean) => {
         setShowSelector(true);
         setParentId(parentId || '');
         setParentDsl(parentDsl);
         setShowSteps(showSteps);
         setSelectedPosition(position);
+        setIsRouteTemplate(routeTemplate === true);
         setSelectorTabIndex((parentId === undefined && parentDsl === 
undefined) ? 'components' : 'eip');
     }
 
@@ -244,7 +248,9 @@ export function useRouteDesignerHook() {
     function onDslSelect(dsl: DslMetaModel, parentId: string, position?: 
number | undefined) {
         switch (dsl.dsl) {
             case 'FromDefinition' :
-                if (routeId !== undefined) {
+                if (isRouteTemplate) {
+                    createRouteTemplate(dsl)
+                } else if (routeId !== undefined) {
                     replaceFrom(dsl)
                 } else {
                     const nodePrefixId = isKamelet() ? 
integration.metadata.name : 'route-' + uuidv4().substring(0, 3);
@@ -315,6 +321,19 @@ export function useRouteDesignerHook() {
         setSelectedUuids([routeConfiguration.uuid]);
     }
 
+    const createRouteTemplate = (dsl: DslMetaModel) => {
+        const clone = CamelUtil.cloneIntegration(integration);
+        const route = CamelDefinitionApi.createRouteDefinition({
+            from: new FromDefinition({uri: dsl.uri}),
+            nodePrefixId: 'route-' + uuidv4().substring(0, 3)
+        });
+        const routeTemplate = 
CamelDefinitionApi.createRouteTemplateDefinition({route: route});
+        const i = CamelDefinitionApiExt.addRouteTemplateToIntegration(clone, 
routeTemplate);
+        setIntegration(i, false);
+        setSelectedStep(routeTemplate);
+        setSelectedUuids([routeTemplate.uuid]);
+    }
+
     const addStep = (step: CamelElement, parentId: string, position?: number | 
undefined) => {
         const clone = CamelUtil.cloneIntegration(integration);
         const i = CamelDefinitionApiExt.addStepToIntegration(clone, step, 
parentId, position);
@@ -378,6 +397,6 @@ export function useRouteDesignerHook() {
     return {
         deleteElement, selectElement, moveElement, onShowDeleteConfirmation, 
onDslSelect, openSelector,
         createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, 
unselectElement, isKamelet, isSourceKamelet,
-        isActionKamelet, isSinkKamelet, openSelectorToReplaceFrom
+        isActionKamelet, isSinkKamelet, openSelectorToReplaceFrom, 
createRouteTemplate
     }
 }
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx 
b/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
index 646f5ed9..f89c8e85 100644
--- a/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
+++ b/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
@@ -25,6 +25,7 @@ import {
     BeanFactoryDefinition,
     RouteConfigurationDefinition,
     RouteDefinition,
+    RouteTemplateDefinition,
     ToDefinition
 } from "karavan-core/lib/model/CamelDefinition";
 import {CamelElement, Integration, IntegrationFile} from 
"karavan-core/lib/model/IntegrationDefinition";
@@ -859,4 +860,11 @@ export class CamelUi {
             .forEach((f: any) => result.push(f));
         return result;
     }
+
+    static getRouteTemplates = (integration: Integration): 
RouteTemplateDefinition[] | undefined => {
+        const result: RouteTemplateDefinition[] = [];
+        integration.spec.flows?.filter((e: any) => e.dslName === 
'RouteTemplateDefinition')
+            .forEach((f: any) => result.push(f));
+        return result;
+    }
 }
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/topology/CustomGroup.tsx 
b/karavan-app/src/main/webui/src/topology/CustomGroup.tsx
index 595250a8..1d57941a 100644
--- a/karavan-app/src/main/webui/src/topology/CustomGroup.tsx
+++ b/karavan-app/src/main/webui/src/topology/CustomGroup.tsx
@@ -18,11 +18,17 @@
 import * as React from 'react';
 
 import './topology.css';
-import {DefaultGroup, observer} from '@patternfly/react-topology';
+import {DefaultGroup, LabelPosition, observer} from 
'@patternfly/react-topology';
 
 const CustomGroup: React.FC<any> = observer(({ element, ...rest }) => {
     return (
-        <DefaultGroup element={element} className={"topology-group"} {...rest}>
+        <DefaultGroup
+            element={element}
+            className={"topology-group"}
+            showLabel={false}
+            showLabelOnHover={true}
+            {...rest}
+        >
         </DefaultGroup>
     )
 })
diff --git a/karavan-app/src/main/webui/src/topology/CustomNode.tsx 
b/karavan-app/src/main/webui/src/topology/CustomNode.tsx
index 2d0b63da..ae7d111c 100644
--- a/karavan-app/src/main/webui/src/topology/CustomNode.tsx
+++ b/karavan-app/src/main/webui/src/topology/CustomNode.tsx
@@ -25,6 +25,10 @@ import './topology.css';
 import {RouteDefinition} from "karavan-core/lib/model/CamelDefinition";
 import {AutoStartupIcon, ErrorHandlerIcon} from "../designer/icons/OtherIcons";
 
+export const COLOR_ORANGE = '#ef9234';
+export const COLOR_BLUE = '#2b9af3';
+export const COLOR_GREEN = '#6ec664';
+
 function getIcon(data: any) {
     if (['route', 'rest', 'routeConfiguration'].includes(data.icon)) {
         return (
@@ -67,7 +71,13 @@ function getAttachments(data: any) {
 const CustomNode: React.FC<any> = observer(({element, ...rest}) => {
 
     const data = element.getData();
-    const badge: string = data.badge === 'REST' ? data.badge : 
data.badge?.substring(0, 1).toUpperCase();
+    const badge: string = ['API', 'RT'].includes(data.badge) ? data.badge : 
data.badge?.substring(0, 1).toUpperCase();
+    let badgeColor = COLOR_ORANGE;
+    if (badge === 'C') {
+        badgeColor = COLOR_BLUE;
+    } else if (badge === 'K') {
+        badgeColor = COLOR_GREEN;
+    }
     if (element.getLabel()?.length > 30) {
         element.setLabel(element.getLabel()?.substring(0, 30) + '...');
     }
@@ -75,8 +85,10 @@ const CustomNode: React.FC<any> = observer(({element, 
...rest}) => {
     return (
         <DefaultNode
             badge={badge}
+            badgeColor={badgeColor}
+            badgeBorderColor={badgeColor}
             showStatusDecorator
-            className="common-node"
+            className={"common-node common-node-" + badge}
             scaleLabel={false}
             element={element}
             attachments={getAttachments(data)}
diff --git a/karavan-app/src/main/webui/src/topology/TopologyApi.tsx 
b/karavan-app/src/main/webui/src/topology/TopologyApi.tsx
index 3416f83b..275f1c73 100644
--- a/karavan-app/src/main/webui/src/topology/TopologyApi.tsx
+++ b/karavan-app/src/main/webui/src/topology/TopologyApi.tsx
@@ -80,11 +80,14 @@ export function getRoutes(tins: TopologyRouteNode[]): 
NodeModel[] {
             status: NodeStatus.default,
             data: {
                 isAlternate: false,
+                badge: tin.templateId !== undefined ? 'RT' : 'R',
                 type: 'route',
                 icon: 'route',
                 step: tin.route,
                 routeId: tin.routeId,
                 fileName: tin.fileName,
+                templateId: tin.templateId,
+                templateTitle: tin.templateTitle,
             }
         }
         return node;
@@ -198,7 +201,7 @@ export function getRestNodes(tins: TopologyRestNode[]): 
NodeModel[] {
             status: NodeStatus.default,
             data: {
                 isAlternate: false,
-                badge: 'REST',
+                badge: 'API',
                 icon: 'rest',
                 type: 'rest',
                 step: tin.rest,
diff --git a/karavan-app/src/main/webui/src/topology/TopologyLegend.tsx 
b/karavan-app/src/main/webui/src/topology/TopologyLegend.tsx
new file mode 100644
index 00000000..0a915988
--- /dev/null
+++ b/karavan-app/src/main/webui/src/topology/TopologyLegend.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 * as React from 'react';
+import './topology.css';
+import {
+    Badge,
+    Card,
+    CardBody,
+    CardTitle,
+    Label,
+} from "@patternfly/react-core";
+import {COLOR_BLUE, COLOR_GREEN, COLOR_ORANGE} from "./CustomNode";
+
+export function TopologyLegend () {
+
+
+    return (
+        <Card isCompact isFlat isRounded className="topology-legend-card">
+            <CardTitle>Legend</CardTitle>
+            <CardBody className='card-body'>
+                <Label icon={<Badge style={{backgroundColor: COLOR_ORANGE, 
padding: 0}}>API</Badge>}>REST API</Label>
+                <Label icon={<Badge style={{backgroundColor: COLOR_ORANGE, 
padding: 0}}>R</Badge>}>Route</Label>
+                <Label icon={<Badge style={{backgroundColor: COLOR_ORANGE, 
padding: 0}}>RT</Badge>}>Route Template</Label>
+                <Label icon={<Badge style={{backgroundColor: COLOR_BLUE, 
padding: 0}}>C</Badge>}>Component</Label>
+                <Label icon={<Badge style={{backgroundColor: COLOR_GREEN, 
padding: 0}}>K</Badge>}>Kamelet</Label>
+            </CardBody>
+        </Card>
+    )
+}
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/topology/TopologyTab.tsx 
b/karavan-app/src/main/webui/src/topology/TopologyTab.tsx
index d8408a99..723d23e8 100644
--- a/karavan-app/src/main/webui/src/topology/TopologyTab.tsx
+++ b/karavan-app/src/main/webui/src/topology/TopologyTab.tsx
@@ -36,6 +36,7 @@ import {TopologyPropertiesPanel} from 
"./TopologyPropertiesPanel";
 import {TopologyToolbar} from "./TopologyToolbar";
 import {useDesignerStore} from "../designer/DesignerStore";
 import {IntegrationFile} from "karavan-core/lib/model/IntegrationDefinition";
+import {TopologyLegend} from "./TopologyLegend";
 
 interface Props {
     files: IntegrationFile[],
@@ -160,6 +161,7 @@ export function TopologyTab(props: Props) {
         >
             <VisualizationProvider controller={controller}>
                 <VisualizationSurface state={{selectedIds}}/>
+                <TopologyLegend/>
             </VisualizationProvider>
         </TopologyView>
     );
diff --git a/karavan-app/src/main/webui/src/topology/topology.css 
b/karavan-app/src/main/webui/src/topology/topology.css
index 6edc633d..7279a162 100644
--- a/karavan-app/src/main/webui/src/topology/topology.css
+++ b/karavan-app/src/main/webui/src/topology/topology.css
@@ -61,6 +61,10 @@
     width: 32px;
 }
 
+.karavan .topology-panel .common-node-R {
+
+}
+
 .karavan .topology-panel .pf-v5-c-panel__header {
     padding-bottom: 0;
 }
@@ -117,6 +121,48 @@
     fill: var(--pf-topology__node__label__text--Fill);
 }
 
-.karavan .topology-group .pf-topology__group__label {
-    display: none;
+.karavan .pf-topology__node__label__badge > text {
+    fill: white;
+}
+
+.karavan .topology-panel .common-node-RT .pf-topology__node__background {
+    outline: 1px dashed grey;
+    outline-offset: 2px;
+    border-radius: 9px;
+}
+
+.karavan .topology-panel .topology-legend-card {
+    position: absolute;
+    bottom: 6px;
+    right: 6px;
+}
+
+.karavan .topology-panel .topology-legend-card .pf-v5-c-card__title {
+    padding: 5px;
+    display: flex;
+    justify-content: center;
+}
+
+.karavan .topology-panel .topology-legend-card .card-body {
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+    padding: 0px 10px 10px 10px;
+}
+
+.karavan .topology-panel .topology-legend-card .pf-v5-c-label {
+    background-color: white;
+}
+
+.karavan .topology-panel .topology-legend-card .pf-v5-c-label__content::before 
{
+    border-radius: 4px;
+}
+
+.karavan .topology-panel .topology-legend-card .pf-v5-c-badge {
+    min-width: 32px;
+    font-weight: normal;
+}
+
+ .pf-topology__group__label .pf-topology__node__label__background {
+    fill: grey;
 }
\ No newline at end of file


Reply via email to