karavan-space/src/designer/route/DslConnections.tsx (439 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, {JSX, useEffect, useState} from 'react';
import './DslConnections.css';
import {DslPosition, EventBus} from "../utils/EventBus";
import {CamelUi} from "../utils/CamelUi";
import {useConnectionsStore, useDesignerStore, useIntegrationStore} from "../DesignerStore";
import {shallow} from "zustand/shallow";
import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt";
import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition";
import {v4 as uuidv4} from "uuid";
import {Button, Tooltip} from "@patternfly/react-core";
import {InfrastructureAPI} from "../utils/InfrastructureAPI";
import {getIntegrations} from "../../topology/TopologyApi";
import {ComponentApi} from "karavan-core/lib/api/ComponentApi";
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() {
const [integration, files] = useIntegrationStore((s) => [s.integration, s.files], shallow)
const [width, height, top, left] = useDesignerStore((s) =>
[s.width, s.height, s.top, s.left], shallow)
const [steps, addStep, deleteStep, clearSteps] =
useConnectionsStore((s) => [s.steps, s.addStep, s.deleteStep, s.clearSteps], shallow)
const [svgKey, setSvgKey] = useState<string>('svgKey');
const [tons, setTons] = useState<Map<string, string[]>>(new Map<string, string[]>());
useEffect(() => {
const integrations = getIntegrations(files);
setTons(prevState => {
const data = new Map<string, string[]>();
TopologyUtils.findTopologyRouteOutgoingNodes(integrations).forEach(t => {
const key = (t.step as any)?.uri + ':' + (t.step as any)?.parameters?.name;
if (data.has(key)) {
const list = data.get(key) || [];
list.push(t.routeId);
data.set(key, list);
} else {
data.set(key, [t.routeId]);
}
});
return data;
});
const sub1 = EventBus.onPosition()?.subscribe((evt: DslPosition) => setPosition(evt));
return () => {
sub1?.unsubscribe();
};
}, [files]);
useEffect(() => {
const toDelete1: string[] = Array.from(steps.keys()).filter(k => CamelDefinitionApiExt.findElementInIntegration(integration, k) === undefined);
toDelete1.forEach(key => deleteStep(key));
setSvgKey(uuidv4())
}, [integration]);
function setPosition(evt: DslPosition) {
if (evt.command === "add") {
addStep(evt.step.uuid, evt);
} else if (evt.command === "delete") {
deleteStep(evt.step.uuid);
} else if (evt.command === "clean") {
clearSteps();
}
}
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 {
const component = ComponentApi.findByName(uri);
return (component !== undefined && component.component.remote !== true) ? 'internal' : 'remote';
}
}
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) => {
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, getElementType(pos.step)]);
while (hasOverlap(outs)) {
outs = addGap(outs);
}
return outs;
}
function getIncoming(data: [string, number, ConnectionType]) {
const pos = steps.get(data[0]);
if (pos) {
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 incomingX = 20;
const lineX1 = incomingX + r;
const lineY1 = fromY;
const lineX2 = fromX - r * 2 + 7;
const lineY2 = fromY;
const isInternal = data[2] === 'internal';
const isNav = data[2] === 'nav';
return (!isInternal
? <g key={pos.step.uuid + "-incoming"}>
<circle key={pos.step.uuid + "-circle"} cx={incomingX} cy={fromY} r={r} className="circle-incoming"/>
<path key={pos.step.uuid + "-path"} d={`M ${lineX1},${lineY1} C ${lineX1},${lineY2} ${lineX2},${lineY1} ${lineX2},${lineY2}`}
className={isNav ? 'path-incoming-nav' : 'path-incoming'}
markerEnd="url(#arrowheadRight)"/>
</g>
: <div key={pos.step.uuid + "-incoming"} style={{display: 'none'}}></div>
)
}
}
// function getToDirectSteps(name: string) {
// return Array.from(steps.values())
// .filter(s => s.step.dslName === 'ToDefinition')
// .filter(s => NAV_COMPONENTS.includes((s.step as any)?.uri))
// .filter(s => (s.step as any)?.parameters?.name === name)
// }
function getIncomingIcons(data: [string, number, ConnectionType]) {
const pos = steps.get(data[0]);
if (pos) {
const step = (pos.step as any);
const uri = step?.uri;
const internalCall: boolean = step && uri && step?.dslName === 'FromDefinition' && INTERNAL_COMPONENTS.includes(uri);
const name: string = internalCall ? (step?.parameters?.name) : undefined;
const routes = internalCall ? tons.get(uri + ':' + name) || [] : [];
const isInternal = data[2] === 'internal';
const fromY = pos.headerRect.y + pos.headerRect.height / 2 - top;
const r = pos.headerRect.height / 2;
const incomingX = 20;
const imageX = incomingX - r + 6;
const imageY = fromY - r + 6;
return (!isInternal
? <div key={pos.step.uuid + "-icon"}
style={{display: "block", position: "absolute", top: imageY, left: imageX}}
>
{CamelUi.getConnectionIcon(pos.step)}
{routes.map((routeId, index) =>
<Tooltip key={`${routeId}:${index}`} content={`Go to route:${routeId}`} position={"right"}>
<Button style={{
position: 'absolute',
left: 27,
top: (index * 16) + (12),
whiteSpace: 'nowrap',
zIndex: 300,
padding: 0
}}
variant={'link'}
aria-label="Goto"
onClick={_ => InfrastructureAPI.onInternalConsumerClick(undefined, undefined, routeId)}>
{routeId}
</Button>
</Tooltip>
)}
</div>
: <div key={pos.step.uuid + "-icon"} style={{display: 'none'}}></div>
)
}
}
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;
})
return result;
}
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);
})
return result;
}
function getOutgoings(): [string, number, ConnectionType][] {
const outgoingDefinitions = TopologyUtils.getOutgoingDefinitions();
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', "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;
const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
return y1 > y2 ? 1 : -1
})
.map(pos => [pos.step.uuid, pos.headerRect.y - top, getElementType(pos.step)]);
while (hasOverlap(outs)) {
outs = addGap(outs);
}
return outs;
}
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;
const r = pos.headerRect.height / 2;
const outgoingX = width - 20;
const outgoingY = data[1] + RADIUS;
const lineX1 = fromX + r;
const lineY1 = fromY;
const lineX2 = outgoingX - r * 2 + (isPoll ? 14 : 6);
const lineY2 = outgoingY;
const lineXi = lineX1 + 40;
const lineYi = lineY2;
const className = isNav
? 'path-incoming-nav'
: (isPoll ? 'path-poll' : (isDynamic ? 'path-dynamic' :'path-incoming'))
return (!isInternal
? <g key={pos.step.uuid + "-outgoing"}>
<circle cx={outgoingX} cy={outgoingY} r={r} className="circle-outgoing"/>
<path
d={`M ${lineX1},${lineY1} C ${lineXi - 20}, ${lineY1} ${lineX1 - RADIUS},${lineYi} ${lineXi},${lineYi} L ${lineX2},${lineY2}`}
className={className} markerStart={isPoll ? "url(#arrowheadLeft)" : "none"} markerEnd={isPoll ? "none" : "url(#arrowheadRight)"}/>
</g>
: <div key={pos.step.uuid + "-outgoing"} style={{display: 'none'}}></div>
)
}
}
function getOutgoingIcons(data: [string, number, ConnectionType]) {
const pos = steps.get(data[0]);
if (pos) {
const step = (pos.step as any);
const uri = step?.uri;
const isInternal = data[2] === 'internal';
const isNav = data[2] === 'nav';
const internalCall = step && uri && step?.dslName === 'ToDefinition' && isNav;
const name = internalCall ? (step?.parameters?.name ? step?.parameters.name : step?.parameters?.address) : '';
const r = pos.headerRect.height / 2;
const outgoingX = width - 20;
const outgoingY = data[1] + RADIUS;
const imageX = outgoingX - r + 6;
const imageY = outgoingY - r + 6;
return (!isInternal
? <div key={pos.step.uuid + "-icon"}
style={{display: "block", position: "absolute", top: imageY, left: imageX}}>
{CamelUi.getConnectionIcon(pos.step)}
{name !== undefined &&
<Tooltip content={`Go to ${uri}:${name}`} position={"left"}>
<Button style={{
position: 'absolute',
right: 27,
top: -12,
whiteSpace: 'nowrap',
zIndex: 300,
padding: 0
}}
variant={'link'}
aria-label="Goto"
onClick={_ => InfrastructureAPI.onInternalConsumerClick(uri, name, undefined)}>
{name}
</Button>
</Tooltip>
}
</div>
: <div key={pos.step.uuid + "-icon"} style={{display: 'none'}}></div>
)
}
}
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"}/>
)
}
function getNext(pos: DslPosition): CamelElement | undefined {
if (pos.nextstep) {
return pos.nextstep;
} else if (pos.parent) {
const parent = steps.get(pos.parent.uuid);
if (parent) return getNext(parent);
}
}
function isSpecial(pos: DslPosition): boolean {
return ['ChoiceDefinition', 'MulticastDefinition', 'LoadBalanceDefinition', 'TryDefinition', 'RouteConfigurationDefinition'].includes(pos.step.dslName);
}
function addArrowToList(list: JSX.Element[], from?: DslPosition, to?: DslPosition, fromHeader?: boolean, toHeader?: boolean): JSX.Element[] {
const result: JSX.Element[] = [...list];
if (from && to) {
const rect1 = fromHeader === true ? from.headerRect : from.rect;
const rect2 = toHeader === true ? to.headerRect : to.rect;
const key = from.step.uuid + "->" + to.step.uuid;
result.push(getComplexArrow(key, rect1, rect2, toHeader === true));
}
return result;
}
function getParentDsl(uuid?: string): string | undefined {
return uuid ? steps.get(uuid)?.parent?.dslName : undefined;
}
function getArrow(pos: DslPosition): JSX.Element[] {
const list: JSX.Element[] = [];
if (pos.parent && pos.parent.dslName === 'TryDefinition' && pos.position === 0) {
const parent = steps.get(pos.parent.uuid);
list.push(...addArrowToList(list, parent, pos, true, false))
} else if (pos.parent && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(pos.parent.dslName)) {
const parent = steps.get(pos.parent.uuid);
list.push(...addArrowToList(list, parent, pos, true, false))
if (parent?.nextstep) {
const to = steps.get(parent.nextstep.uuid);
list.push(...addArrowToList(list, pos, to, true, true))
}
} else if (pos.parent && pos.parent.dslName === 'ChoiceDefinition') {
const parent = steps.get(pos.parent.uuid);
list.push(...addArrowToList(list, parent, pos, true, false))
} else if (pos.parent && ['WhenDefinition', 'OtherwiseDefinition', 'CatchDefinition', 'FinallyDefinition', 'TryDefinition'].includes(pos.parent.dslName)) {
if (pos.position === 0) {
const parent = steps.get(pos.parent.uuid);
list.push(...addArrowToList(list, parent, pos, true, false))
}
if (pos.position === (pos.inStepsLength - 1) && !isSpecial(pos)) {
const nextElement = getNext(pos);
const parentDsl1 = getParentDsl(nextElement?.uuid);
if (parentDsl1 && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(parentDsl1)) {
// do nothing
} else if (nextElement) {
const next = steps.get(nextElement.uuid);
list.push(...addArrowToList(list, pos, next, true, true))
}
}
} else if (pos.step && !isSpecial(pos)) {
if (pos.nextstep) {
const next = steps.get(pos.nextstep.uuid);
const fromHeader = !pos.step.hasSteps();
list.push(...addArrowToList(list, pos, next, fromHeader, true))
}
if (pos.step.hasSteps() && (pos.step as any).steps.length > 0) {
const firstStep = (pos.step as any).steps[0];
const next = steps.get(firstStep.uuid);
list.push(...addArrowToList(list, pos, next, true, true))
}
}
if (['WhenDefinition', 'OtherwiseDefinition'].includes(pos.step.dslName) && pos.step.hasSteps() && (pos.step as any).steps.length === 0) {
const parentDsl = getParentDsl(pos?.nextstep?.uuid);
if (parentDsl && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(parentDsl)) {
// do nothing
} else if (pos.nextstep) {
const to = steps.get(pos.nextstep.uuid);
list.push(...addArrowToList(list, pos, to, true, true))
} else {
const next = getNext(pos);
const parentDsl1 = getParentDsl(next?.uuid);
if (parentDsl1 && ['RouteConfigurationDefinition', 'MulticastDefinition', 'LoadBalanceDefinition'].includes(parentDsl1)) {
// do nothing
} else if (next) {
const to = steps.get(next.uuid);
list.push(...addArrowToList(list, pos, to, true, true))
}
}
}
if (pos.parent?.dslName === 'TryDefinition' && pos.inSteps && pos.position === (pos.inStepsLength - 1)) {
const parent = steps.get(pos.parent.uuid);
if (parent && parent.nextstep) {
const to = steps.get(parent.nextstep.uuid);
list.push(...addArrowToList(list, pos, to, true, true))
}
}
if (!isSpecial(pos) && pos.inSteps && pos.nextstep && (pos.parent?.dslName &&
!['MulticastDefinition', 'LoadBalanceDefinition'].includes(pos.parent?.dslName))) {
const next = steps.get(pos.nextstep.uuid);
if (pos.step.hasSteps() && pos.prevStep) {
} else {
list.push(...addArrowToList(list, pos, next, true, true))
}
}
if (!isSpecial(pos) && pos.inSteps && pos.nextstep && (pos.parent?.dslName &&
!['MulticastDefinition', 'LoadBalanceDefinition'].includes(pos.parent?.dslName))) {
const next = steps.get(pos.nextstep.uuid);
if (next && !isSpecial(next) && next.inSteps) {
// console.log(pos)
// const to = steps.get(parent.nextstep.uuid);
// list.push(...addArrowToList(list, pos, to, true, true))
}
}
return list;
}
function getComplexArrow(key: string, rect1: DOMRect, rect2: DOMRect, toHeader: boolean) {
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 > DIAMETER ? 20 : gapX / 2;
const radY = gapY > DIAMETER ? 20 : gapY / 2;
const endY = rect2.y - top - radY - (toHeader ? 9 : 6);
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}`
return (
<path key={uuidv4()} name={key} d={path} className="path" markerEnd="url(#arrowheadRight)"/>
)
}
function getSvg() {
const stepsArray = Array.from(steps.values());
const arrows = stepsArray.map(pos => getArrow(pos)).flat(1);
const uniqueArrows = [...new Map(arrows.map(item => [(item as any).key, item])).values()];
return (
<svg key={svgKey}
style={{width: width, height: height, position: "absolute", left: 0, top: 0}}
viewBox={"0 0 " + (width) + " " + (height)}>
<defs key='defs'>
<marker key='maker1' id="arrowheadRight" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto"
className="arrow">
<polygon points="0 0, 9 3, 0 6"/>
</marker>
<marker key='maker2' id="arrowheadLeft" markerWidth="9" markerHeight="6" refX="0" refY="3" orient="auto"
className="arrow">
<polygon points="9 0, 0 3, 9 6"/>
</marker>
</defs>
{stepsArray.map(pos => getCircle(pos))}
{uniqueArrows}
{getIncomings().map(p => getIncoming(p))}
{getOutgoings().map(p => getOutgoing(p))}
</svg>
)
}
return (
<div id="connections" className="connections" style={{width: width, height: height}}>
{getSvg()}
{getIncomings().map(p => getIncomingIcons(p))}
{getOutgoings().map(p => getOutgoingIcons(p))}
</div>
)
}