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 <[email protected]>
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