This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit b4fbb2925156e811bcff97b1324ec8893d00dfef Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Tue Dec 5 19:07:19 2023 -0500 First example of new Designer #1012 --- karavan-designer/public/example/demo.camel.yaml | 92 +++++ karavan-designer/src/designer/DesignerStore.ts | 30 +- karavan-designer/src/designer/karavan.css | 409 +-------------------- .../src/designer/route/DslConnections.tsx | 280 +++++--------- karavan-designer/src/designer/route/DslElement.css | 18 - .../src/designer/route/DslProperties.css | 250 +++++++++++++ .../src/designer/route/DslProperties.tsx | 21 +- .../src/designer/route/RouteDesigner.tsx | 24 +- .../src/designer/route/element/DslElement.css | 220 +++++++++++ .../designer/route/{ => element}/DslElement.tsx | 169 +++++---- .../src/designer/route/element/DslElementIcons.tsx | 80 ++++ .../route/{ => element}/DslElementMoveModal.tsx | 6 +- .../src/designer/route/usePropertiesHook.tsx | 11 +- .../src/designer/route/useRouteDesignerHook.tsx | 3 +- karavan-designer/src/designer/utils/CamelUi.tsx | 9 + karavan-designer/src/designer/utils/EventBus.ts | 32 +- 16 files changed, 950 insertions(+), 704 deletions(-) diff --git a/karavan-designer/public/example/demo.camel.yaml b/karavan-designer/public/example/demo.camel.yaml index 56bd06fc..40e3c45b 100644 --- a/karavan-designer/public/example/demo.camel.yaml +++ b/karavan-designer/public/example/demo.camel.yaml @@ -4,3 +4,95 @@ from: uri: timer id: from-cfa5 + steps: + - choice: + when: + - expression: + simple: + id: simple-99bf + id: when-ab5e + steps: + - to: + uri: arangodb + id: to-f70a + - removeProperties: + id: removeProperties-344a + - expression: + simple: + id: simple-d199 + id: when-37cd + steps: + - to: + uri: amqp + id: to-fbfe + otherwise: + id: otherwise-382c + steps: + - log: + message: ${body} + id: log-6831 + id: choice-c1db + - multicast: + id: multicast-6a53 + steps: + - log: + message: ${body} + id: log-799d + - log: + message: ${body} + id: log-fc8e + - log: + message: ${body} + id: log-1e42 + - filter: + expression: + simple: + id: simple-7ff9 + id: filter-8c99 + steps: + - process: + id: process-e1c1 + description: Call cutom java bean + - delay: + expression: + simple: + id: simple-64a6 + id: delay-b1ec + - doTry: + id: doTry-46cd + doCatch: + - id: doCatch-c6e7 + steps: + - log: + message: ${body} + id: log-77df + - wireTap: + id: wireTap-a25e + doFinally: + id: doFinally-0a65 + steps: + - log: + message: ${body} + id: log-f4fa + - log: + message: ${body} + id: log-cd30 + steps: + - pollEnrich: + expression: + simple: + id: simple-6181 + id: pollEnrich-a41b + - filter: + expression: + simple: + id: simple-a69b + id: filter-07cf + steps: + - setBody: + expression: + simple: + id: simple-f0dc + id: setBody-3c0c + - circuitBreaker: + id: circuitBreaker-4af8 diff --git a/karavan-designer/src/designer/DesignerStore.ts b/karavan-designer/src/designer/DesignerStore.ts index 4831855f..f9bc38d1 100644 --- a/karavan-designer/src/designer/DesignerStore.ts +++ b/karavan-designer/src/designer/DesignerStore.ts @@ -16,7 +16,7 @@ */ import {CamelElement, Integration} from "karavan-core/lib/model/IntegrationDefinition"; -import {DslPosition, EventBus} from "./utils/EventBus"; +import {ButtonPosition, DslPosition, EventBus} from "./utils/EventBus"; import {createWithEqualityFn} from "zustand/traditional"; import {shallow} from "zustand/shallow"; @@ -120,6 +120,10 @@ interface ConnectionsState { deleteStep: (uuid: string) => void; clearSteps: () => void; setSteps: (steps: Map<string, DslPosition>) => void; + buttons: ButtonPosition[]; + addButton: (button: ButtonPosition) => void; + deleteButton: (button: ButtonPosition) => void; + clearButtons: () => void; } export const useConnectionsStore = createWithEqualityFn<ConnectionsState>((set) => ({ @@ -148,6 +152,30 @@ export const useConnectionsStore = createWithEqualityFn<ConnectionsState>((set) setSteps: (steps: Map<string, DslPosition>) => { set({steps: steps}) }, + buttons: [], + addButton: (button: ButtonPosition) => { + set((state: ConnectionsState) => { + const index = state.buttons.findIndex(b => b.uuid === button.uuid); + if (index !== -1) { + state.buttons.splice(index, 1); + } + state.buttons.push(button); + return state; + }) + }, + clearButtons: () => { + set((state: ConnectionsState) => { + state.buttons.length = 0; + return state; + }) + }, + deleteButton: (button: ButtonPosition) => { + set((state: ConnectionsState) => { + const index = state.buttons.findIndex(b => b.uuid === button.uuid); + state.buttons.splice(index, 1); + return state; + }) + }, }), shallow) type DesignerState = { diff --git a/karavan-designer/src/designer/karavan.css b/karavan-designer/src/designer/karavan.css index 31aa866b..5e558152 100644 --- a/karavan-designer/src/designer/karavan.css +++ b/karavan-designer/src/designer/karavan.css @@ -355,241 +355,6 @@ display: none; } -.karavan .properties .headers { - grid-row-gap: 10px; - row-gap: 10px; - display: contents; -} - -.karavan .properties .headers .top { - width: 100%; - display: flex; - flex-direction: row; -} - -.karavan .properties .headers .top h1 { - width: 100%; - margin-top: auto; - margin-bottom: auto; -} - -.karavan .properties .footer { - height: 100%; - display: contents; -} - -.karavan .properties .pf-v5-c-form { - row-gap: 10px; -} - -.karavan .properties .pf-v5-c-form__group-label { - padding-bottom: 3px; - display: flex; - justify-content: space-between; -} - -.karavan .properties .pf-v5-c-form-control:focus-within { - --pf-v5-c-form-control--after--BorderBottomColor: var(--pf-v5-c-form-control--after--BorderBottomColor); - --pf-v5-c-form-control--after--BorderBottomWidth: var(--pf-v5-c-form-control--after--BorderBottomWidth); -} - -.karavan .properties .pf-v5-c-select { - --pf-v5-c-select__toggle--FontSize: 14px; - --pf-v5-c-select__menu-item--FontSize: 14px; -} - -.karavan .properties .input-group .pf-v5-c-text-input-group__text { - width: 100%; -} - -.karavan .properties .input-group .pf-v5-c-chip-group, -.karavan .properties .input-group .pf-v5-c-text-input-group__main, -.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip-group__list, -.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip-group__list .pf-v5-c-chip-group__list-item, -.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip-group__main { - display: block; -} - -.karavan .properties .input-group .pf-v5-c-chip-group { - margin-left: 0; -} - -.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip .pf-v5-c-chip__text { - max-width: inherit; -} - -.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip { - width: 100%; -} - -.karavan .properties .input-group .pf-v5-c-text-input-group__utilities { - align-items: end; - margin-top: auto; -} - -.karavan .properties .chip .pf-v5-c-button { - position: absolute; - right: 0; -} - -.karavan .properties .expression-title { - font-size: 17px; - font-weight: bold; -} - -.karavan .properties .text-area { - font-size: 13px; -} - -.karavan .properties .pf-v5-c-select__menu-search { - padding: 0px 6px 6px 6px; -} - -.karavan .properties .pf-v5-c-select__toggle-typeahead { - height: auto; -} - -.karavan .properties .pf-v5-c-select__menu-item { - /*width: 280px;*/ -} - -.karavan .properties .pf-v5-c-select__menu-item-description { - overflow-wrap: anywhere; -} - -.karavan .properties .number { - display: flex; - justify-content: space-between; -} - -.karavan .properties .number .number-property { - width: 100%; -} - -.karavan .properties .number .clear-button { - color: #b1b1b7; - --pf-v5-c-button--BorderRadius: var(--pf-v5-c-button--m-control--BorderRadius); - --pf-v5-c-button--disabled--BackgroundColor: var(--pf-v5-c-button--m-control--disabled--BackgroundColor); - --pf-v5-c-button--after--BorderWidth: var(--pf-v5-c-button--m-control--after--BorderWidth); - --pf-v5-c-button--after--BorderColor: var(--pf-v5-c-button--m-control--after--BorderTopColor) var(--pf-v5-c-button--m-control--after--BorderRightColor) var(--pf-v5-c-button--m-control--after--BorderBottomColor) var(--pf-v5-c-button--m-control--after--BorderLeftColor); - color: var(--pf-v5-c-button--m-control--Color); - background-color: var(--pf-v5-c-button--m-control--BackgroundColor); - padding-left: 5px; - padding-right: 5px; -} - -.karavan .properties .help-button { - font-size: 12px; -} - -.karavan .properties .component-selector { - border-width: var(--pf-v5-global--BorderWidth--sm); - border-top-color: var(--pf-v5-global--BorderColor--300); - border-right-color: var(--pf-v5-global--BorderColor--300); - border-bottom-color: var(--pf-v5-global--BorderColor--200); - border-left-color: var(--pf-v5-global--BorderColor--300); - border-style: solid; -} - -.karavan .properties .object-value { - display: flex; - flex-direction: row; - margin-bottom: 3px; -} - -.karavan .properties .object-key-value .object { - padding-top: 0; -} - -.karavan .properties .object-value .delete-button { - margin: 0; - padding: 5px 3px 0px 6px; - height: 16px; - color: #909090; -} - -.karavan .properties .object-key-value, -.karavan .properties .object-key-value .object-field { - display: flex; - flex-direction: row; - gap: 3px; -} - -.karavan .properties .object-key-value .delete-button { - margin: auto 0 22px 0; - padding: 0px 0px 0px 3px; - height: 16px; - color: #909090; -} - -.karavan .properties .expression, -.karavan .properties .object, -.karavan .properties .dataformat, -.karavan .properties .parameters { - padding-top: 6px; - padding-left: 16px; - row-gap: 6px; - display: grid; - width: 100%; -} - -.karavan .properties .expression .pf-v5-c-form__group-label, -.karavan .properties .object .pf-v5-c-form__group-label, -.karavan .properties .dataformat .pf-v5-c-form__group-label, -.karavan .properties .parameters .pf-v5-c-form__group-label { - font-weight: 100; -} - -.karavan .properties .expression .pf-v5-c-form__group, -.karavan .properties .object .pf-v5-c-form__group, -.karavan .properties .dataformat .pf-v5-c-form__group, -.karavan .properties .parameters .pf-v5-c-form__group { - margin-bottom: 10px; -} - -.karavan .properties .expression .pf-v5-c-select__menu-wrapper, -.karavan .properties .object .pf-v5-c-select__menu-wrapper, -.karavan .properties .dataformat .pf-v5-c-select__menu-wrapper, -.karavan .properties .parameters .pf-v5-c-select__menu-wrapper { - width: 350px; -} - -.karavan .properties .change-button { - font-size: 15px; - height: 15px; - line-height: 1; - border: 0; - padding: 0; - margin: auto auto auto 6px; - background: transparent; -} - -.karavan .properties .change-button svg { - margin-right: 6px; -} - -.karavan .properties .add-button { - color: var(--pf-v5-global--active-color--100); -} - -.karavan .properties .delete-button { - color: #909090; -} - -.karavan .properties .pf-v5-c-expandable-section__toggle { - margin: 0; - padding: 0; -} - -.karavan .properties .pf-v5-c-expandable-section__content { - margin: 0; -} - -.karavan .properties .pf-v5-c-expandable-section__content p { - min-height: 6px; -} - - /*Graph*/ .karavan .dsl-page .graph { display: block; @@ -691,7 +456,7 @@ flex-direction: column; width: fit-content; border-color: var(--pf-v5-global--Color--200); - border-radius: 16px; + border-radius: 42px; border-width: 1px; min-width: 120px; padding: 3px 4px 4px 4px; @@ -718,178 +483,6 @@ gap: 6px; } -.karavan .dsl-page .flows .step-element .header-route { - display: block; - border: none; - background: transparent; - padding: 0px 0px 0px 0px; - margin-bottom: 10px; - min-width: 160px; - z-index: 101; -} - -.karavan .step-element .header-route .delete-button { - position: absolute; - top: 4px; - right: 4px; - line-height: 1; - border: 0; - padding: 0; - margin: 0; - background: transparent; - color: #909090; - visibility: hidden; -} - -.karavan .step-element .header .delete-button, -.element-builder .header .delete-button { - position: absolute; - top: -5px; - line-height: 1; - border: 0; - padding: 0; - margin: 0 0 0 10px; - background: transparent; - color: #909090; - visibility: hidden; -} - -/*.karavan .step-element:hover .delete-button,*/ -/*.karavan .step-element:hover .delete-button,*/ -.karavan .step-element .header:hover .delete-button, -.karavan .step-element .header-route:hover .delete-button, -.element-builder .header:hover .delete-button { - visibility: visible; -} - -.modal-delete { - width: 350px !important; -} - -.karavan .step-element .header { - height: 50px; -} - -.karavan .step-element-selected { - background-color: rgba(var(--pf-v5-global--palette--blue-50), 1); -} - -.karavan .step-element .selected .header-icon { - border-color: var(--pf-v5-global--primary-color--100); - background-color: var(--pf-v5-global--palette--blue-50); - border-width: 2px; -} - -.karavan .step-element .header .header-text { - position: absolute; - top: 8px; - left: 0; - width: 100%; - display: flex; - flex-direction: row; -} - -.karavan .step-element .header .spacer { - width: 50% -} - -.karavan .step-element .header .text-bottom { - background-color: rgba(255, 255, 255, 0.5); -} - -.karavan .step-element .header .text-right { - padding-left: 17px; - width: 50%; - text-align: start; -} - -.karavan .step-element .header .header-text-required { - color: var(--pf-v5-global--danger-color--100); - font-weight: bold; -} - -.karavan .step-element .header-icon { - border-color: var(--pf-v5-global--Color--200); - border-style: solid; - border-radius: 30px; - border-width: 1px; - background: white; - width: 30px; - height: 30px; - margin: auto; - display: flex; - justify-content: center; - align-items: center; -} - -.karavan .step-element .header .icon, -.element-builder .header .icon { - height: 20px; - width: auto; - border: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; -} - -.karavan .step-element .add-element-button { - top: 5px; - left: 5px; - font-size: 15px; - height: 15px; - line-height: 1; - border: 0; - padding: 0; - background: transparent; - color: var(--pf-v5-global--primary-color--100); - visibility: hidden; - z-index: 100; - position: absolute; -} - -.karavan .step-element .insert-element-button { - position: absolute; - top: -5px; - line-height: 1; - border: 0; - padding: 0; - margin: 0 0 0 -25px; - background: transparent; - background: transparent; - color: var(--pf-v5-global--primary-color--100); - visibility: hidden; - z-index: 100; -} - -.karavan .step-element .header:hover .insert-element-button { - visibility: visible; -} - -.karavan .step-element .add-button-bottom { - position: relative; -} - -.karavan .step-element .add-button-left { - position: absolute; - top: 4px; - left: 4px; -} - -.karavan .step-element .add-button { - font-size: 15px; - height: 15px; - line-height: 1; - border: 0; - padding: 0; - margin: 0; - background: transparent; - color: var(--pf-v5-global--primary-color--100); - visibility: hidden; - z-index: 100; -} - .karavan .dsl-page .flows .hidden-step { display: none; } diff --git a/karavan-designer/src/designer/route/DslConnections.tsx b/karavan-designer/src/designer/route/DslConnections.tsx index a636339e..6bb318c2 100644 --- a/karavan-designer/src/designer/route/DslConnections.tsx +++ b/karavan-designer/src/designer/route/DslConnections.tsx @@ -17,9 +17,8 @@ import React, {useEffect} from 'react'; import '../karavan.css'; import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; -import {DslPosition, EventBus} from "../utils/EventBus"; +import {ButtonPosition, DslPosition, EventBus} from "../utils/EventBus"; import {CamelUi} from "../utils/CamelUi"; -import {SagaDefinition} from "karavan-core/lib/model/CamelDefinition"; import {useConnectionsStore, useDesignerStore, useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt"; @@ -32,12 +31,16 @@ export function DslConnections() { const [integration] = useIntegrationStore((state) => [state.integration], shallow) const [width, height, top, left, hideLogDSL] = useDesignerStore((s) => [s.width, s.height, s.top, s.left, s.hideLogDSL], shallow) - const [ steps, addStep, deleteStep, clearSteps] = useConnectionsStore((s) => [s.steps, s.addStep, s.deleteStep, s.clearSteps], shallow) + const [steps, addStep, deleteStep, clearSteps, buttons, addButton, clearButtons, deleteButton] = + useConnectionsStore((s) => [s.steps, s.addStep, s.deleteStep, s.clearSteps, + s.buttons, s.addButton, s.clearButtons, s.deleteButton], shallow) useEffect(() => { - const sub = EventBus.onPosition()?.subscribe((evt: DslPosition) => setPosition(evt)); + const sub1 = EventBus.onPosition()?.subscribe((evt: DslPosition) => setPosition(evt)); + const sub2 = EventBus.onButtonPosition()?.subscribe((btn: ButtonPosition) => setButtonPosition(btn)); return () => { - sub?.unsubscribe(); + sub1?.unsubscribe(); + sub2?.unsubscribe(); }; }); @@ -46,14 +49,22 @@ export function DslConnections() { toDelete.forEach(key => deleteStep(key)); }, [integration]); + function setButtonPosition(btn: ButtonPosition) { + if (btn.command === "add") { + addButton(btn); + } else if (btn.command === "delete") { + deleteButton(btn); + } else if (btn.command === "clean") { + clearButtons(); + } + } + function setPosition(evt: DslPosition) { if (evt.command === "add") { addStep(evt.step.uuid, evt); - } - else if (evt.command === "delete") { + } else if (evt.command === "delete") { deleteStep(evt.step.uuid); - } - else if (evt.command === "clean") { + } else if (evt.command === "clean") { clearSteps(); } } @@ -110,7 +121,8 @@ export function DslConnections() { const imageX = incomingX - r + 5; const imageY = fromY - r + 5; return ( - <div key={pos.step.uuid + "-icon"} style={{display: "block", position: "absolute", top: imageY, left: imageX}}> + <div key={pos.step.uuid + "-icon"} + style={{display: "block", position: "absolute", top: imageY, left: imageX}}> {CamelUi.getConnectionIcon(pos.step)} </div> ) @@ -174,16 +186,14 @@ export function DslConnections() { const lineXi = lineX1 + 40; const lineYi = lineY2; - let image = CamelUi.getConnectionIconString(pos.step); - const imageX = outgoingX - r + 5; - const imageY = outgoingY - r + 5; return ( <g key={pos.step.uuid + "-outgoing"}> <circle cx={outgoingX} cy={outgoingY} r={r} className="circle-outgoing"/> {/*<image x={imageX} y={imageY} href={image} className="icon"/>*/} {/*<text x={imageX + 25} y={imageY + 40} className="caption" textAnchor="end">{CamelUi.getOutgoingTitle(pos.step)}</text>*/} - <path d={`M ${lineX1},${lineY1} C ${lineXi - 20}, ${lineY1} ${lineX1 - 15},${lineYi} ${lineXi},${lineYi} L ${lineX2},${lineY2}`} - className="path-incoming" markerEnd="url(#arrowhead)"/> + <path + d={`M ${lineX1},${lineY1} C ${lineXi - 20}, ${lineY1} ${lineX1 - 15},${lineYi} ${lineXi},${lineYi} L ${lineX2},${lineY2}`} + className="path-incoming" markerEnd="url(#arrowhead)"/> </g> ) } @@ -198,185 +208,25 @@ export function DslConnections() { const imageX = outgoingX - r + 5; const imageY = outgoingY - r + 5; return ( - <div key={pos.step.uuid + "-icon"} style={{display: "block", position: "absolute", top: imageY, left: imageX}}> + <div key={pos.step.uuid + "-icon"} + style={{display: "block", position: "absolute", top: imageY, left: imageX}}> {CamelUi.getConnectionIcon(pos.step)} </div> ) } } - function getInternals(): [string, number, boolean][] { - const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions(); - let outs: [string, number, boolean][] = Array.from(steps.values()) - .filter(pos => outgoingDefinitions.includes(pos.step.dslName) && TopologyUtils.hasInternalUri(pos.step)) - .sort((pos1: DslPosition, pos2: DslPosition) => { - const y1 = pos1.headerRect.y + pos1.headerRect.height / 2; - const y2 = pos2.headerRect.y + pos2.headerRect.height / 2; - return y1 > y2 ? 1 : -1 - }) - .map(pos => [pos.step.uuid, pos.headerRect.y - top, pos.isSelected]); - return outs; - } - - function getInternalLines(data: [string, number, boolean]) { - const pos = steps.get(data[0]); - const uri = (pos?.step as any).uri; - if (uri && uri.length && pos) { - const key = pos.step.uuid + "-outgoing" - const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; - const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top; - const r = pos.headerRect.height / 2; - const className = (TopologyUtils.hasDirectUri(pos.step) ? "path-direct" : "path-seda") + (data[2] ? "-selected" : ""); - return getInternalLine(uri, key, className, fromX, fromY, r, data[1]); - } else if (pos?.step.dslName === 'SagaDefinition'){ - const saga = (pos?.step as SagaDefinition); - const fromX = pos.headerRect.x + pos.headerRect.width / 2 - left; - const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top; - const r = pos.headerRect.height / 2; - const result:any[] = []; - if (saga.completion && (saga.completion.startsWith("direct") || saga.completion.startsWith("seda"))){ - const key = pos.step.uuid + "-completion" - const className = saga.completion.startsWith("direct") ? "path-direct" : "path-seda"; - result.push(getInternalLine(saga.completion, key, className, fromX, fromY, r, data[1])); - } - if (saga.compensation && (saga.compensation.startsWith("direct") || saga.compensation.startsWith("seda"))){ - const key = pos.step.uuid + "-compensation" - const className = saga.compensation.startsWith("direct") ? "path-direct" : "path-seda"; - result.push(getInternalLine(saga.compensation, key, className, fromX, fromY, r, data[1])); - } - return result; - } - } - - function getInternalLine(uri: string, key: string, className: string, fromX: number, fromY: number, r: number, i: number) { - const target = Array.from(steps.values()) - .filter(s => s.step.dslName === 'FromDefinition') - .filter(s => (s.step as any).uri && (s.step as any).uri === uri)[0]; - if (target) { - const targetX = target.headerRect.x + target.headerRect.width / 2 - left; - const targetY = target.headerRect.y + target.headerRect.height / 2 - top; - const gap = 100; - const add = 0.2; - - // right - if (targetX - fromX >= gap) { - const startX = fromX + r; - const startY = fromY; - const endX = targetX - r * 2 + 4; - const endY = targetY; - - const coefX = 24 + (i * add); - const coefY = (targetY > fromY) ? 24 : -24; - - const pointX1 = startX + coefX; - const pointY1 = startY; - const pointX2 = startX + coefX; - const pointY2 = startY + coefY; - - const pointLX = pointX1; - const pointLY = targetY - coefY; - - const pointX3 = pointLX; - const pointY3 = endY; - const pointX4 = pointLX + coefX; - const pointY4 = endY; - - return getInternalPath(key, className, startX, startY, pointX1, pointY1, pointX2, pointY2, pointLX, pointLY, pointX3, pointY3, pointX4, pointY4, endX, endY); - } else if (targetX > fromX && targetX - fromX < gap) { - const startX = fromX - r; - const startY = fromY; - const endX = targetX - r * 2 + 4; - const endY = targetY; - - const coefX = -24 - (i * add); - const coefY = (targetY > fromY) ? 24 : -24; - - const pointX1 = startX + coefX; - const pointY1 = startY; - const pointX2 = startX + coefX; - const pointY2 = startY + coefY; - - const pointLX = pointX1; - const pointLY = targetY - coefY; - - const pointX3 = pointLX; - const pointY3 = endY; - const pointX4 = pointLX - coefX/2; - const pointY4 = endY; - - return getInternalPath(key, className, startX, startY, pointX1, pointY1, pointX2, pointY2, pointLX, pointLY, pointX3, pointY3, pointX4, pointY4, endX, endY); - } else if (targetX <= fromX && fromX - targetX < gap) { - const startX = fromX + r; - const startY = fromY; - const endX = targetX + r * 2 - 4; - const endY = targetY; - - const coefX = 24 + (i * add); - const coefY = (targetY > fromY) ? 24 : -24; - - const pointX1 = startX + coefX; - const pointY1 = startY; - const pointX2 = startX + coefX; - const pointY2 = startY + coefY; - - const pointLX = pointX1; - const pointLY = targetY - coefY; - - const pointX3 = pointLX; - const pointY3 = endY; - const pointX4 = pointLX - coefX/2; - const pointY4 = endY; - - return getInternalPath(key, className, startX, startY, pointX1, pointY1, pointX2, pointY2, pointLX, pointLY, pointX3, pointY3, pointX4, pointY4, endX, endY); - } else { - const startX = fromX - r; - const startY = fromY; - const endX = targetX + r * 2 - 4; - const endY = targetY; - - const coefX = -24 - (i * add); - const coefY = (targetY > fromY) ? 24 : -24; - - const pointX1 = startX + coefX; - const pointY1 = startY; - const pointX2 = startX + coefX; - const pointY2 = startY + coefY; - - const pointLX = pointX1; - const pointLY = targetY - coefY; - - const pointX3 = pointLX; - const pointY3 = endY; - const pointX4 = pointLX + coefX; - const pointY4 = endY; - - return getInternalPath(key, className, startX, startY, pointX1, pointY1, pointX2, pointY2, pointLX, pointLY, pointX3, pointY3, pointX4, pointY4, endX, endY); - } - } - } - - function getInternalPath(key: string, className: string, startX: number, startY: number, pointX1: number, pointY1: number, pointX2: number, pointY2: number, pointLX: number, pointLY: number, - pointX3: number, pointY3: number, pointX4: number, pointY4: number, endX: number, endY: number) { - return ( - <g key={key}> - <path d={`M ${startX} ${startY} - Q ${pointX1} ${pointY1} ${pointX2} ${pointY2} L ${pointLX},${pointLY} - Q ${pointX3} ${pointY3} ${pointX4} ${pointY4} L ${endX},${endY}`} - className={className} markerEnd="url(#arrowhead)"/> - </g> - ) - } - function getCircle(pos: DslPosition) { const cx = pos.headerRect.x + pos.headerRect.width / 2 - left; const cy = pos.headerRect.y + pos.headerRect.height / 2 - top; const r = pos.headerRect.height / 2; return ( - <circle cx={cx} cy={cy} r={r} stroke="transparent" strokeWidth="3" fill="transparent" key={pos.step.uuid + "-circle"}/> + <circle cx={cx} cy={cy} r={r} stroke="transparent" strokeWidth="3" fill="transparent" + key={pos.step.uuid + "-circle"}/> ) } - function hasSteps (step: CamelElement): boolean { + function hasSteps(step: CamelElement): boolean { return (step.hasSteps() && !['FromDefinition'].includes(step.dslName)) || ['RouteDefinition', 'TryDefinition', 'ChoiceDefinition', 'SwitchDefinition'].includes(step.dslName); } @@ -393,18 +243,21 @@ export function DslConnections() { const endY = pos.headerRect.y - 9 - top; if (pos.parent) { const parent = steps.get(pos.parent.uuid); - if (parent) { + const showArrow = pos.prevStep !== undefined && !['TryDefinition', 'ChoiceDefinition'].includes(pos.prevStep.dslName); + const name = pos.prevStep?.dslName; + if (parent && showArrow) { const startX = parent.headerRect.x + parent.headerRect.width / 2 - left; const startY = parent.headerRect.y + parent.headerRect.height - top; if ((!pos.inSteps || (pos.inSteps && pos.position === 0)) && parent.step.dslName !== 'MulticastDefinition') { return ( - <path name={pos.step.dslName} d={`M ${startX},${startY} C ${startX},${endY} ${endX},${startY} ${endX},${endY}`} + <path name={pos.step.dslName} + d={`M ${startX},${startY} C ${startX},${endY} ${endX},${startY} ${endX},${endY}`} className="path" key={pos.step.uuid} markerEnd="url(#arrowhead)"/> ) } else if (parent.step.dslName === 'MulticastDefinition' && pos.inSteps) { return ( <path d={`M ${startX},${startY} C ${startX},${endY} ${endX},${startY} ${endX},${endY}`} - className="path" key={pos.step.uuid} markerEnd="url(#arrowhead)"/> + name={name} className="path" key={pos.step.uuid} markerEnd="url(#arrowhead)"/> ) } else if (pos.inSteps && pos.position > 0 && !hasSteps(pos.step)) { const prev = getPreviousStep(pos); @@ -413,7 +266,8 @@ export function DslConnections() { const prevX = r.x + r.width / 2 - left; const prevY = r.y + r.height - top; return ( - <line x1={prevX} y1={prevY} x2={endX} y2={endY} className="path" key={pos.step.uuid} markerEnd="url(#arrowhead)"/> + <line name={name} x1={prevX} y1={prevY} x2={endX} y2={endY} className="path" + key={pos.step.uuid} markerEnd="url(#arrowhead)"/> ) } } else if (pos.inSteps && pos.position > 0 && hasSteps(pos.step)) { @@ -423,7 +277,8 @@ export function DslConnections() { const prevX = r.x + r.width / 2 - left; const prevY = r.y + r.height - top; return ( - <line x1={prevX} y1={prevY} x2={endX} y2={endY} className="path" key={pos.step.uuid} markerEnd="url(#arrowhead)"/> + <line name={name} x1={prevX} y1={prevY} x2={endX} y2={endY} className="path" + key={pos.step.uuid} markerEnd="url(#arrowhead)"/> ) } } @@ -431,6 +286,55 @@ export function DslConnections() { } } + function getButtonArrow(btn: ButtonPosition) { + const rect1 = btn.rect; + const uuid = btn.nextstep.uuid; + const nextStep = steps.get(uuid); + const rect2 = nextStep?.rect; + if (rect1 && rect2) { + const startX = rect1.x + rect1.width / 2 - left; + const startY = rect1.y + rect1.height - top - 2; + const endX = rect2.x + rect2.width / 2 - left; + const endTempY = rect2.y - top - 9; + + const gapX = Math.abs(endX - startX); + const gapY = Math.abs(endTempY - startY); + + const radX = gapX > 30 ? 20 : gapX/2; + const radY = gapY > 30 ? 20 : gapY/2; + const endY = rect2.y - top - 9 - radY; + + const iRadX = startX > endX ? -1 * radX : radX; + const iRadY = startY > endY ? -1 * radY : radY; + + const LX1 = startX; + const LY1 = endY - radY; + + const Q1_X1 = startX; + const Q1_Y1 = LY1 + radY; + const Q1_X2 = startX + iRadX; + const Q1_Y2 = LY1 + radY; + + const LX2 = startX + (endX - startX) - iRadX; + const LY2 = LY1 + radY; + + const Q2_X1 = LX2 + iRadX; + const Q2_Y1 = endY; + const Q2_X2 = LX2 + iRadX; + const Q2_Y2 = endY + radY; + + const path = `M ${startX} ${startY}` + + ` L ${LX1} ${LY1} ` + + ` Q ${Q1_X1} ${Q1_Y1} ${Q1_X2} ${Q1_Y2}` + + ` L ${LX2} ${LY2}` + + ` Q ${Q2_X1} ${Q2_Y1} ${Q2_X2} ${Q2_Y2}` + // + ` L ${endX} ${endY}`; + return ( + <path key={btn.uuid} d={path} className="path" markerEnd="url(#arrowhead)"/> + ) + } + } + function getSvg() { const stepsArray = Array.from(steps.values()); return ( @@ -438,12 +342,14 @@ export function DslConnections() { style={{width: width, height: height, position: "absolute", left: 0, top: 0}} viewBox={"0 0 " + (width) + " " + (height)}> <defs> - <marker id="arrowhead" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto" className="arrow"> + <marker id="arrowhead" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto" + className="arrow"> <polygon points="0 0, 9 3, 0 6"/> </marker> </defs> {stepsArray.map(pos => getCircle(pos))} {stepsArray.map(pos => getArrow(pos))} + {buttons.map(btn => getButtonArrow(btn)).filter(b => b !== undefined)} {getIncomings().map(p => getIncoming(p))} {getOutgoings().map(p => getOutgoing(p))} {/*{getInternals().map((p) => getInternalLines(p)).flat()}*/} @@ -452,7 +358,7 @@ export function DslConnections() { } return ( - <div id="connections" className="connections" style={{ width: width, height: height}}> + <div id="connections" className="connections" style={{width: width, height: height}}> {getSvg()} {getIncomings().map(p => getIncomingIcons(p))} {getOutgoings().map(p => getOutgoingIcons(p))} diff --git a/karavan-designer/src/designer/route/DslElement.css b/karavan-designer/src/designer/route/DslElement.css deleted file mode 100644 index d3ed7177..00000000 --- a/karavan-designer/src/designer/route/DslElement.css +++ /dev/null @@ -1,18 +0,0 @@ -.disabled { - opacity: 0.5; -} -.menu-button { - position: absolute; - top: 26px; - line-height: 1; - border: 0; - padding: 0; - margin: 0 0 0 10px; - background: transparent; - color: var(--pf-v5-global--primary-color--100); - visibility: hidden; -} - -.dsl-element:hover .menu-button { - visibility: visible; -} diff --git a/karavan-designer/src/designer/route/DslProperties.css b/karavan-designer/src/designer/route/DslProperties.css new file mode 100644 index 00000000..7040fd40 --- /dev/null +++ b/karavan-designer/src/designer/route/DslProperties.css @@ -0,0 +1,250 @@ +/* + * 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 .properties .headers { + grid-row-gap: 10px; + row-gap: 10px; + display: contents; +} + +.karavan .properties .headers .top { + width: 100%; + display: flex; + flex-direction: row; +} + +.karavan .properties .headers .top h1 { + width: 100%; + margin-top: auto; + margin-bottom: auto; +} + +.karavan .properties .footer { + height: 100%; + display: contents; +} + +.karavan .properties .pf-v5-c-form { + row-gap: 10px; +} + +.karavan .properties .pf-v5-c-form__group-label { + padding-bottom: 3px; + display: flex; + justify-content: space-between; +} + +.karavan .properties .pf-v5-c-form-control:focus-within { + --pf-v5-c-form-control--after--BorderBottomColor: var(--pf-v5-c-form-control--after--BorderBottomColor); + --pf-v5-c-form-control--after--BorderBottomWidth: var(--pf-v5-c-form-control--after--BorderBottomWidth); +} + +.karavan .properties .pf-v5-c-select { + --pf-v5-c-select__toggle--FontSize: 14px; + --pf-v5-c-select__menu-item--FontSize: 14px; +} + +.karavan .properties .input-group .pf-v5-c-text-input-group__text { + width: 100%; +} + +.karavan .properties .input-group .pf-v5-c-chip-group, +.karavan .properties .input-group .pf-v5-c-text-input-group__main, +.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip-group__list, +.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip-group__list .pf-v5-c-chip-group__list-item, +.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip-group__main { + display: block; +} + +.karavan .properties .input-group .pf-v5-c-chip-group { + margin-left: 0; +} + +.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip .pf-v5-c-chip__text { + max-width: inherit; +} + +.karavan .properties .input-group .pf-v5-c-chip-group .pf-v5-c-chip { + width: 100%; +} + +.karavan .properties .input-group .pf-v5-c-text-input-group__utilities { + align-items: end; + margin-top: auto; +} + +.karavan .properties .chip .pf-v5-c-button { + position: absolute; + right: 0; +} + +.karavan .properties .expression-title { + font-size: 17px; + font-weight: bold; +} + +.karavan .properties .text-area { + font-size: 13px; +} + +.karavan .properties .pf-v5-c-select__menu-search { + padding: 0px 6px 6px 6px; +} + +.karavan .properties .pf-v5-c-select__toggle-typeahead { + height: auto; +} + +.karavan .properties .pf-v5-c-select__menu-item { + /*width: 280px;*/ +} + +.karavan .properties .pf-v5-c-select__menu-item-description { + overflow-wrap: anywhere; +} + +.karavan .properties .number { + display: flex; + justify-content: space-between; +} + +.karavan .properties .number .number-property { + width: 100%; +} + +.karavan .properties .number .clear-button { + color: #b1b1b7; + --pf-v5-c-button--BorderRadius: var(--pf-v5-c-button--m-control--BorderRadius); + --pf-v5-c-button--disabled--BackgroundColor: var(--pf-v5-c-button--m-control--disabled--BackgroundColor); + --pf-v5-c-button--after--BorderWidth: var(--pf-v5-c-button--m-control--after--BorderWidth); + --pf-v5-c-button--after--BorderColor: var(--pf-v5-c-button--m-control--after--BorderTopColor) var(--pf-v5-c-button--m-control--after--BorderRightColor) var(--pf-v5-c-button--m-control--after--BorderBottomColor) var(--pf-v5-c-button--m-control--after--BorderLeftColor); + color: var(--pf-v5-c-button--m-control--Color); + background-color: var(--pf-v5-c-button--m-control--BackgroundColor); + padding-left: 5px; + padding-right: 5px; +} + +.karavan .properties .help-button { + font-size: 12px; +} + +.karavan .properties .component-selector { + border-width: var(--pf-v5-global--BorderWidth--sm); + border-top-color: var(--pf-v5-global--BorderColor--300); + border-right-color: var(--pf-v5-global--BorderColor--300); + border-bottom-color: var(--pf-v5-global--BorderColor--200); + border-left-color: var(--pf-v5-global--BorderColor--300); + border-style: solid; +} + +.karavan .properties .object-value { + display: flex; + flex-direction: row; + margin-bottom: 3px; +} + +.karavan .properties .object-key-value .object { + padding-top: 0; +} + +.karavan .properties .object-value .delete-button { + margin: 0; + padding: 5px 3px 0px 6px; + height: 16px; + color: #909090; +} + +.karavan .properties .object-key-value, +.karavan .properties .object-key-value .object-field { + display: flex; + flex-direction: row; + gap: 3px; +} + +.karavan .properties .object-key-value .delete-button { + margin: auto 0 22px 0; + padding: 0px 0px 0px 3px; + height: 16px; + color: #909090; +} + +.karavan .properties .expression, +.karavan .properties .object, +.karavan .properties .dataformat, +.karavan .properties .parameters { + padding-top: 6px; + padding-left: 16px; + row-gap: 6px; + display: grid; + width: 100%; +} + +.karavan .properties .expression .pf-v5-c-form__group-label, +.karavan .properties .object .pf-v5-c-form__group-label, +.karavan .properties .dataformat .pf-v5-c-form__group-label, +.karavan .properties .parameters .pf-v5-c-form__group-label { + font-weight: 100; +} + +.karavan .properties .expression .pf-v5-c-form__group, +.karavan .properties .object .pf-v5-c-form__group, +.karavan .properties .dataformat .pf-v5-c-form__group, +.karavan .properties .parameters .pf-v5-c-form__group { + margin-bottom: 10px; +} + +.karavan .properties .expression .pf-v5-c-select__menu-wrapper, +.karavan .properties .object .pf-v5-c-select__menu-wrapper, +.karavan .properties .dataformat .pf-v5-c-select__menu-wrapper, +.karavan .properties .parameters .pf-v5-c-select__menu-wrapper { + width: 350px; +} + +.karavan .properties .change-button { + font-size: 15px; + height: 15px; + line-height: 1; + border: 0; + padding: 0; + margin: auto auto auto 6px; + background: transparent; +} + +.karavan .properties .change-button svg { + margin-right: 6px; +} + +.karavan .properties .add-button { + color: var(--pf-v5-global--active-color--100); +} + +.karavan .properties .delete-button { + color: #909090; +} + +.karavan .properties .pf-v5-c-expandable-section__toggle { + margin: 0; + padding: 0; +} + +.karavan .properties .pf-v5-c-expandable-section__content { + margin: 0; +} + +.karavan .properties .pf-v5-c-expandable-section__content p { + min-height: 6px; +} + diff --git a/karavan-designer/src/designer/route/DslProperties.tsx b/karavan-designer/src/designer/route/DslProperties.tsx index 8231759c..b958b2b3 100644 --- a/karavan-designer/src/designer/route/DslProperties.tsx +++ b/karavan-designer/src/designer/route/DslProperties.tsx @@ -22,6 +22,7 @@ import { TextVariants, ExpandableSection, Button, Tooltip, } from '@patternfly/react-core'; import '../karavan.css'; +import './DslProperties.css'; import "@patternfly/patternfly/patternfly.css"; import {DataFormatField} from "./property/DataFormatField"; import {DslPropertyField} from "./property/DslPropertyField"; @@ -30,6 +31,7 @@ import {CamelUi} from "../utils/CamelUi"; import {CamelMetadataApi, DataFormats, PropertyMeta} from "karavan-core/lib/model/CamelMetadata"; import {IntegrationHeader} from "../utils/IntegrationHeader"; import CloneIcon from "@patternfly/react-icons/dist/esm/icons/clone-icon"; +import ConvertIcon from "@patternfly/react-icons/dist/esm/icons/optimize-icon"; import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; import {usePropertiesHook} from "./usePropertiesHook"; @@ -43,7 +45,8 @@ export function DslProperties(props: Props) { const [integration] = useIntegrationStore((state) => [state.integration], shallow) - const {cloneElement, onDataFormatChange, onPropertyChange, onParametersChange, onExpressionChange} = usePropertiesHook(props.isRouteDesigner); + const {convertStep, cloneElement, onDataFormatChange, onPropertyChange, onParametersChange, onExpressionChange} = + usePropertiesHook(props.isRouteDesigner); const [selectedStep, dark] = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) @@ -54,10 +57,26 @@ export function DslProperties(props: Props) { const title = selectedStep && CamelDisplayUtil.getTitle(selectedStep) const description = selectedStep && CamelUi.getDescription(selectedStep); const descriptionLines: string [] = description ? description?.split("\n") : [""]; + const targetDsl = CamelUi.getConvertTargetDsl(selectedStep?.dslName); + const targetDslTitle = targetDsl?.replace("Definition", ""); return ( <div className="headers"> <div className="top"> <Title headingLevel="h1" size="md">{title}</Title> + {targetDsl && + <Button + variant={"link"} + icon={<ConvertIcon/>} + iconPosition={"right"} + onClick={event => { + if (selectedStep) { + convertStep(selectedStep, targetDsl); + } + }} + > + Convert to {targetDslTitle} + </Button> + } </div> <Text component={TextVariants.p}>{descriptionLines.at(0)}</Text> {descriptionLines.length > 1 && diff --git a/karavan-designer/src/designer/route/RouteDesigner.tsx b/karavan-designer/src/designer/route/RouteDesigner.tsx index c57260db..b6b1c942 100644 --- a/karavan-designer/src/designer/route/RouteDesigner.tsx +++ b/karavan-designer/src/designer/route/RouteDesigner.tsx @@ -27,7 +27,7 @@ import {DslSelector} from "./DslSelector"; import {DslProperties} from "./DslProperties"; import {DslConnections} from "./DslConnections"; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; -import {DslElement} from "./DslElement"; +import {DslElement} from "./element/DslElement"; import {CamelUi} from "../utils/CamelUi"; import {useRouteDesignerHook} from "./useRouteDesignerHook"; import {useConnectionsStore, useDesignerStore, useIntegrationStore, useSelectorStore} from "../DesignerStore"; @@ -36,7 +36,7 @@ import useResizeObserver from "./useResizeObserver"; import {Command, EventBus} from "../utils/EventBus"; import useMutationsObserver from "./useDrawerMutationsObserver"; import {DeleteConfirmation} from "./DeleteConfirmation"; -import {DslElementMoveModal} from "./DslElementMoveModal"; +import {DslElementMoveModal} from "./element/DslElementMoveModal"; export function RouteDesigner() { @@ -152,15 +152,21 @@ export function RouteDesigner() { inSteps={false} position={index} step={routeConfiguration} + nextStep={undefined} + prevStep={undefined} parent={undefined}/> ))} - {routes?.map((route: any, index: number) => ( - <DslElement key={route.uuid} - inSteps={false} - position={index} - step={route} - parent={undefined}/> - ))} + {routes?.map((route: any, index: number) => { + return ( + <DslElement key={route.uuid} + inSteps={false} + position={index} + step={route} + nextStep={undefined} + prevStep={undefined} + parent={undefined}/> + ) + })} {getGraphButtons()} </div> </div>) diff --git a/karavan-designer/src/designer/route/element/DslElement.css b/karavan-designer/src/designer/route/element/DslElement.css new file mode 100644 index 00000000..85f7b46a --- /dev/null +++ b/karavan-designer/src/designer/route/element/DslElement.css @@ -0,0 +1,220 @@ +/* + * 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 .step-element .header-route { + display: block; + border: none; + background: transparent; + padding: 0px 0px 0px 0px; + margin-bottom: 10px; + min-width: 160px; + z-index: 101; +} + +.karavan .step-element .header-route .delete-button { + position: absolute; + top: 4px; + right: 4px; + line-height: 1; + border: 0; + padding: 0; + margin: 0; + background: transparent; + color: #909090; + visibility: hidden; +} + +.karavan .step-element .header .delete-button, +.element-builder .header .delete-button { + position: absolute; + top: -8px; + line-height: 1; + border: 0; + padding: 0; + margin: 0 0 0 10px; + background: transparent; + color: #909090; + visibility: hidden; +} + +/*.karavan .step-element:hover .delete-button,*/ +/*.karavan .step-element:hover .delete-button,*/ +.karavan .step-element .header:hover .delete-button, +.karavan .step-element .header-route:hover .delete-button, +.element-builder .header:hover .delete-button { + visibility: visible; +} + +.modal-delete { + width: 350px !important; +} + +.karavan .step-element .header { + height: 50px; +} + +.karavan .step-element-selected { + background-color: rgba(var(--pf-v5-global--palette--blue-50), 1); +} + +.karavan .step-element .selected .header-icon { + border-color: var(--pf-v5-global--primary-color--100); + background-color: var(--pf-v5-global--palette--blue-50); + border-width: 2px; +} + +.karavan .step-element .header .header-text { + position: absolute; + top: 8px; + left: 0; + width: 100%; + display: flex; + flex-direction: row; +} + +.karavan .step-element .header .spacer { + width: 50% +} + +.karavan .step-element .header .text-bottom { + background-color: rgba(255, 255, 255, 0.5); +} + +.karavan .step-element .header .text-right { + padding-left: 17px; + width: 50%; + text-align: start; +} + +.karavan .step-element .header .header-text-required { + color: var(--pf-v5-global--danger-color--100); + font-weight: bold; +} + +.karavan .step-element .header-icon { + border-color: var(--pf-v5-global--Color--200); + border-style: solid; + border-radius: 30px; + border-width: 1px; + background: white; + width: 30px; + height: 30px; + margin: auto; + display: flex; + justify-content: center; + align-items: center; +} + +.karavan .step-element .header .icon, +.element-builder .header .icon { + height: 20px; + width: auto; + border: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.karavan .step-element .add-element-button { + top: 5px; + left: 5px; + font-size: 15px; + height: 15px; + line-height: 1; + border: 0; + padding: 0; + background: transparent; + color: var(--pf-v5-global--primary-color--100); + visibility: hidden; + z-index: 100; + position: absolute; +} + +.karavan .step-element .insert-element-button { + position: absolute; + top: -8px; + line-height: 1; + border: 0; + padding: 0; + margin: 0 0 0 -30px; + background: transparent; + color: var(--pf-v5-global--primary-color--100); + visibility: hidden; + z-index: 100; +} + +.karavan .step-element .header:hover .insert-element-button { + visibility: visible; +} + +.karavan .step-element .add-button-bottom { + position: relative; +} + +.karavan .step-element .add-button-left { + position: absolute; + top: 4px; + left: 4px; +} + +.karavan .step-element .add-button { + font-size: 15px; + height: 15px; + line-height: 1; + border: 0; + padding: 0; + margin: 0; + background: transparent; + color: var(--pf-v5-global--primary-color--100); + visibility: hidden; + z-index: 100; +} + +.disabled { + opacity: 0.5; +} +.menu-button { + position: absolute; + top: 26px; + line-height: 1; + border: 0; + padding: 0; + margin: 0 0 0 10px; + background: transparent; + color: var(--pf-v5-global--primary-color--100); + visibility: hidden; +} + +.dsl-element:hover .menu-button { + visibility: visible; +} + +.add-button-icon, .insert-button-icon { + fill: var(--pf-v5-global--primary-color--100); + width: 20px; + height: 20px; + background: white; +} + +.delete-button-icon { + fill: var(--pf-v5-global--danger-color--100); + width: 20px; + height: 20px; + background: white; +} \ No newline at end of file diff --git a/karavan-designer/src/designer/route/DslElement.tsx b/karavan-designer/src/designer/route/element/DslElement.tsx similarity index 79% rename from karavan-designer/src/designer/route/DslElement.tsx rename to karavan-designer/src/designer/route/element/DslElement.tsx index e3dfdbfc..ce780636 100644 --- a/karavan-designer/src/designer/route/DslElement.tsx +++ b/karavan-designer/src/designer/route/element/DslElement.tsx @@ -15,27 +15,25 @@ * limitations under the License. */ import React, {CSSProperties, useMemo, useState} from 'react'; -import {Menu, MenuContent, MenuItem, MenuList, Popover, Text, Tooltip,} from '@patternfly/react-core'; -import '../karavan.css'; +import {Text, Tooltip,} from '@patternfly/react-core'; +import '../../karavan.css'; import './DslElement.css'; -import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; -import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-circle-icon"; -import SyncIcon from "@patternfly/react-icons/dist/js/icons/sync-icon"; -import TurnIcon from "@patternfly/react-icons/dist/js/icons/chevron-circle-right-icon"; -import InsertIcon from "@patternfly/react-icons/dist/js/icons/arrow-alt-circle-right-icon"; import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition"; -import {CamelUi} from "../utils/CamelUi"; -import {EventBus} from "../utils/EventBus"; +import {CamelUi} from "../../utils/CamelUi"; +import {EventBus} from "../../utils/EventBus"; import {ChildElement, CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt"; import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil"; -import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; +import {useDesignerStore, useIntegrationStore} from "../../DesignerStore"; import {shallow} from "zustand/shallow"; -import {useRouteDesignerHook} from "./useRouteDesignerHook"; +import {useRouteDesignerHook} from "../useRouteDesignerHook"; +import {AddElementIcon, DeleteElementIcon, InsertElementIcon} from "./DslElementIcons"; interface Props { step: CamelElement, parent: CamelElement | undefined, + nextStep: CamelElement | undefined, + prevStep: CamelElement | undefined, inSteps: boolean position: number } @@ -43,7 +41,16 @@ interface Props { export function DslElement(props: Props) { const headerRef = React.useRef<HTMLDivElement>(null); - const {selectElement, moveElement, onShowDeleteConfirmation, openSelector, isKamelet, isSourceKamelet, isActionKamelet} = useRouteDesignerHook(); + const addButtonRef = React.useRef<HTMLDivElement>(null); + const { + selectElement, + moveElement, + onShowDeleteConfirmation, + openSelector, + isKamelet, + isSourceKamelet, + isActionKamelet + } = useRouteDesignerHook(); const [integration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) @@ -98,12 +105,20 @@ export function DslElement(props: Props) { } function hasBorder(): boolean { - return (props.step?.hasSteps() && !['FromDefinition'].includes(props.step.dslName)) - || ['RouteConfigurationDefinition', - 'RouteDefinition', - 'TryDefinition', - 'ChoiceDefinition', - 'SwitchDefinition'].includes(props.step.dslName); + const step = props.step; + if (['FilterDefinition'].includes(step.dslName)) { + return true; + } + if (['FromDefinition', + 'RouteDefinition', + 'TryDefinition', + 'CatchDefinition', 'FinallyDefinition', + 'ChoiceDefinition', + 'SwitchDefinition', 'WhenDefinition', 'OtherwiseDefinition' + ].includes(step.dslName)) { + return false; + } + return props.step?.hasSteps(); } function isNotDraggable(): boolean { @@ -177,7 +192,26 @@ export function DslElement(props: Props) { return style; } + function sendButtonPosition(el: HTMLButtonElement | null) { + const {nextStep, step, parent} = props; + let needArrow = !hasBorder() && !['ChoiceDefinition', 'MulticastDefinition', 'TryDefinition'].includes(step.dslName); + + if (parent + && ['TryDefinition'].includes(parent.dslName) + && !['CatchDefinition', 'FinallyDefinition'].includes(step.dslName)) { + needArrow = true; + } + + if (el && nextStep && needArrow) { + const rect = headerRef.current?.getBoundingClientRect(); + + if (rect) + EventBus.sendButtonPosition("add", step.uuid, nextStep, rect); + } + } + function sendPosition(el: HTMLDivElement | null) { + const {step, prevStep, parent} = props; const isSelected = isElementSelected(); const isHidden = isElementHidden(); if (el) { @@ -186,15 +220,14 @@ export function DslElement(props: Props) { const headerIcon: any = Array.from(header.childNodes.values()).filter((n: any) => n.classList.contains("header-icon"))[0]; const headerRect = headerIcon.getBoundingClientRect(); const rect = el.getBoundingClientRect(); - if (props.step.showChildren) { + if (step.showChildren) { if (isHidden) { - // EventBus.sendPosition("delete", props.step, props.parent, new DOMRect(), new DOMRect(), 0); - EventBus.sendPosition("add", props.step, props.parent, rect, headerRect, props.position, props.inSteps, isSelected); + EventBus.sendPosition("add", step, prevStep, parent, rect, headerRect, props.position, props.inSteps, isSelected); } else { - EventBus.sendPosition("add", props.step, props.parent, rect, headerRect, props.position, props.inSteps, isSelected); + EventBus.sendPosition("add", step, prevStep, parent, rect, headerRect, props.position, props.inSteps, isSelected); } } else { - EventBus.sendPosition("delete", props.step, props.parent, new DOMRect(), new DOMRect(), 0); + EventBus.sendPosition("delete", step, prevStep, parent, new DOMRect(), new DOMRect(), 0); } } } @@ -237,7 +270,6 @@ export function DslElement(props: Props) { </div> {showInsertButton && getInsertElementButton()} {getDeleteButton()} - {/*{getMenuButton()}*/} {showAddButton && getAddElementButton()} </div> ) @@ -304,7 +336,7 @@ export function DslElement(props: Props) { const step = props.step; const isBorder = child.name === 'steps' && hasBorderOverSteps(step); const style: CSSProperties = { - borderStyle: isBorder ? "dotted" : "none", + // borderStyle: isBorder ? "dotted" : "none", borderColor: "var(--step-border-color)", borderWidth: "1px", borderRadius: "16px", @@ -343,15 +375,25 @@ export function DslElement(props: Props) { return ( <div className={child.name + " has-child"} style={getChildrenElementsStyle(child, notOnlySteps)} key={step.uuid + "-child-" + index}> - {children.map((element, index) => ( - <div key={step.uuid + child.className + index}> - <DslElement - inSteps={child.name === 'steps'} - position={index} - step={element} - parent={step}/> - </div> - ))} + {children.map((element, index) => { + let prevStep = children.at(index - 1); + let nextStep = undefined; + if (['TryDefinition', 'ChoiceDefinition'].includes(step.dslName)) { + nextStep = props.nextStep; + } else { + nextStep = children.at(index + 1); + } + return (<div key={step.uuid + child.className + index}> + <DslElement + inSteps={child.name === 'steps'} + position={index} + step={element} + nextStep={nextStep} + prevStep={prevStep} + parent={step}/> + </div>) + } + )} {child.name === 'steps' && getAddStepButton()} </div> ) @@ -366,16 +408,23 @@ export function DslElement(props: Props) { } function getAddStepButton() { - const {step} = props; + const {step, nextStep} = props; const hideAddButton = step.dslName === 'StepDefinition' && !CamelDisplayUtil.isStepDefinitionExpanded(integration, step.uuid, selectedUuids.at(0)); if (hideAddButton) return (<></>) else return ( <Tooltip position={"bottom"} - 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"}> - <AddIcon/> - </button> + content={<div>{nextStep?.dslName.replace("Definition", "")}</div>} + // content={<div>{"Add step to " + CamelDisplayUtil.getTitle(step)}</div>} + > + <div ref={addButtonRef}> + <button type="button" + ref={el => sendButtonPosition(el)} + aria-label="Add" + onClick={e => onOpenSelector(e)} + className={isAddStepButtonLeft() ? "add-button add-button-left" : "add-button add-button-bottom"}> + <AddElementIcon/> + </button> + </div> </Tooltip> ) } @@ -389,7 +438,7 @@ export function DslElement(props: Props) { aria-label="Add" onClick={e => onOpenSelector(e, false)} className={"add-element-button"}> - <AddIcon/> + <AddElementIcon/> </button> </Tooltip> ) @@ -398,8 +447,11 @@ export function DslElement(props: Props) { function getInsertElementButton() { return ( <Tooltip position={"left"} content={<div>{"Insert element before"}</div>}> - <button type="button" aria-label="Insert" onClick={e => onOpenSelector(e, true, true)} - className={"insert-element-button"}><InsertIcon/> + <button type="button" + aria-label="Insert" + onClick={e => onOpenSelector(e, true, true)} + className={"insert-element-button"}> + <InsertElementIcon/> </button> </Tooltip> ) @@ -409,39 +461,12 @@ export function DslElement(props: Props) { return ( <Tooltip position={"right"} content={<div>{"Delete element"}</div>}> <button type="button" aria-label="Delete" onClick={e => onDeleteElement(e)} className="delete-button"> - <DeleteIcon/> + <DeleteElementIcon/> </button> </Tooltip> ) } - function getMenuButton() { - return ( - <Popover - aria-label="Convert Popover" - hasNoPadding - position={"right"} - hideOnOutsideClick={true} - showClose={false} - bodyContent={ - <Menu activeItemId={''} onSelect={event => {}} isPlain> - <MenuContent> - <MenuList> - <MenuItem itemId={0} icon={<SyncIcon aria-hidden />}>Convert to SetHeader</MenuItem> - {/*<MenuItem itemId={1}>Action</MenuItem>*/} - {/*<MenuItem itemId={2}>Action</MenuItem>*/} - </MenuList> - </MenuContent> - </Menu> - } - > - <button type="button" aria-label="Menu" onClick={e => {}} className="menu-button"> - <TurnIcon/> - </button> - </Popover> - ) - } - const element: CamelElement = props.step; const className = "step-element" + (isElementSelected() ? " step-element-selected" : "") + (!props.step.showChildren ? " hidden-step" : "") @@ -451,7 +476,7 @@ export function DslElement(props: Props) { className={className} ref={el => sendPosition(el)} style={{ - borderStyle: hasBorder() ? "dotted" : "none", + borderStyle: hasBorder() ? "dashed" : "none", borderColor: isElementSelected() ? "var(--step-border-color-selected)" : "var(--step-border-color)", marginTop: isInStepWithChildren() ? "16px" : "8px", zIndex: element.dslName === 'ToDefinition' ? 20 : 10, diff --git a/karavan-designer/src/designer/route/element/DslElementIcons.tsx b/karavan-designer/src/designer/route/element/DslElementIcons.tsx new file mode 100644 index 00000000..3b6b984b --- /dev/null +++ b/karavan-designer/src/designer/route/element/DslElementIcons.tsx @@ -0,0 +1,80 @@ +/* + * 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 "./DslElement.css" + +export function DeleteElementIcon() { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + xmlSpace="preserve" + viewBox="0 0 32 32" + className="delete-button-icon" + > + <path d="M16 2C8.2 2 2 8.2 2 16s6.2 14 14 14 14-6.2 14-14S23.8 2 16 2zm0 26C9.4 28 4 22.6 4 16S9.4 4 16 4s12 5.4 12 12-5.4 12-12 12z" /> + <path + d="M0 0h32v32H0z" + style={{ + fill: "none", + }} + /> + <path d="M21.4 23 16 17.6 10.6 23 9 21.4l5.4-5.4L9 10.6 10.6 9l5.4 5.4L21.4 9l1.6 1.6-5.4 5.4 5.4 5.4z" /> + </svg> + ) +} + +export function AddElementIcon() { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + xmlSpace="preserve" + viewBox="0 0 32 32" + className="add-button-icon" + > + <path + d="M16 4c6.6 0 12 5.4 12 12s-5.4 12-12 12S4 22.6 4 16 9.4 4 16 4m0-2C8.3 2 2 8.3 2 16s6.3 14 14 14 14-6.3 14-14S23.7 2 16 2z"/> + <path d="M24 15h-7V8h-2v7H8v2h7v7h2v-7h7z"/> + <path + d="M0 0h32v32H0z" + style={{ + fill: "none", + }} + /> + </svg> + ) +} + +export function InsertElementIcon() { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + xmlSpace="preserve" + viewBox="0 0 32 32" + className="insert-button-icon" + > + <path d="m16 8-1.43 1.393L20.15 15H8v2h12.15l-5.58 5.573L16 24l8-8-8-8z" /> + <path d="M16 30a14 14 0 1 1 14-14 14.016 14.016 0 0 1-14 14Zm0-26a12 12 0 1 0 12 12A12.014 12.014 0 0 0 16 4Z" /> + <path + d="M0 0h32v32H0z" + data-name="<Transparent Rectangle>" + style={{ + fill: "none", + }} + /> + </svg> + ) +} \ No newline at end of file diff --git a/karavan-designer/src/designer/route/DslElementMoveModal.tsx b/karavan-designer/src/designer/route/element/DslElementMoveModal.tsx similarity index 94% rename from karavan-designer/src/designer/route/DslElementMoveModal.tsx rename to karavan-designer/src/designer/route/element/DslElementMoveModal.tsx index d1e17ecf..75d4f58f 100644 --- a/karavan-designer/src/designer/route/DslElementMoveModal.tsx +++ b/karavan-designer/src/designer/route/element/DslElementMoveModal.tsx @@ -20,10 +20,10 @@ import { Flex, Modal, ModalVariant, } from '@patternfly/react-core'; -import '../karavan.css'; -import {useDesignerStore, useIntegrationStore} from "../DesignerStore"; +import '../../karavan.css'; +import {useDesignerStore, useIntegrationStore} from "../../DesignerStore"; import {shallow} from "zustand/shallow"; -import {useRouteDesignerHook} from "./useRouteDesignerHook"; +import {useRouteDesignerHook} from "../useRouteDesignerHook"; import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt"; export function DslElementMoveModal() { diff --git a/karavan-designer/src/designer/route/usePropertiesHook.tsx b/karavan-designer/src/designer/route/usePropertiesHook.tsx index 87513b18..a9bd9ef9 100644 --- a/karavan-designer/src/designer/route/usePropertiesHook.tsx +++ b/karavan-designer/src/designer/route/usePropertiesHook.tsx @@ -126,5 +126,14 @@ export function usePropertiesHook (isRouteDesigner: boolean = true) { // TODO: } - return {cloneElement, onPropertyChange, onParametersChange, onDataFormatChange, onExpressionChange, getInternalComponentName} + const convertStep = (step: CamelElement, targetDslName: string ) => { + // const clone = CamelUtil.cloneIntegration(integration); + // const i = CamelDefinitionApiExt.addStepToIntegration(clone, step, parentId, position); + // const selectedStep = step.dslName === 'RouteDefinition' ? (step as RouteDefinition).from : step; + // setIntegration(i, false); + // setSelectedStep(selectedStep); + // setSelectedUuids([selectedStep.uuid]); + } + + return {convertStep, cloneElement, onPropertyChange, onParametersChange, onDataFormatChange, onExpressionChange, getInternalComponentName} } \ No newline at end of file diff --git a/karavan-designer/src/designer/route/useRouteDesignerHook.tsx b/karavan-designer/src/designer/route/useRouteDesignerHook.tsx index f48fc850..b65966a0 100644 --- a/karavan-designer/src/designer/route/useRouteDesignerHook.tsx +++ b/karavan-designer/src/designer/route/useRouteDesignerHook.tsx @@ -105,7 +105,8 @@ export function useRouteDesignerHook () { } const deleteElement = () => { - EventBus.sendPosition("clean", new CamelElement(""), undefined, new DOMRect(), new DOMRect(), 0); + EventBus.sendPosition("clean", new CamelElement(""), undefined, undefined, new DOMRect(), new DOMRect(), 0); + EventBus.sendButtonPosition("clean", '', new CamelElement(""), new DOMRect()); let i = integration; selectedUuids.forEach(uuidToDelete => { i = CamelDefinitionApiExt.deleteStepFromIntegration(i, uuidToDelete); diff --git a/karavan-designer/src/designer/utils/CamelUi.tsx b/karavan-designer/src/designer/utils/CamelUi.tsx index ec4d5796..83476b1b 100644 --- a/karavan-designer/src/designer/utils/CamelUi.tsx +++ b/karavan-designer/src/designer/utils/CamelUi.tsx @@ -166,8 +166,17 @@ export class RouteToCreate { const INTEGRATION_PATTERNS = 'Integration Patterns'; const connectorNavs = ['routing', "transformation", "error", "configuration", "endpoint", "kamelet", "component"]; +const stepConvertMap = new Map<string, string>([ + ["SetBodyDefinition", "SetHeaderDefinition"], + ["SetHeaderDefinition", "SetBodyDefinition"], +]); + export class CamelUi { + static getConvertTargetDsl = (sourceDsl?: string): string | undefined => { + return sourceDsl ? stepConvertMap.get(sourceDsl) : undefined; + } + static createNewInternalRoute = (uri: string): RouteToCreate | undefined => { const uris = uri.toString().split(":"); const componentName = uris[0]; diff --git a/karavan-designer/src/designer/utils/EventBus.ts b/karavan-designer/src/designer/utils/EventBus.ts index 0a20d3c6..55906f0f 100644 --- a/karavan-designer/src/designer/utils/EventBus.ts +++ b/karavan-designer/src/designer/utils/EventBus.ts @@ -18,10 +18,26 @@ import {Subject} from 'rxjs'; import {CamelElement, Integration} from "karavan-core/lib/model/IntegrationDefinition"; import {v4 as uuidv4} from "uuid"; -const positions = new Subject<DslPosition>(); +export class ButtonPosition { + uuid: string = ''; + nextstep: CamelElement = new CamelElement(""); + rect: DOMRect = new DOMRect(); + command: "add" | "delete" | "clean" = "add"; + + constructor(command: "add" | "delete" | "clean", + uuid: string, + nextstep: CamelElement, + rect: DOMRect) { + this.uuid = uuid; + this.command = command; + this.nextstep = nextstep; + this.rect = rect; + } +} export class DslPosition { step: CamelElement = new CamelElement(""); + prevStep: CamelElement | undefined; parent: CamelElement | undefined; inSteps: boolean = false; isSelected: boolean = false; @@ -32,6 +48,7 @@ export class DslPosition { constructor(command: "add" | "delete" | "clean", step: CamelElement, + prevStep: CamelElement | undefined, parent:CamelElement | undefined, rect: DOMRect, headerRect:DOMRect, @@ -40,6 +57,7 @@ export class DslPosition { isSelected: boolean = false) { this.command = command; this.step = step; + this.prevStep = prevStep; this.parent = parent; this.rect = rect; this.headerRect = headerRect; @@ -85,17 +103,25 @@ export class ToastMessage { this.variant = variant; } } +const dslPositions = new Subject<DslPosition>(); +const buttonPositions = new Subject<ButtonPosition>(); export const EventBus = { sendPosition: (command: "add" | "delete" | "clean", step: CamelElement, + prevStep: CamelElement | undefined, parent: CamelElement | undefined, rect: DOMRect, headerRect: DOMRect, position: number, inSteps: boolean = false, - isSelected: boolean = false) => positions.next(new DslPosition(command, step, parent, rect, headerRect, position, inSteps, isSelected)), - onPosition: () => positions.asObservable(), + isSelected: boolean = false) => dslPositions.next(new DslPosition(command, step, prevStep, parent, rect, headerRect, position, inSteps, isSelected)), + onPosition: () => dslPositions.asObservable(), + + sendButtonPosition: (command: "add" | "delete" | "clean", uuid: string, + nextStep: CamelElement, + rect: DOMRect) => buttonPositions.next(new ButtonPosition(command, uuid, nextStep, rect)), + onButtonPosition: () => buttonPositions.asObservable(), sendIntegrationUpdate: (i: Integration, propertyOnly: boolean) => updates.next(new IntegrationUpdate(i, propertyOnly)), onIntegrationUpdate: () => updates.asObservable(),