diff --git a/packages/ui-tests/cypress/e2e/codeEditor/multiFlowEditor.cy.ts b/packages/ui-tests/cypress/e2e/codeEditor/multiFlowEditor.cy.ts index 9157561e5..3286e13fa 100644 --- a/packages/ui-tests/cypress/e2e/codeEditor/multiFlowEditor.cy.ts +++ b/packages/ui-tests/cypress/e2e/codeEditor/multiFlowEditor.cy.ts @@ -56,7 +56,8 @@ describe('Test for Multi route actions from the code editor', () => { cy.editorDeleteLine(7, 4); cy.openDesignPage(); cy.showAllRoutes(); - cy.get('[data-id^="log"]').should('have.length', 1); + /** We check how many nodes are remaining */ + cy.get('[data-id^="log"][data-kind="node"]').should('have.length', 1); cy.get('[data-testid="flows-list-route-count"]').should('have.text', '2/2'); }); diff --git a/packages/ui-tests/cypress/e2e/designer/multiflow/multiFlowDesigner.cy.ts b/packages/ui-tests/cypress/e2e/designer/multiflow/multiFlowDesigner.cy.ts index 6f9f8205b..a4ab36036 100644 --- a/packages/ui-tests/cypress/e2e/designer/multiflow/multiFlowDesigner.cy.ts +++ b/packages/ui-tests/cypress/e2e/designer/multiflow/multiFlowDesigner.cy.ts @@ -101,7 +101,8 @@ describe('Test for Multi route actions from the canvas', () => { cy.addNewRoute(); cy.showAllRoutes(); - cy.get('[data-id^="log"]').should('have.length', 3); + /** We check how many nodes are remaining */ + cy.get('[data-id^="log"][data-kind="node"]').should('have.length', 3); cy.get('[data-testid="flows-list-route-count"]').should('have.text', '3/3'); }); }); diff --git a/packages/ui/src/components/Visualization/Canvas/__snapshots__/Canvas.test.tsx.snap b/packages/ui/src/components/Visualization/Canvas/__snapshots__/Canvas.test.tsx.snap index 7c4630a16..2d1f5cdd5 100644 --- a/packages/ui/src/components/Visualization/Canvas/__snapshots__/Canvas.test.tsx.snap +++ b/packages/ui/src/components/Visualization/Canvas/__snapshots__/Canvas.test.tsx.snap @@ -30,6 +30,23 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` + + + + + @@ -70,7 +87,7 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` class="foreign-object" data-nodelabel="route-8888" height="902.5" - width="870" + width="900" x="-85" y="12.5" > @@ -154,7 +171,7 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` @@ -162,7 +179,7 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` class="foreign-object" data-nodelabel="choice" height="637.5" - width="750" + width="780" x="-25" y="72.5" > @@ -246,7 +263,7 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` @@ -254,7 +271,7 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` class="foreign-object" data-nodelabel="otherwise" height="205" - width="360" + width="390" x="305" y="445" > @@ -344,16 +361,16 @@ exports[`Canvas Catalog button should NOT be present if \`CatalogModalContext\` > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -789,6 +924,23 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p + + + + + @@ -829,7 +981,7 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p class="foreign-object" data-nodelabel="route-8888" height="902.5" - width="870" + width="900" x="-85" y="12.5" > @@ -913,7 +1065,7 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p @@ -921,7 +1073,7 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p class="foreign-object" data-nodelabel="choice" height="637.5" - width="750" + width="780" x="-25" y="72.5" > @@ -1005,7 +1157,7 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p @@ -1013,7 +1165,7 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p class="foreign-object" data-nodelabel="otherwise" height="205" - width="360" + width="390" x="305" y="445" > @@ -1103,16 +1255,16 @@ exports[`Canvas Catalog button should be present if \`CatalogModalContext\` is p > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2360,6 +2630,23 @@ exports[`Canvas should render correctly 1`] = ` + + + + + @@ -2400,7 +2687,7 @@ exports[`Canvas should render correctly 1`] = ` class="foreign-object" data-nodelabel="route-8888" height="902.5" - width="870" + width="900" x="-85" y="12.5" > @@ -2484,7 +2771,7 @@ exports[`Canvas should render correctly 1`] = ` @@ -2492,7 +2779,7 @@ exports[`Canvas should render correctly 1`] = ` class="foreign-object" data-nodelabel="choice" height="637.5" - width="750" + width="780" x="-25" y="72.5" > @@ -2576,7 +2863,7 @@ exports[`Canvas should render correctly 1`] = ` @@ -2584,7 +2871,7 @@ exports[`Canvas should render correctly 1`] = ` class="foreign-object" data-nodelabel="otherwise" height="205" - width="360" + width="390" x="305" y="445" > @@ -2674,16 +2961,16 @@ exports[`Canvas should render correctly 1`] = ` > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3119,6 +3524,23 @@ exports[`Canvas should render correctly with more routes 1`] = ` + + + + + @@ -3159,7 +3581,7 @@ exports[`Canvas should render correctly with more routes 1`] = ` class="foreign-object" data-nodelabel="route-8888" height="902.5" - width="870" + width="900" x="-85" y="12.5" > @@ -3243,7 +3665,7 @@ exports[`Canvas should render correctly with more routes 1`] = ` @@ -3251,7 +3673,7 @@ exports[`Canvas should render correctly with more routes 1`] = ` class="foreign-object" data-nodelabel="choice" height="637.5" - width="750" + width="780" x="-25" y="72.5" > @@ -3335,7 +3757,7 @@ exports[`Canvas should render correctly with more routes 1`] = ` @@ -3343,7 +3765,7 @@ exports[`Canvas should render correctly with more routes 1`] = ` class="foreign-object" data-nodelabel="otherwise" height="205" - width="360" + width="390" x="305" y="445" > @@ -3433,16 +3855,16 @@ exports[`Canvas should render correctly with more routes 1`] = ` > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/components/Visualization/Canvas/controller.service.ts b/packages/ui/src/components/Visualization/Canvas/controller.service.ts index 2f5089a1a..e636903f0 100644 --- a/packages/ui/src/components/Visualization/Canvas/controller.service.ts +++ b/packages/ui/src/components/Visualization/Canvas/controller.service.ts @@ -18,7 +18,7 @@ import { Visualization, withPanZoom, } from '@patternfly/react-topology'; -import { CustomGroupWithSelection, CustomNodeWithSelection, NoBendpointsEdge } from '../Custom'; +import { CustomGroupWithSelection, CustomNodeWithSelection, NoBendpointsEdge, EdgeEndWithButton } from '../Custom'; import { LayoutType } from './canvas.models'; export class ControllerService { @@ -109,6 +109,8 @@ export class ControllerService { switch (type) { case 'group': return CustomGroupWithSelection; + case 'edge-end': + return EdgeEndWithButton; default: switch (kind) { case ModelKind.graph: diff --git a/packages/ui/src/components/Visualization/Canvas/flow.service.test.ts b/packages/ui/src/components/Visualization/Canvas/flow.service.test.ts index 25a71e49c..6918ada83 100644 --- a/packages/ui/src/components/Visualization/Canvas/flow.service.test.ts +++ b/packages/ui/src/components/Visualization/Canvas/flow.service.test.ts @@ -1,5 +1,4 @@ -import { createVisualizationNode } from '../../../models/visualization'; -import { BaseVisualCamelEntity } from '../../../models/visualization/base-visual-entity'; +import { CamelRouteVisualEntity, createVisualizationNode } from '../../../models/visualization'; import { FlowService } from './flow.service'; describe('FlowService', () => { @@ -86,18 +85,7 @@ describe('FlowService', () => { }); it('should return a group node for a multiple nodes VisualizationNode with a group', () => { - const routeNode = createVisualizationNode('route', { - entity: { getId: () => 'myId' } as BaseVisualCamelEntity, - isGroup: true, - }); - - const fromNode = createVisualizationNode('timer', { - path: 'from', - icon: undefined, - processorName: 'from', - componentName: 'timer', - }); - routeNode.addChild(fromNode); + const routeNode = new CamelRouteVisualEntity({ from: { uri: 'timer:clock', steps: [] } }).toVizNode(); const { nodes, edges } = FlowService.getFlowDiagram(routeNode); diff --git a/packages/ui/src/components/Visualization/Canvas/flow.service.ts b/packages/ui/src/components/Visualization/Canvas/flow.service.ts index 2b9d5b5f2..c93e06262 100644 --- a/packages/ui/src/components/Visualization/Canvas/flow.service.ts +++ b/packages/ui/src/components/Visualization/Canvas/flow.service.ts @@ -64,9 +64,12 @@ export class FlowService { private static getEdgesFromVizNode(vizNodeParam: IVisualizationNode): CanvasEdge[] { const edges: CanvasEdge[] = []; + const nodeInteractions = vizNodeParam.getNodeInteraction(); if (vizNodeParam.getNextNode() !== undefined) { edges.push(this.getEdge(vizNodeParam.id, vizNodeParam.getNextNode()!.id)); + } else if (nodeInteractions.canHaveNextStep) { + edges.push(this.getEdgeEnd(vizNodeParam.id)); } return edges; @@ -111,4 +114,14 @@ export class FlowService { edgeStyle: EdgeStyle.solid, }; } + + private static getEdgeEnd(source: string): CanvasEdge { + return { + id: `${source}-end`, + type: 'edge-end', + source, + target: source, + edgeStyle: EdgeStyle.dashed, + }; + } } diff --git a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemAddStep.tsx b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemAddStep.tsx index 514493401..16b84d67c 100644 --- a/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemAddStep.tsx +++ b/packages/ui/src/components/Visualization/Custom/ContextMenu/ItemAddStep.tsx @@ -1,34 +1,49 @@ import { ContextMenuItem } from '@patternfly/react-topology'; import { FunctionComponent, PropsWithChildren, useCallback, useContext } from 'react'; import { IDataTestID } from '../../../../models'; -import { AddStepMode, IVisualizationNode } from '../../../../models/visualization/base-visual-entity'; +import { + AddStepMode, + IVisualizationNode, + IVisualizationNodeData, +} from '../../../../models/visualization/base-visual-entity'; import { CatalogModalContext } from '../../../../providers/catalog-modal.provider'; import { EntitiesContext } from '../../../../providers/entities.provider'; +import { EntitiesContextResult } from '../../../../hooks'; interface ItemAddStepProps extends PropsWithChildren { mode: AddStepMode.PrependStep | AddStepMode.AppendStep; vizNode: IVisualizationNode; } -export const ItemAddStep: FunctionComponent = (props) => { - const entitiesContext = useContext(EntitiesContext); - const catalogModalContext = useContext(CatalogModalContext); +export const addNode = async ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + catalogModalContext: any, + entitiesContext: EntitiesContextResult | null, + vizNode: IVisualizationNode, + mode: AddStepMode = AddStepMode.AppendStep, +) => { + if (!vizNode || !entitiesContext) return; - const onAddNode = useCallback(async () => { - if (!props.vizNode || !entitiesContext) return; + /** Get compatible nodes and the location where can be introduced */ + const compatibleNodes = entitiesContext.camelResource.getCompatibleComponents(mode, vizNode.data); + + /** Open Catalog modal, filtering the compatible nodes */ + const definedComponent = await catalogModalContext?.getNewComponent(compatibleNodes); + if (!definedComponent) return; - /** Get compatible nodes and the location where can be introduced */ - const compatibleNodes = entitiesContext.camelResource.getCompatibleComponents(props.mode, props.vizNode.data); + /** Add new node to the entities */ + vizNode.addBaseEntityStep(definedComponent, mode); - /** Open Catalog modal, filtering the compatible nodes */ - const definedComponent = await catalogModalContext?.getNewComponent(compatibleNodes); - if (!definedComponent) return; + /** Update entity */ + entitiesContext.updateEntitiesFromCamelResource(); +}; - /** Add new node to the entities */ - props.vizNode.addBaseEntityStep(definedComponent, props.mode); +export const ItemAddStep: FunctionComponent = (props) => { + const entitiesContext = useContext(EntitiesContext); + const catalogModalContext = useContext(CatalogModalContext); - /** Update entity */ - entitiesContext.updateEntitiesFromCamelResource(); + const onAddNode = useCallback(async () => { + addNode(catalogModalContext, entitiesContext, props.vizNode, props.mode); }, [catalogModalContext, entitiesContext, props.mode, props.vizNode]); return ( diff --git a/packages/ui/src/components/Visualization/Custom/Edge/EdgeEnd.tsx b/packages/ui/src/components/Visualization/Custom/Edge/EdgeEnd.tsx new file mode 100644 index 000000000..7a39b22e3 --- /dev/null +++ b/packages/ui/src/components/Visualization/Custom/Edge/EdgeEnd.tsx @@ -0,0 +1,57 @@ +import { PlusIcon } from '@patternfly/react-icons'; +import { + Decorator, + DefaultEdge, + EdgeModel, + EdgeTerminalType, + GraphElement, + isEdge, + observer, +} from '@patternfly/react-topology'; +import { FunctionComponent, useCallback, useContext } from 'react'; +import { IVisualizationNode } from '../../../../models'; +import { CatalogModalContext, EntitiesContext } from '../../../../providers'; +import { addNode } from '../ContextMenu/ItemAddStep'; +import { LayoutType } from '../../Canvas'; + +type DefaultEdgeProps = Parameters[0]; +interface EdgeEndProps extends DefaultEdgeProps { + /** We're not providing Data to edges */ + element: GraphElement; +} + +export const EdgeEndWithButton: FunctionComponent = observer(({ element, ...rest }) => { + if (!isEdge(element)) { + throw new Error('EdgeEndWithButton must be used only on Edge elements'); + } + const entitiesContext = useContext(EntitiesContext); + const catalogModalContext = useContext(CatalogModalContext); + const vizNode: IVisualizationNode = element.getSource().getData()?.vizNode; + const isHorizontal = element.getGraph().getLayout() === LayoutType.DagreHorizontal; + const endPoint = element.getEndPoint(); + + const onAdd = useCallback(() => { + addNode(catalogModalContext, entitiesContext, vizNode); + }, [catalogModalContext, entitiesContext, vizNode]); + + let x = endPoint.x; + let y = endPoint.y; + if (isHorizontal) { + x += 14; + } else { + y += 4; + } + + return ( + + + } onClick={onAdd} /> + + + ); +}); diff --git a/packages/ui/src/components/Visualization/Custom/NoBendingEdge.tsx b/packages/ui/src/components/Visualization/Custom/NoBendingEdge.tsx index 190fe2d2c..6cf06b7f2 100644 --- a/packages/ui/src/components/Visualization/Custom/NoBendingEdge.tsx +++ b/packages/ui/src/components/Visualization/Custom/NoBendingEdge.tsx @@ -1,7 +1,68 @@ -import { BaseEdge, Point } from '@patternfly/react-topology'; +import { BaseEdge, getTopCollapsedParent, Point } from '@patternfly/react-topology'; +import { LayoutType } from '../Canvas'; export class NoBendpointsEdge extends BaseEdge { getBendpoints(): Point[] { return []; } + + getStartPoint(): Point { + if (this.getTarget() === this.getSource()) { + const parent = getTopCollapsedParent(this.getSource()); + const isHorizontal = this.getGraph().getLayout() === LayoutType.DagreHorizontal; + const parentPos = parent.getPosition(); + const parentSize = parent.getDimensions(); + let x, y; + if (isHorizontal) { + if (parent.getType() === 'group') { + x = parentPos.x + parentSize.width / 2.0; + y = parentPos.y; + } else { + x = parentPos.x + parentSize.width; + y = parentPos.y + parentSize.height / 2.0; + } + } else { + if (parent.getType() === 'group') { + x = parentPos.x; + y = parentPos.y + parentSize.height / 2.0; + } else { + x = parentPos.x + parentSize.width / 2.0; + y = parentPos.y + parentSize.height; + } + } + return new Point(x, y); + } + + return super.getStartPoint(); + } + + getEndPoint(): Point { + if (this.getTarget() === this.getSource()) { + const parent = getTopCollapsedParent(this.getSource()); + const isHorizontal = this.getGraph().getLayout() === LayoutType.DagreHorizontal; + const parentPos = parent.getPosition(); + const parentSize = parent.getDimensions(); + let x, y; + if (isHorizontal) { + if (parent.getType() === 'group') { + x = parentPos.x + parentSize.width / 2.0 + 15; + y = parentPos.y; + } else { + x = parentPos.x + parentSize.width / 2.0 + 55; + y = parentPos.y + parentSize.height / 2.0; + } + } else { + if (parent.getType() === 'group') { + x = parentPos.x; + y = parentPos.y + parentSize.height / 2.0 + 15; + } else { + x = parentPos.x + parentSize.width / 2.0; + y = parentPos.y + parentSize.height / 2.0 + 85; + } + } + return new Point(x, y); + } + + return super.getEndPoint(); + } } diff --git a/packages/ui/src/components/Visualization/Custom/index.ts b/packages/ui/src/components/Visualization/Custom/index.ts index c64861e62..3b8d52f3a 100644 --- a/packages/ui/src/components/Visualization/Custom/index.ts +++ b/packages/ui/src/components/Visualization/Custom/index.ts @@ -1,3 +1,4 @@ export * from './Group/CustomGroup'; export * from './NoBendingEdge'; export * from './Node/CustomNode'; +export * from './Edge/EdgeEnd'; diff --git a/packages/ui/src/tests/__snapshots__/nodes-edges.test.ts.snap b/packages/ui/src/tests/__snapshots__/nodes-edges.test.ts.snap index 2fb521901..de387427c 100644 --- a/packages/ui/src/tests/__snapshots__/nodes-edges.test.ts.snap +++ b/packages/ui/src/tests/__snapshots__/nodes-edges.test.ts.snap @@ -4922,6 +4922,20 @@ exports[`Nodes and Edges should generate edges for steps with branches 2`] = ` "target": "choice-1234", "type": "edge", }, + { + "edgeStyle": "dashed", + "id": "setHeader-1234-end", + "source": "setHeader-1234", + "target": "setHeader-1234", + "type": "edge-end", + }, + { + "edgeStyle": "dashed", + "id": "log-1234-end", + "source": "log-1234", + "target": "log-1234", + "type": "edge-end", + }, { "edgeStyle": "solid", "id": "choice-1234-to-sql-1234", @@ -4929,5 +4943,12 @@ exports[`Nodes and Edges should generate edges for steps with branches 2`] = ` "target": "sql-1234", "type": "edge", }, + { + "edgeStyle": "dashed", + "id": "sql-1234-end", + "source": "sql-1234", + "target": "sql-1234", + "type": "edge-end", + }, ] `;