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 a983bd1b Fix #1449 a983bd1b is described below commit a983bd1b8a2c0ff64ed370d649a291e20ad16c78 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Mon Oct 28 16:32:46 2024 -0400 Fix #1449 --- .../src/main/webui/src/designer/icons/EipIcons.tsx | 4 +- .../webui/src/designer/property/DslProperties.css | 10 +- .../src/designer/property/PropertiesHeader.tsx | 259 ++++++++++++--------- .../src/designer/property/usePropertiesHook.tsx | 1 + .../webui/src/designer/route/DslConnections.css | 9 + .../webui/src/designer/route/DslConnections.tsx | 34 +-- .../src/designer/route/element/DslElement.css | 4 + .../src/main/webui/src/designer/utils/CamelUi.tsx | 4 +- karavan-core/src/core/api/CamelDisplayUtil.ts | 4 +- karavan-core/src/core/api/CamelUtil.ts | 23 +- karavan-designer/src/designer/icons/EipIcons.tsx | 4 +- .../src/designer/property/DslProperties.css | 10 +- .../src/designer/property/PropertiesHeader.tsx | 259 ++++++++++++--------- .../src/designer/property/usePropertiesHook.tsx | 1 + .../src/designer/route/DslConnections.css | 9 + .../src/designer/route/DslConnections.tsx | 34 +-- .../src/designer/route/element/DslElement.css | 4 + karavan-designer/src/designer/utils/CamelUi.tsx | 4 +- karavan-space/src/designer/icons/EipIcons.tsx | 4 +- .../src/designer/property/DslProperties.css | 10 +- .../src/designer/property/PropertiesHeader.tsx | 259 ++++++++++++--------- .../src/designer/property/usePropertiesHook.tsx | 1 + .../src/designer/route/DslConnections.css | 9 + .../src/designer/route/DslConnections.tsx | 34 +-- .../src/designer/route/element/DslElement.css | 4 + karavan-space/src/designer/utils/CamelUi.tsx | 4 +- 26 files changed, 608 insertions(+), 394 deletions(-) diff --git a/karavan-app/src/main/webui/src/designer/icons/EipIcons.tsx b/karavan-app/src/main/webui/src/designer/icons/EipIcons.tsx index fc928d9f..4bdac684 100644 --- a/karavan-app/src/main/webui/src/designer/icons/EipIcons.tsx +++ b/karavan-app/src/main/webui/src/designer/icons/EipIcons.tsx @@ -50,14 +50,14 @@ export function AggregateIcon() { ); } -export function ToIcon() { +export function ToIcon(classname: string = '') { return ( <svg xmlns="http://www.w3.org/2000/svg" width={800} height={800} viewBox="0 0 32 32" - className="icon" + className={classname ? "icon " + classname : "icon"} > <path d="m12.103 11.923 2.58 2.59H2.513v2h12.17l-2.58 2.59 1.41 1.41 5-5-5-5z"/> <path diff --git a/karavan-app/src/main/webui/src/designer/property/DslProperties.css b/karavan-app/src/main/webui/src/designer/property/DslProperties.css index 97b97a38..e2401cf3 100644 --- a/karavan-app/src/main/webui/src/designer/property/DslProperties.css +++ b/karavan-app/src/main/webui/src/designer/property/DslProperties.css @@ -24,14 +24,22 @@ width: 100%; display: flex; flex-direction: row; + /*justify-content: space-between;*/ + gap: 10px; } .karavan .properties .headers .top h1 { - width: 100%; + /*width: 100%;*/ margin-top: auto; margin-bottom: auto; + text-wrap: nowrap; +} + +.karavan .properties .headers .top .pf-v5-c-switch { + column-gap: 6px; } + .karavan .properties .footer { height: 100%; display: contents; diff --git a/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx b/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx index b8aca6b4..00a3e1ee 100644 --- a/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx +++ b/karavan-app/src/main/webui/src/designer/property/PropertiesHeader.tsx @@ -25,13 +25,13 @@ import { MenuToggle, DropdownList, DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, - Switch, + Switch, Radio, Tooltip, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; import "@patternfly/patternfly/patternfly.css"; import {CamelUi} from "../utils/CamelUi"; -import {useDesignerStore, useSelectorStore} from "../DesignerStore"; +import {useDesignerStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; @@ -56,7 +56,13 @@ export function PropertiesHeader(props: Props) { const [isHeadersExpanded, setIsHeadersExpanded] = useState<boolean>(false); const [isExchangePropertiesExpanded, setIsExchangePropertiesExpanded] = useState<boolean>(false); const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - const [isStepTypeOpen, setIsStepTypeOpen] = React.useState(false); + const [stepIsPoll, setStepIsPoll] = React.useState(false); + const [stepDynamic, setStepDynamic] = React.useState(false); + + useEffect(() => { + setStepDynamic(selectedStep?.dslName === 'ToDynamicDefinition') + setStepIsPoll(selectedStep?.dslName === 'PollDefinition') + }, []) useEffect(() => { setMenuOpen(false) @@ -68,67 +74,70 @@ export function PropertiesHeader(props: Props) { const targetDslTitle = targetDsl?.replace("Definition", ""); const showMenu = hasSteps || targetDsl !== undefined; return showMenu ? - <Dropdown - popperProps={{position: "end"}} - isOpen={isMenuOpen} - onSelect={() => { - }} - onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} - toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( - <MenuToggle - className="header-menu-toggle" - ref={toggleRef} - aria-label="menu" - variant="plain" - onClick={() => setMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - > - <EllipsisVIcon/> - </MenuToggle> - )} - > - <DropdownList> - {isFrom && - <DropdownItem key="changeFrom" onClick={(ev) => { - ev.preventDefault() - openSelectorToReplaceFrom((selectedStep as any).id) - setMenuOpen(false); - }}> - Change From... - </DropdownItem>} - {hasSteps && - <DropdownItem key="saveStepsRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, true); - setMenuOpen(false); - } - }}> - Save Steps to Route - </DropdownItem>} - {hasSteps && !isFrom && - <DropdownItem key="saveElementRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, false); + <div style={{display: 'flex', flexDirection: 'row', justifyContent: 'end', width: '100%'}}> + <Dropdown + popperProps={{position: "end"}} + isOpen={isMenuOpen} + onSelect={() => { + }} + onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle + className="header-menu-toggle" + ref={toggleRef} + aria-label="menu" + variant="plain" + onClick={() => setMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + > + <EllipsisVIcon/> + </MenuToggle> + )} + > + <DropdownList> + {isFrom && + <DropdownItem key="changeFrom" onClick={(ev) => { + ev.preventDefault() + openSelectorToReplaceFrom((selectedStep as any).id) setMenuOpen(false); - } - }}> - Save Element to Route - </DropdownItem>} - {targetDsl && - <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} - </DropdownList> - </Dropdown> : <></>; + }}> + Change From... + </DropdownItem>} + {hasSteps && + <DropdownItem key="saveStepsRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, true); + setMenuOpen(false); + } + }}> + Save Steps to Route + </DropdownItem>} + {hasSteps && !isFrom && + <DropdownItem key="saveElementRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, false); + setMenuOpen(false); + } + }}> + Save Element to Route + </DropdownItem>} + {targetDsl && + <DropdownItem key="convert" + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} + </DropdownList> + </Dropdown> + </div> + : <></>; } function getExchangePropertiesSection(): React.JSX.Element { @@ -138,35 +147,35 @@ export function PropertiesHeader(props: Props) { isExpanded={isExchangePropertiesExpanded}> <Flex className='component-headers' direction={{default: "column"}}> {exchangeProperties.map((header, index, array) => - <Flex key={index}> - <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" - isCode> - {header.name} - </ClipboardCopy> - <FlexItem align={{default: 'alignRight'}}> - <Popover - position={"left"} - headerContent={header.name} - bodyContent={header.description} - footerContent={ - <Flex> - <Text component={TextVariants.p}>{header.javaType}</Text> - <FlexItem align={{default: 'alignRight'}}> - <Badge isRead>{header.label}</Badge> - </FlexItem> - </Flex> - } - > - <button type="button" aria-label="More info" onClick={e => { - e.preventDefault(); - e.stopPropagation(); - }} className="pf-v5-c-form__group-label-help"> - <HelpIcon/> - </button> - </Popover> - </FlexItem> - </Flex> - )} + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <FlexItem align={{default: 'alignRight'}}> + <Popover + position={"left"} + headerContent={header.name} + bodyContent={header.description} + footerContent={ + <Flex> + <Text component={TextVariants.p}>{header.javaType}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Badge isRead>{header.label}</Badge> + </FlexItem> + </Flex> + } + > + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + </FlexItem> + </Flex> + )} </Flex> </ExpandableSection> ) @@ -235,24 +244,58 @@ export function PropertiesHeader(props: Props) { const component = ComponentApi.findStepComponent(selectedStep); const groups = (isFrom || isPoll) ? ['consumer', 'common'] : ['producer', 'common']; const isKamelet = CamelUi.isKamelet(selectedStep); - const isStepComponent = !isFrom && selectedStep !== undefined && !isKamelet && ['ToDefinition', 'PollDefinition'].includes(selectedStep?.dslName); + const isStepComponent = !isFrom && selectedStep !== undefined && !isKamelet && ['ToDefinition', 'PollDefinition', 'ToDynamicDefinition'].includes(selectedStep?.dslName); + + function changeStepType(poll: boolean, dynamic: boolean) { + if (selectedStep) { + if (poll) { + convertStep(selectedStep, 'PollDefinition'); + setStepIsPoll(true); + setStepDynamic(false); + } else if (dynamic) { + convertStep(selectedStep, 'ToDynamicDefinition'); + setStepIsPoll(false); + setStepDynamic(true); + } else { + convertStep(selectedStep, 'ToDefinition'); + setStepIsPoll(false); + setStepDynamic(false); + } + } + } function getComponentStepTypeSwitch() { - return (component?.component.producerOnly - ? <></> - : <Switch - id="step-type-switch" - label="Poll" - isChecked={isStepTypeOpen} - onChange={(event, checked) => { - if (selectedStep) { - convertStep(selectedStep, checked ? 'PollDefinition' : 'ToDefinition'); - setIsStepTypeOpen(checked); - } - }} - ouiaId="step-type-switch" - isReversed - /> + const pollSupported = !component?.component.producerOnly; + return (<div style={{display: 'flex', flexDirection: 'row', justifyContent: 'end', width: '100%', gap: '10px'}}> + <Tooltip content='Send messages to a dynamic endpoint evaluated on-demand' position='top-end'> + <Switch + id="step-type-dynamic" + label="Dynamic" + isChecked={stepDynamic} + onChange={(event, checked) => { + changeStepType(stepIsPoll, checked) + }} + ouiaId="step-type-switch" + isDisabled={stepIsPoll} + isReversed + /> + </Tooltip> + {pollSupported && + <Tooltip content='Simple Polling Consumer to obtain the additional data' position='top-end'> + <Switch + id="step-type-poll" + label="Poll" + isChecked={stepIsPoll} + onChange={(event, checked) => { + changeStepType(checked, stepDynamic) + }} + ouiaId="step-type-switch" + isDisabled={stepDynamic} + isReversed + /> + </Tooltip> + } + </div> ) } diff --git a/karavan-app/src/main/webui/src/designer/property/usePropertiesHook.tsx b/karavan-app/src/main/webui/src/designer/property/usePropertiesHook.tsx index 2d806c0d..5c8e38b4 100644 --- a/karavan-app/src/main/webui/src/designer/property/usePropertiesHook.tsx +++ b/karavan-app/src/main/webui/src/designer/property/usePropertiesHook.tsx @@ -177,6 +177,7 @@ export function usePropertiesHook(designerType: 'routes' | 'rest' | 'beans' = 'r } const convertStep = (step: CamelElement, targetDslName: string) => { + console.log(targetDslName) try { // setSelectedStep(undefined); if (targetDslName === 'ChoiceDefinition' && step.dslName === 'FilterDefinition') { diff --git a/karavan-app/src/main/webui/src/designer/route/DslConnections.css b/karavan-app/src/main/webui/src/designer/route/DslConnections.css index 03da5f2a..a75582ea 100644 --- a/karavan-app/src/main/webui/src/designer/route/DslConnections.css +++ b/karavan-app/src/main/webui/src/designer/route/DslConnections.css @@ -54,6 +54,15 @@ fill: transparent; } +.karavan .dsl-page .graph .connections .path-dynamic { + stroke-dasharray: 5; + -webkit-animation: dashdrawR 0.5s linear infinite; + animation: dashdrawR 0.5s linear infinite; + stroke: var(--pf-v5-global--Color--200); + stroke-width: 1; + fill: transparent; +} + .karavan .dsl-page .graph .connections .path-poll { stroke-dasharray: 5; -webkit-animation: dashdrawL 0.5s linear infinite; diff --git a/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx b/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx index 99fd0934..be404896 100644 --- a/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx +++ b/karavan-app/src/main/webui/src/designer/route/DslConnections.tsx @@ -33,6 +33,7 @@ import {INTERNAL_COMPONENTS} from "karavan-core/lib/api/ComponentApi"; const overlapGap: number = 40; const DIAMETER: number = 34; const RADIUS: number = DIAMETER / 2; +type ConnectionType = 'internal' | 'remote' | 'nav' | 'poll' | 'dynamic'; export function DslConnections() { @@ -83,10 +84,12 @@ export function DslConnections() { } } - function getElementType(element: CamelElement): 'internal' | 'remote' | 'nav' | 'poll' { + function getElementType(element: CamelElement): ConnectionType { const uri = (element as any).uri; if (element.dslName === 'PollDefinition') { return 'poll'; + } else if (element.dslName === 'ToDynamicDefinition') { + return 'dynamic'; } else if (INTERNAL_COMPONENTS.includes((uri))) { return 'nav'; } else { @@ -95,8 +98,8 @@ export function DslConnections() { } } - function getIncomings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { - let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values()) + function getIncomings(): [string, number, ConnectionType][] { + let outs: [string, number, ConnectionType][] = Array.from(steps.values()) .filter(pos => ["FromDefinition"].includes(pos.step.dslName)) .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source')) .sort((pos1: DslPosition, pos2: DslPosition) => { @@ -111,7 +114,7 @@ export function DslConnections() { return outs; } - function getIncoming(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getIncoming(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; @@ -144,7 +147,7 @@ export function DslConnections() { // .filter(s => (s.step as any)?.parameters?.name === name) // } - function getIncomingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getIncomingIcons(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const step = (pos.step as any); @@ -186,7 +189,7 @@ export function DslConnections() { } } - function hasOverlap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): boolean { + function hasOverlap(data: [string, number, ConnectionType][]): boolean { let result = false; data.forEach((d, i, arr) => { if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result = true; @@ -194,8 +197,8 @@ export function DslConnections() { return result; } - function addGap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { - const result: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = []; + function addGap(data: [string, number, ConnectionType][]): [string, number, ConnectionType][] { + const result: [string, number, ConnectionType][] = []; data.forEach((d, i, arr) => { if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result.push([d[0], d[1] + overlapGap, d[2]]) else result.push(d); @@ -204,12 +207,12 @@ export function DslConnections() { } - function getOutgoings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { + function getOutgoings(): [string, number, ConnectionType][] { const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions(); - let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values()) + let outs: [string, number, ConnectionType][] = Array.from(steps.values()) .filter(pos => outgoingDefinitions.includes(pos.step.dslName)) .filter(pos => pos.step.dslName !== 'KameletDefinition' || (pos.step.dslName === 'KameletDefinition' && !CamelUi.isActionKamelet(pos.step))) - .filter(pos => ['ToDefinition', 'PollDefinition'].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step)) + .filter(pos => ['ToDefinition', 'PollDefinition', "ToDynamicDefinition"].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step)) .filter(pos => !CamelUi.isKameletSink(pos.step)) .sort((pos1: DslPosition, pos2: DslPosition) => { const y1 = pos1.headerRect.y + pos1.headerRect.height / 2; @@ -223,11 +226,12 @@ export function DslConnections() { return outs; } - function getOutgoing(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getOutgoing(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); const isInternal = data[2] === 'internal'; const isNav = data[2] === 'nav'; const isPoll = data[2] === 'poll'; + const isDynamic = data[2] === 'dynamic'; if (pos) { const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top; @@ -244,7 +248,9 @@ export function DslConnections() { const lineXi = lineX1 + 40; const lineYi = lineY2; - const className = isNav ? 'path-incoming-nav' : (isPoll ? 'path-poll' : 'path-incoming') + const className = isNav + ? 'path-incoming-nav' + : (isPoll ? 'path-poll' : (isDynamic ? 'path-dynamic' :'path-incoming')) return (!isInternal ? <g key={pos.step.uuid + "-outgoing"}> @@ -258,7 +264,7 @@ export function DslConnections() { } } - function getOutgoingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getOutgoingIcons(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const step = (pos.step as any); 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 785f667b..1b431837 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 @@ -128,6 +128,10 @@ border-radius: 33px; } +.karavan .step-element .header-icon-circle .dynamic{ + fill: var(--pf-v5-global--Color--400); +} + .karavan .step-element .header-icon-square { border-radius: 33px; } 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 045b154d..646f5ed9 100644 --- a/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx +++ b/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx @@ -728,7 +728,9 @@ export class CamelUi { case 'AggregateDefinition': return <AggregateIcon/>; case 'ToDefinition': - return <ToIcon/>; + return ToIcon(); + case 'ToDynamicDefinition': + return ToIcon('dynamic'); case 'PollDefinition': return <PollIcon/>; case 'ChoiceDefinition' : diff --git a/karavan-core/src/core/api/CamelDisplayUtil.ts b/karavan-core/src/core/api/CamelDisplayUtil.ts index d2078144..e103435b 100644 --- a/karavan-core/src/core/api/CamelDisplayUtil.ts +++ b/karavan-core/src/core/api/CamelDisplayUtil.ts @@ -33,7 +33,7 @@ export class CamelDisplayUtil { } else if (element.dslName === 'RouteDefinition') { const routeId = (element as RouteDefinition).id return routeId ? routeId : CamelUtil.capitalizeName((element as any).stepName); - } else if ((element as any).uri && (['ToDefinition', 'FromDefinition', 'PollDefinition'].includes(element.dslName))) { + } else if ((element as any).uri && (['ToDefinition', 'FromDefinition', 'PollDefinition', 'ToDynamicDefinition'].includes(element.dslName))) { const uri = (element as any).uri return ComponentApi.getComponentTitleFromUri(uri) || ''; } else { @@ -46,7 +46,7 @@ export class CamelDisplayUtil { const kamelet: KameletModel | undefined = CamelUtil.getKamelet(element); if (kamelet) { return kamelet.spec.definition.description; - } else if ((element as any).uri && (['ToDefinition', 'FromDefinition', 'PollDefinition'].includes(element.dslName))) { + } else if ((element as any).uri && (['ToDefinition', 'FromDefinition', 'PollDefinition', 'ToDynamicDefinition'].includes(element.dslName))) { const uri = (element as any).uri return ComponentApi.getComponentDescriptionFromUri(uri) || ''; } else { diff --git a/karavan-core/src/core/api/CamelUtil.ts b/karavan-core/src/core/api/CamelUtil.ts index 993bb306..5ee687a1 100644 --- a/karavan-core/src/core/api/CamelUtil.ts +++ b/karavan-core/src/core/api/CamelUtil.ts @@ -182,21 +182,16 @@ export class CamelUtil { const uri: string = (element as any).uri; const name = ComponentApi.getComponentNameFromUri(uri); - if (dslName === 'ToDynamicDefinition') { - const component = ComponentApi.findByName(dslName); - return component ? ComponentApi.getComponentProperties(component?.component.name, 'producer') : []; + if (name) { + const component = ComponentApi.findByName(name); + return component + ? ComponentApi.getComponentProperties( + component?.component.name, + element.dslName === 'FromDefinition' ? 'consumer' : 'producer', + ) + : []; } else { - if (name) { - const component = ComponentApi.findByName(name); - return component - ? ComponentApi.getComponentProperties( - component?.component.name, - element.dslName === 'FromDefinition' ? 'consumer' : 'producer', - ) - : []; - } else { - return []; - } + return []; } }; diff --git a/karavan-designer/src/designer/icons/EipIcons.tsx b/karavan-designer/src/designer/icons/EipIcons.tsx index fc928d9f..4bdac684 100644 --- a/karavan-designer/src/designer/icons/EipIcons.tsx +++ b/karavan-designer/src/designer/icons/EipIcons.tsx @@ -50,14 +50,14 @@ export function AggregateIcon() { ); } -export function ToIcon() { +export function ToIcon(classname: string = '') { return ( <svg xmlns="http://www.w3.org/2000/svg" width={800} height={800} viewBox="0 0 32 32" - className="icon" + className={classname ? "icon " + classname : "icon"} > <path d="m12.103 11.923 2.58 2.59H2.513v2h12.17l-2.58 2.59 1.41 1.41 5-5-5-5z"/> <path diff --git a/karavan-designer/src/designer/property/DslProperties.css b/karavan-designer/src/designer/property/DslProperties.css index 97b97a38..e2401cf3 100644 --- a/karavan-designer/src/designer/property/DslProperties.css +++ b/karavan-designer/src/designer/property/DslProperties.css @@ -24,14 +24,22 @@ width: 100%; display: flex; flex-direction: row; + /*justify-content: space-between;*/ + gap: 10px; } .karavan .properties .headers .top h1 { - width: 100%; + /*width: 100%;*/ margin-top: auto; margin-bottom: auto; + text-wrap: nowrap; +} + +.karavan .properties .headers .top .pf-v5-c-switch { + column-gap: 6px; } + .karavan .properties .footer { height: 100%; display: contents; diff --git a/karavan-designer/src/designer/property/PropertiesHeader.tsx b/karavan-designer/src/designer/property/PropertiesHeader.tsx index b8aca6b4..00a3e1ee 100644 --- a/karavan-designer/src/designer/property/PropertiesHeader.tsx +++ b/karavan-designer/src/designer/property/PropertiesHeader.tsx @@ -25,13 +25,13 @@ import { MenuToggle, DropdownList, DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, - Switch, + Switch, Radio, Tooltip, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; import "@patternfly/patternfly/patternfly.css"; import {CamelUi} from "../utils/CamelUi"; -import {useDesignerStore, useSelectorStore} from "../DesignerStore"; +import {useDesignerStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; @@ -56,7 +56,13 @@ export function PropertiesHeader(props: Props) { const [isHeadersExpanded, setIsHeadersExpanded] = useState<boolean>(false); const [isExchangePropertiesExpanded, setIsExchangePropertiesExpanded] = useState<boolean>(false); const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - const [isStepTypeOpen, setIsStepTypeOpen] = React.useState(false); + const [stepIsPoll, setStepIsPoll] = React.useState(false); + const [stepDynamic, setStepDynamic] = React.useState(false); + + useEffect(() => { + setStepDynamic(selectedStep?.dslName === 'ToDynamicDefinition') + setStepIsPoll(selectedStep?.dslName === 'PollDefinition') + }, []) useEffect(() => { setMenuOpen(false) @@ -68,67 +74,70 @@ export function PropertiesHeader(props: Props) { const targetDslTitle = targetDsl?.replace("Definition", ""); const showMenu = hasSteps || targetDsl !== undefined; return showMenu ? - <Dropdown - popperProps={{position: "end"}} - isOpen={isMenuOpen} - onSelect={() => { - }} - onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} - toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( - <MenuToggle - className="header-menu-toggle" - ref={toggleRef} - aria-label="menu" - variant="plain" - onClick={() => setMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - > - <EllipsisVIcon/> - </MenuToggle> - )} - > - <DropdownList> - {isFrom && - <DropdownItem key="changeFrom" onClick={(ev) => { - ev.preventDefault() - openSelectorToReplaceFrom((selectedStep as any).id) - setMenuOpen(false); - }}> - Change From... - </DropdownItem>} - {hasSteps && - <DropdownItem key="saveStepsRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, true); - setMenuOpen(false); - } - }}> - Save Steps to Route - </DropdownItem>} - {hasSteps && !isFrom && - <DropdownItem key="saveElementRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, false); + <div style={{display: 'flex', flexDirection: 'row', justifyContent: 'end', width: '100%'}}> + <Dropdown + popperProps={{position: "end"}} + isOpen={isMenuOpen} + onSelect={() => { + }} + onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle + className="header-menu-toggle" + ref={toggleRef} + aria-label="menu" + variant="plain" + onClick={() => setMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + > + <EllipsisVIcon/> + </MenuToggle> + )} + > + <DropdownList> + {isFrom && + <DropdownItem key="changeFrom" onClick={(ev) => { + ev.preventDefault() + openSelectorToReplaceFrom((selectedStep as any).id) setMenuOpen(false); - } - }}> - Save Element to Route - </DropdownItem>} - {targetDsl && - <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} - </DropdownList> - </Dropdown> : <></>; + }}> + Change From... + </DropdownItem>} + {hasSteps && + <DropdownItem key="saveStepsRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, true); + setMenuOpen(false); + } + }}> + Save Steps to Route + </DropdownItem>} + {hasSteps && !isFrom && + <DropdownItem key="saveElementRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, false); + setMenuOpen(false); + } + }}> + Save Element to Route + </DropdownItem>} + {targetDsl && + <DropdownItem key="convert" + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} + </DropdownList> + </Dropdown> + </div> + : <></>; } function getExchangePropertiesSection(): React.JSX.Element { @@ -138,35 +147,35 @@ export function PropertiesHeader(props: Props) { isExpanded={isExchangePropertiesExpanded}> <Flex className='component-headers' direction={{default: "column"}}> {exchangeProperties.map((header, index, array) => - <Flex key={index}> - <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" - isCode> - {header.name} - </ClipboardCopy> - <FlexItem align={{default: 'alignRight'}}> - <Popover - position={"left"} - headerContent={header.name} - bodyContent={header.description} - footerContent={ - <Flex> - <Text component={TextVariants.p}>{header.javaType}</Text> - <FlexItem align={{default: 'alignRight'}}> - <Badge isRead>{header.label}</Badge> - </FlexItem> - </Flex> - } - > - <button type="button" aria-label="More info" onClick={e => { - e.preventDefault(); - e.stopPropagation(); - }} className="pf-v5-c-form__group-label-help"> - <HelpIcon/> - </button> - </Popover> - </FlexItem> - </Flex> - )} + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <FlexItem align={{default: 'alignRight'}}> + <Popover + position={"left"} + headerContent={header.name} + bodyContent={header.description} + footerContent={ + <Flex> + <Text component={TextVariants.p}>{header.javaType}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Badge isRead>{header.label}</Badge> + </FlexItem> + </Flex> + } + > + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + </FlexItem> + </Flex> + )} </Flex> </ExpandableSection> ) @@ -235,24 +244,58 @@ export function PropertiesHeader(props: Props) { const component = ComponentApi.findStepComponent(selectedStep); const groups = (isFrom || isPoll) ? ['consumer', 'common'] : ['producer', 'common']; const isKamelet = CamelUi.isKamelet(selectedStep); - const isStepComponent = !isFrom && selectedStep !== undefined && !isKamelet && ['ToDefinition', 'PollDefinition'].includes(selectedStep?.dslName); + const isStepComponent = !isFrom && selectedStep !== undefined && !isKamelet && ['ToDefinition', 'PollDefinition', 'ToDynamicDefinition'].includes(selectedStep?.dslName); + + function changeStepType(poll: boolean, dynamic: boolean) { + if (selectedStep) { + if (poll) { + convertStep(selectedStep, 'PollDefinition'); + setStepIsPoll(true); + setStepDynamic(false); + } else if (dynamic) { + convertStep(selectedStep, 'ToDynamicDefinition'); + setStepIsPoll(false); + setStepDynamic(true); + } else { + convertStep(selectedStep, 'ToDefinition'); + setStepIsPoll(false); + setStepDynamic(false); + } + } + } function getComponentStepTypeSwitch() { - return (component?.component.producerOnly - ? <></> - : <Switch - id="step-type-switch" - label="Poll" - isChecked={isStepTypeOpen} - onChange={(event, checked) => { - if (selectedStep) { - convertStep(selectedStep, checked ? 'PollDefinition' : 'ToDefinition'); - setIsStepTypeOpen(checked); - } - }} - ouiaId="step-type-switch" - isReversed - /> + const pollSupported = !component?.component.producerOnly; + return (<div style={{display: 'flex', flexDirection: 'row', justifyContent: 'end', width: '100%', gap: '10px'}}> + <Tooltip content='Send messages to a dynamic endpoint evaluated on-demand' position='top-end'> + <Switch + id="step-type-dynamic" + label="Dynamic" + isChecked={stepDynamic} + onChange={(event, checked) => { + changeStepType(stepIsPoll, checked) + }} + ouiaId="step-type-switch" + isDisabled={stepIsPoll} + isReversed + /> + </Tooltip> + {pollSupported && + <Tooltip content='Simple Polling Consumer to obtain the additional data' position='top-end'> + <Switch + id="step-type-poll" + label="Poll" + isChecked={stepIsPoll} + onChange={(event, checked) => { + changeStepType(checked, stepDynamic) + }} + ouiaId="step-type-switch" + isDisabled={stepDynamic} + isReversed + /> + </Tooltip> + } + </div> ) } diff --git a/karavan-designer/src/designer/property/usePropertiesHook.tsx b/karavan-designer/src/designer/property/usePropertiesHook.tsx index 2d806c0d..5c8e38b4 100644 --- a/karavan-designer/src/designer/property/usePropertiesHook.tsx +++ b/karavan-designer/src/designer/property/usePropertiesHook.tsx @@ -177,6 +177,7 @@ export function usePropertiesHook(designerType: 'routes' | 'rest' | 'beans' = 'r } const convertStep = (step: CamelElement, targetDslName: string) => { + console.log(targetDslName) try { // setSelectedStep(undefined); if (targetDslName === 'ChoiceDefinition' && step.dslName === 'FilterDefinition') { diff --git a/karavan-designer/src/designer/route/DslConnections.css b/karavan-designer/src/designer/route/DslConnections.css index 03da5f2a..a75582ea 100644 --- a/karavan-designer/src/designer/route/DslConnections.css +++ b/karavan-designer/src/designer/route/DslConnections.css @@ -54,6 +54,15 @@ fill: transparent; } +.karavan .dsl-page .graph .connections .path-dynamic { + stroke-dasharray: 5; + -webkit-animation: dashdrawR 0.5s linear infinite; + animation: dashdrawR 0.5s linear infinite; + stroke: var(--pf-v5-global--Color--200); + stroke-width: 1; + fill: transparent; +} + .karavan .dsl-page .graph .connections .path-poll { stroke-dasharray: 5; -webkit-animation: dashdrawL 0.5s linear infinite; diff --git a/karavan-designer/src/designer/route/DslConnections.tsx b/karavan-designer/src/designer/route/DslConnections.tsx index 99fd0934..be404896 100644 --- a/karavan-designer/src/designer/route/DslConnections.tsx +++ b/karavan-designer/src/designer/route/DslConnections.tsx @@ -33,6 +33,7 @@ import {INTERNAL_COMPONENTS} from "karavan-core/lib/api/ComponentApi"; const overlapGap: number = 40; const DIAMETER: number = 34; const RADIUS: number = DIAMETER / 2; +type ConnectionType = 'internal' | 'remote' | 'nav' | 'poll' | 'dynamic'; export function DslConnections() { @@ -83,10 +84,12 @@ export function DslConnections() { } } - function getElementType(element: CamelElement): 'internal' | 'remote' | 'nav' | 'poll' { + function getElementType(element: CamelElement): ConnectionType { const uri = (element as any).uri; if (element.dslName === 'PollDefinition') { return 'poll'; + } else if (element.dslName === 'ToDynamicDefinition') { + return 'dynamic'; } else if (INTERNAL_COMPONENTS.includes((uri))) { return 'nav'; } else { @@ -95,8 +98,8 @@ export function DslConnections() { } } - function getIncomings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { - let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values()) + function getIncomings(): [string, number, ConnectionType][] { + let outs: [string, number, ConnectionType][] = Array.from(steps.values()) .filter(pos => ["FromDefinition"].includes(pos.step.dslName)) .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source')) .sort((pos1: DslPosition, pos2: DslPosition) => { @@ -111,7 +114,7 @@ export function DslConnections() { return outs; } - function getIncoming(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getIncoming(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; @@ -144,7 +147,7 @@ export function DslConnections() { // .filter(s => (s.step as any)?.parameters?.name === name) // } - function getIncomingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getIncomingIcons(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const step = (pos.step as any); @@ -186,7 +189,7 @@ export function DslConnections() { } } - function hasOverlap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): boolean { + function hasOverlap(data: [string, number, ConnectionType][]): boolean { let result = false; data.forEach((d, i, arr) => { if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result = true; @@ -194,8 +197,8 @@ export function DslConnections() { return result; } - function addGap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { - const result: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = []; + function addGap(data: [string, number, ConnectionType][]): [string, number, ConnectionType][] { + const result: [string, number, ConnectionType][] = []; data.forEach((d, i, arr) => { if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result.push([d[0], d[1] + overlapGap, d[2]]) else result.push(d); @@ -204,12 +207,12 @@ export function DslConnections() { } - function getOutgoings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { + function getOutgoings(): [string, number, ConnectionType][] { const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions(); - let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values()) + let outs: [string, number, ConnectionType][] = Array.from(steps.values()) .filter(pos => outgoingDefinitions.includes(pos.step.dslName)) .filter(pos => pos.step.dslName !== 'KameletDefinition' || (pos.step.dslName === 'KameletDefinition' && !CamelUi.isActionKamelet(pos.step))) - .filter(pos => ['ToDefinition', 'PollDefinition'].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step)) + .filter(pos => ['ToDefinition', 'PollDefinition', "ToDynamicDefinition"].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step)) .filter(pos => !CamelUi.isKameletSink(pos.step)) .sort((pos1: DslPosition, pos2: DslPosition) => { const y1 = pos1.headerRect.y + pos1.headerRect.height / 2; @@ -223,11 +226,12 @@ export function DslConnections() { return outs; } - function getOutgoing(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getOutgoing(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); const isInternal = data[2] === 'internal'; const isNav = data[2] === 'nav'; const isPoll = data[2] === 'poll'; + const isDynamic = data[2] === 'dynamic'; if (pos) { const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top; @@ -244,7 +248,9 @@ export function DslConnections() { const lineXi = lineX1 + 40; const lineYi = lineY2; - const className = isNav ? 'path-incoming-nav' : (isPoll ? 'path-poll' : 'path-incoming') + const className = isNav + ? 'path-incoming-nav' + : (isPoll ? 'path-poll' : (isDynamic ? 'path-dynamic' :'path-incoming')) return (!isInternal ? <g key={pos.step.uuid + "-outgoing"}> @@ -258,7 +264,7 @@ export function DslConnections() { } } - function getOutgoingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getOutgoingIcons(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const step = (pos.step as any); diff --git a/karavan-designer/src/designer/route/element/DslElement.css b/karavan-designer/src/designer/route/element/DslElement.css index 785f667b..1b431837 100644 --- a/karavan-designer/src/designer/route/element/DslElement.css +++ b/karavan-designer/src/designer/route/element/DslElement.css @@ -128,6 +128,10 @@ border-radius: 33px; } +.karavan .step-element .header-icon-circle .dynamic{ + fill: var(--pf-v5-global--Color--400); +} + .karavan .step-element .header-icon-square { border-radius: 33px; } diff --git a/karavan-designer/src/designer/utils/CamelUi.tsx b/karavan-designer/src/designer/utils/CamelUi.tsx index 045b154d..646f5ed9 100644 --- a/karavan-designer/src/designer/utils/CamelUi.tsx +++ b/karavan-designer/src/designer/utils/CamelUi.tsx @@ -728,7 +728,9 @@ export class CamelUi { case 'AggregateDefinition': return <AggregateIcon/>; case 'ToDefinition': - return <ToIcon/>; + return ToIcon(); + case 'ToDynamicDefinition': + return ToIcon('dynamic'); case 'PollDefinition': return <PollIcon/>; case 'ChoiceDefinition' : diff --git a/karavan-space/src/designer/icons/EipIcons.tsx b/karavan-space/src/designer/icons/EipIcons.tsx index fc928d9f..4bdac684 100644 --- a/karavan-space/src/designer/icons/EipIcons.tsx +++ b/karavan-space/src/designer/icons/EipIcons.tsx @@ -50,14 +50,14 @@ export function AggregateIcon() { ); } -export function ToIcon() { +export function ToIcon(classname: string = '') { return ( <svg xmlns="http://www.w3.org/2000/svg" width={800} height={800} viewBox="0 0 32 32" - className="icon" + className={classname ? "icon " + classname : "icon"} > <path d="m12.103 11.923 2.58 2.59H2.513v2h12.17l-2.58 2.59 1.41 1.41 5-5-5-5z"/> <path diff --git a/karavan-space/src/designer/property/DslProperties.css b/karavan-space/src/designer/property/DslProperties.css index 97b97a38..e2401cf3 100644 --- a/karavan-space/src/designer/property/DslProperties.css +++ b/karavan-space/src/designer/property/DslProperties.css @@ -24,14 +24,22 @@ width: 100%; display: flex; flex-direction: row; + /*justify-content: space-between;*/ + gap: 10px; } .karavan .properties .headers .top h1 { - width: 100%; + /*width: 100%;*/ margin-top: auto; margin-bottom: auto; + text-wrap: nowrap; +} + +.karavan .properties .headers .top .pf-v5-c-switch { + column-gap: 6px; } + .karavan .properties .footer { height: 100%; display: contents; diff --git a/karavan-space/src/designer/property/PropertiesHeader.tsx b/karavan-space/src/designer/property/PropertiesHeader.tsx index b8aca6b4..00a3e1ee 100644 --- a/karavan-space/src/designer/property/PropertiesHeader.tsx +++ b/karavan-space/src/designer/property/PropertiesHeader.tsx @@ -25,13 +25,13 @@ import { MenuToggle, DropdownList, DropdownItem, Flex, Popover, FlexItem, Badge, ClipboardCopy, - Switch, + Switch, Radio, Tooltip, } from '@patternfly/react-core'; import '../karavan.css'; import './DslProperties.css'; import "@patternfly/patternfly/patternfly.css"; import {CamelUi} from "../utils/CamelUi"; -import {useDesignerStore, useSelectorStore} from "../DesignerStore"; +import {useDesignerStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; @@ -56,7 +56,13 @@ export function PropertiesHeader(props: Props) { const [isHeadersExpanded, setIsHeadersExpanded] = useState<boolean>(false); const [isExchangePropertiesExpanded, setIsExchangePropertiesExpanded] = useState<boolean>(false); const [isMenuOpen, setMenuOpen] = useState<boolean>(false); - const [isStepTypeOpen, setIsStepTypeOpen] = React.useState(false); + const [stepIsPoll, setStepIsPoll] = React.useState(false); + const [stepDynamic, setStepDynamic] = React.useState(false); + + useEffect(() => { + setStepDynamic(selectedStep?.dslName === 'ToDynamicDefinition') + setStepIsPoll(selectedStep?.dslName === 'PollDefinition') + }, []) useEffect(() => { setMenuOpen(false) @@ -68,67 +74,70 @@ export function PropertiesHeader(props: Props) { const targetDslTitle = targetDsl?.replace("Definition", ""); const showMenu = hasSteps || targetDsl !== undefined; return showMenu ? - <Dropdown - popperProps={{position: "end"}} - isOpen={isMenuOpen} - onSelect={() => { - }} - onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} - toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( - <MenuToggle - className="header-menu-toggle" - ref={toggleRef} - aria-label="menu" - variant="plain" - onClick={() => setMenuOpen(!isMenuOpen)} - isExpanded={isMenuOpen} - > - <EllipsisVIcon/> - </MenuToggle> - )} - > - <DropdownList> - {isFrom && - <DropdownItem key="changeFrom" onClick={(ev) => { - ev.preventDefault() - openSelectorToReplaceFrom((selectedStep as any).id) - setMenuOpen(false); - }}> - Change From... - </DropdownItem>} - {hasSteps && - <DropdownItem key="saveStepsRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, true); - setMenuOpen(false); - } - }}> - Save Steps to Route - </DropdownItem>} - {hasSteps && !isFrom && - <DropdownItem key="saveElementRoute" onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - saveAsRoute(selectedStep, false); + <div style={{display: 'flex', flexDirection: 'row', justifyContent: 'end', width: '100%'}}> + <Dropdown + popperProps={{position: "end"}} + isOpen={isMenuOpen} + onSelect={() => { + }} + onOpenChange={(isOpen: boolean) => setMenuOpen(isOpen)} + toggle={(toggleRef: React.Ref<MenuToggleElement>) => ( + <MenuToggle + className="header-menu-toggle" + ref={toggleRef} + aria-label="menu" + variant="plain" + onClick={() => setMenuOpen(!isMenuOpen)} + isExpanded={isMenuOpen} + > + <EllipsisVIcon/> + </MenuToggle> + )} + > + <DropdownList> + {isFrom && + <DropdownItem key="changeFrom" onClick={(ev) => { + ev.preventDefault() + openSelectorToReplaceFrom((selectedStep as any).id) setMenuOpen(false); - } - }}> - Save Element to Route - </DropdownItem>} - {targetDsl && - <DropdownItem key="convert" - onClick={(ev) => { - ev.preventDefault() - if (selectedStep) { - convertStep(selectedStep, targetDsl); - setMenuOpen(false); - } - }}> - Convert to {targetDslTitle} - </DropdownItem>} - </DropdownList> - </Dropdown> : <></>; + }}> + Change From... + </DropdownItem>} + {hasSteps && + <DropdownItem key="saveStepsRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, true); + setMenuOpen(false); + } + }}> + Save Steps to Route + </DropdownItem>} + {hasSteps && !isFrom && + <DropdownItem key="saveElementRoute" onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + saveAsRoute(selectedStep, false); + setMenuOpen(false); + } + }}> + Save Element to Route + </DropdownItem>} + {targetDsl && + <DropdownItem key="convert" + onClick={(ev) => { + ev.preventDefault() + if (selectedStep) { + convertStep(selectedStep, targetDsl); + setMenuOpen(false); + } + }}> + Convert to {targetDslTitle} + </DropdownItem>} + </DropdownList> + </Dropdown> + </div> + : <></>; } function getExchangePropertiesSection(): React.JSX.Element { @@ -138,35 +147,35 @@ export function PropertiesHeader(props: Props) { isExpanded={isExchangePropertiesExpanded}> <Flex className='component-headers' direction={{default: "column"}}> {exchangeProperties.map((header, index, array) => - <Flex key={index}> - <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" - isCode> - {header.name} - </ClipboardCopy> - <FlexItem align={{default: 'alignRight'}}> - <Popover - position={"left"} - headerContent={header.name} - bodyContent={header.description} - footerContent={ - <Flex> - <Text component={TextVariants.p}>{header.javaType}</Text> - <FlexItem align={{default: 'alignRight'}}> - <Badge isRead>{header.label}</Badge> - </FlexItem> - </Flex> - } - > - <button type="button" aria-label="More info" onClick={e => { - e.preventDefault(); - e.stopPropagation(); - }} className="pf-v5-c-form__group-label-help"> - <HelpIcon/> - </button> - </Popover> - </FlexItem> - </Flex> - )} + <Flex key={index}> + <ClipboardCopy key={index} hoverTip="Copy" clickTip="Copied" variant="inline-compact" + isCode> + {header.name} + </ClipboardCopy> + <FlexItem align={{default: 'alignRight'}}> + <Popover + position={"left"} + headerContent={header.name} + bodyContent={header.description} + footerContent={ + <Flex> + <Text component={TextVariants.p}>{header.javaType}</Text> + <FlexItem align={{default: 'alignRight'}}> + <Badge isRead>{header.label}</Badge> + </FlexItem> + </Flex> + } + > + <button type="button" aria-label="More info" onClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} className="pf-v5-c-form__group-label-help"> + <HelpIcon/> + </button> + </Popover> + </FlexItem> + </Flex> + )} </Flex> </ExpandableSection> ) @@ -235,24 +244,58 @@ export function PropertiesHeader(props: Props) { const component = ComponentApi.findStepComponent(selectedStep); const groups = (isFrom || isPoll) ? ['consumer', 'common'] : ['producer', 'common']; const isKamelet = CamelUi.isKamelet(selectedStep); - const isStepComponent = !isFrom && selectedStep !== undefined && !isKamelet && ['ToDefinition', 'PollDefinition'].includes(selectedStep?.dslName); + const isStepComponent = !isFrom && selectedStep !== undefined && !isKamelet && ['ToDefinition', 'PollDefinition', 'ToDynamicDefinition'].includes(selectedStep?.dslName); + + function changeStepType(poll: boolean, dynamic: boolean) { + if (selectedStep) { + if (poll) { + convertStep(selectedStep, 'PollDefinition'); + setStepIsPoll(true); + setStepDynamic(false); + } else if (dynamic) { + convertStep(selectedStep, 'ToDynamicDefinition'); + setStepIsPoll(false); + setStepDynamic(true); + } else { + convertStep(selectedStep, 'ToDefinition'); + setStepIsPoll(false); + setStepDynamic(false); + } + } + } function getComponentStepTypeSwitch() { - return (component?.component.producerOnly - ? <></> - : <Switch - id="step-type-switch" - label="Poll" - isChecked={isStepTypeOpen} - onChange={(event, checked) => { - if (selectedStep) { - convertStep(selectedStep, checked ? 'PollDefinition' : 'ToDefinition'); - setIsStepTypeOpen(checked); - } - }} - ouiaId="step-type-switch" - isReversed - /> + const pollSupported = !component?.component.producerOnly; + return (<div style={{display: 'flex', flexDirection: 'row', justifyContent: 'end', width: '100%', gap: '10px'}}> + <Tooltip content='Send messages to a dynamic endpoint evaluated on-demand' position='top-end'> + <Switch + id="step-type-dynamic" + label="Dynamic" + isChecked={stepDynamic} + onChange={(event, checked) => { + changeStepType(stepIsPoll, checked) + }} + ouiaId="step-type-switch" + isDisabled={stepIsPoll} + isReversed + /> + </Tooltip> + {pollSupported && + <Tooltip content='Simple Polling Consumer to obtain the additional data' position='top-end'> + <Switch + id="step-type-poll" + label="Poll" + isChecked={stepIsPoll} + onChange={(event, checked) => { + changeStepType(checked, stepDynamic) + }} + ouiaId="step-type-switch" + isDisabled={stepDynamic} + isReversed + /> + </Tooltip> + } + </div> ) } diff --git a/karavan-space/src/designer/property/usePropertiesHook.tsx b/karavan-space/src/designer/property/usePropertiesHook.tsx index 2d806c0d..5c8e38b4 100644 --- a/karavan-space/src/designer/property/usePropertiesHook.tsx +++ b/karavan-space/src/designer/property/usePropertiesHook.tsx @@ -177,6 +177,7 @@ export function usePropertiesHook(designerType: 'routes' | 'rest' | 'beans' = 'r } const convertStep = (step: CamelElement, targetDslName: string) => { + console.log(targetDslName) try { // setSelectedStep(undefined); if (targetDslName === 'ChoiceDefinition' && step.dslName === 'FilterDefinition') { diff --git a/karavan-space/src/designer/route/DslConnections.css b/karavan-space/src/designer/route/DslConnections.css index 03da5f2a..a75582ea 100644 --- a/karavan-space/src/designer/route/DslConnections.css +++ b/karavan-space/src/designer/route/DslConnections.css @@ -54,6 +54,15 @@ fill: transparent; } +.karavan .dsl-page .graph .connections .path-dynamic { + stroke-dasharray: 5; + -webkit-animation: dashdrawR 0.5s linear infinite; + animation: dashdrawR 0.5s linear infinite; + stroke: var(--pf-v5-global--Color--200); + stroke-width: 1; + fill: transparent; +} + .karavan .dsl-page .graph .connections .path-poll { stroke-dasharray: 5; -webkit-animation: dashdrawL 0.5s linear infinite; diff --git a/karavan-space/src/designer/route/DslConnections.tsx b/karavan-space/src/designer/route/DslConnections.tsx index 99fd0934..be404896 100644 --- a/karavan-space/src/designer/route/DslConnections.tsx +++ b/karavan-space/src/designer/route/DslConnections.tsx @@ -33,6 +33,7 @@ import {INTERNAL_COMPONENTS} from "karavan-core/lib/api/ComponentApi"; const overlapGap: number = 40; const DIAMETER: number = 34; const RADIUS: number = DIAMETER / 2; +type ConnectionType = 'internal' | 'remote' | 'nav' | 'poll' | 'dynamic'; export function DslConnections() { @@ -83,10 +84,12 @@ export function DslConnections() { } } - function getElementType(element: CamelElement): 'internal' | 'remote' | 'nav' | 'poll' { + function getElementType(element: CamelElement): ConnectionType { const uri = (element as any).uri; if (element.dslName === 'PollDefinition') { return 'poll'; + } else if (element.dslName === 'ToDynamicDefinition') { + return 'dynamic'; } else if (INTERNAL_COMPONENTS.includes((uri))) { return 'nav'; } else { @@ -95,8 +98,8 @@ export function DslConnections() { } } - function getIncomings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { - let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values()) + function getIncomings(): [string, number, ConnectionType][] { + let outs: [string, number, ConnectionType][] = Array.from(steps.values()) .filter(pos => ["FromDefinition"].includes(pos.step.dslName)) .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source')) .sort((pos1: DslPosition, pos2: DslPosition) => { @@ -111,7 +114,7 @@ export function DslConnections() { return outs; } - function getIncoming(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getIncoming(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; @@ -144,7 +147,7 @@ export function DslConnections() { // .filter(s => (s.step as any)?.parameters?.name === name) // } - function getIncomingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getIncomingIcons(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const step = (pos.step as any); @@ -186,7 +189,7 @@ export function DslConnections() { } } - function hasOverlap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): boolean { + function hasOverlap(data: [string, number, ConnectionType][]): boolean { let result = false; data.forEach((d, i, arr) => { if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result = true; @@ -194,8 +197,8 @@ export function DslConnections() { return result; } - function addGap(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][]): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { - const result: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = []; + function addGap(data: [string, number, ConnectionType][]): [string, number, ConnectionType][] { + const result: [string, number, ConnectionType][] = []; data.forEach((d, i, arr) => { if (i > 0 && d[1] - arr[i - 1][1] < overlapGap) result.push([d[0], d[1] + overlapGap, d[2]]) else result.push(d); @@ -204,12 +207,12 @@ export function DslConnections() { } - function getOutgoings(): [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] { + function getOutgoings(): [string, number, ConnectionType][] { const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions(); - let outs: [string, number, 'internal' | 'remote' | 'nav' | 'poll'][] = Array.from(steps.values()) + let outs: [string, number, ConnectionType][] = Array.from(steps.values()) .filter(pos => outgoingDefinitions.includes(pos.step.dslName)) .filter(pos => pos.step.dslName !== 'KameletDefinition' || (pos.step.dslName === 'KameletDefinition' && !CamelUi.isActionKamelet(pos.step))) - .filter(pos => ['ToDefinition', 'PollDefinition'].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step)) + .filter(pos => ['ToDefinition', 'PollDefinition', "ToDynamicDefinition"].includes(pos.step.dslName) && !CamelUi.isActionKamelet(pos.step)) .filter(pos => !CamelUi.isKameletSink(pos.step)) .sort((pos1: DslPosition, pos2: DslPosition) => { const y1 = pos1.headerRect.y + pos1.headerRect.height / 2; @@ -223,11 +226,12 @@ export function DslConnections() { return outs; } - function getOutgoing(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getOutgoing(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); const isInternal = data[2] === 'internal'; const isNav = data[2] === 'nav'; const isPoll = data[2] === 'poll'; + const isDynamic = data[2] === 'dynamic'; if (pos) { const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top; @@ -244,7 +248,9 @@ export function DslConnections() { const lineXi = lineX1 + 40; const lineYi = lineY2; - const className = isNav ? 'path-incoming-nav' : (isPoll ? 'path-poll' : 'path-incoming') + const className = isNav + ? 'path-incoming-nav' + : (isPoll ? 'path-poll' : (isDynamic ? 'path-dynamic' :'path-incoming')) return (!isInternal ? <g key={pos.step.uuid + "-outgoing"}> @@ -258,7 +264,7 @@ export function DslConnections() { } } - function getOutgoingIcons(data: [string, number, 'internal' | 'remote' | 'nav' | 'poll']) { + function getOutgoingIcons(data: [string, number, ConnectionType]) { const pos = steps.get(data[0]); if (pos) { const step = (pos.step as any); diff --git a/karavan-space/src/designer/route/element/DslElement.css b/karavan-space/src/designer/route/element/DslElement.css index 785f667b..1b431837 100644 --- a/karavan-space/src/designer/route/element/DslElement.css +++ b/karavan-space/src/designer/route/element/DslElement.css @@ -128,6 +128,10 @@ border-radius: 33px; } +.karavan .step-element .header-icon-circle .dynamic{ + fill: var(--pf-v5-global--Color--400); +} + .karavan .step-element .header-icon-square { border-radius: 33px; } diff --git a/karavan-space/src/designer/utils/CamelUi.tsx b/karavan-space/src/designer/utils/CamelUi.tsx index 045b154d..646f5ed9 100644 --- a/karavan-space/src/designer/utils/CamelUi.tsx +++ b/karavan-space/src/designer/utils/CamelUi.tsx @@ -728,7 +728,9 @@ export class CamelUi { case 'AggregateDefinition': return <AggregateIcon/>; case 'ToDefinition': - return <ToIcon/>; + return ToIcon(); + case 'ToDynamicDefinition': + return ToIcon('dynamic'); case 'PollDefinition': return <PollIcon/>; case 'ChoiceDefinition' :