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