From 257f3594f20e5f1484951a8e4baaa82cb4af0da6 Mon Sep 17 00:00:00 2001 From: Khuda Dad Nomani Date: Tue, 14 May 2024 15:36:09 +0100 Subject: [PATCH] re-write service-server diagram --- .../components/ServiceFlowDiagram.stories.tsx | 156 ++++----- .../FloatingConnectionLine.tsx | 63 ++++ .../FlowDiagrams/Edges/OperationEdge.tsx | 206 ++++++++++-- .../Edges/OperationEdgeTextLabel.tsx | 7 +- .../ui/components/FlowDiagrams/Edges/Utils.ts | 104 ++++++ .../FlowDiagrams/Nodes/ServiceServerNode.tsx | 6 +- .../FlowDiagrams/ServiceFlowDiagram.tsx | 315 ++++++++---------- 7 files changed, 574 insertions(+), 283 deletions(-) create mode 100644 packages/ui/components/FlowDiagrams/ConnectionLines/FloatingConnectionLine.tsx create mode 100644 packages/ui/components/FlowDiagrams/Edges/Utils.ts diff --git a/apps/design-system/src/components/ServiceFlowDiagram.stories.tsx b/apps/design-system/src/components/ServiceFlowDiagram.stories.tsx index 40a2d9d21..7490ef018 100644 --- a/apps/design-system/src/components/ServiceFlowDiagram.stories.tsx +++ b/apps/design-system/src/components/ServiceFlowDiagram.stories.tsx @@ -1,124 +1,114 @@ -import { StoryObj, Meta } from '@storybook/react' +import { StoryObj, Meta } from "@storybook/react" -import { ServiceFlowDiagram, Server, Service } from '@asyncapi/studio-ui' -import type { FlowDiagramServer, FlowDiagramOperation, ServerPlacement } from '@asyncapi/studio-ui' +import { ServiceFlowDiagram, Server, Service, OperationAction } from "@asyncapi/studio-ui" const meta: Meta = { component: ServiceFlowDiagram, parameters: { - layout: 'centered', + layout: "centered", backgrounds: { - default: 'dark' + default: "dark", }, }, argTypes: { - onAddOperation: { action: 'onAddOperation' }, - onAddServer: { action: 'onAddServer' }, - onOperationClick: { action: 'onOperationClick' }, - onServerClick: { action: 'onServerClick' }, - onServiceClick: { action: 'onServiceClick' }, - } + onAddOperation: { action: "onAddOperation" }, + onAddServer: { action: "onAddServer" }, + onOperationClick: { action: "onOperationClick" }, + onServerClick: { action: "onServerClick" }, + onServiceClick: { action: "onServiceClick" }, + }, } - export default meta type Story = StoryObj -const operations = new Map([ - ['sendUserHasBeenRemoved', { type: 'send', source: 'Production Kafka Broker' }], - ['sendUserSignedUp', { type: 'send', source: 'Production Kafka Broker' }], - ['onUserSignedUp', { type: 'send', source: 'A WebSocket Client' }], - ['POST /users', { type: 'receive', source: 'An HTTP Client' }] -]); +const operations = [ + { id: "sendUserHasBeenRemoved", action: OperationAction.SEND, source: "Production Kafka Broker" }, + { id: "sendUserSignedUp", action: OperationAction.SEND, source: "Production Kafka Broker" }, + { id: "onUserSignedUp", action: OperationAction.SEND, source: "A WebSocket Client" }, + { id: "POST /users", action: OperationAction.RECEIVE, source: "An HTTP Client" }, +] + +const service = { + position: { x: 300, y: 400 }, + component: ( + + ), +} +const servers = [ + { + id: "Production Kafka Broker", + position: { x: service.position.x - 20, y: service.position.y - 300 }, + component: , + }, + { + id: "A Yet Unused Server", + position: { x: service.position.x + 300, y: service.position.y - 300 }, + component: , + }, + { + id: "A WebSocket Client", + position: { x: service.position.x - 20, y: service.position.y + 500 }, + component: , + }, + { + id: "An HTTP Client", + position: { x: service.position.x + 300, y: service.position.y + 500 }, + component: , + }, +] -const servers = new Map([ - ["top-left", { id: "Production Kafka Broker", component: }], - ["top-right", { id: "A Yet Unused Server", component: }], - ["bottom-left", { id: "A WebSocket Client", component: }], - ["bottom-right", { id: "An HTTP Client", component: }] -]); +const addServerButtonPosition = { x: service.position.x + 500, y: service.position.y - 300 } export const Default: Story = { args: { - service: { position: { x: 300, y: 400 }, component: }, + service, operations, - servers + addServerButtonPosition, + servers, }, } - export const WithOneServer: Story = { args: { - service: { position: { x: 300, y: 400 }, component: }, + service, + addServerButtonPosition, operations, - servers: new Map(Array.from(servers.entries()).slice(0, 1)) + servers: [servers[0]], }, } - -const operationsSelected = new Map([ - ['sendUserHasBeenRemoved', { type: 'send', source: 'Production Kafka Broker', selected: true }], - ['sendUserSignedUp', { type: 'send', source: 'Production Kafka Broker' }], -]); +const withOperationSelected = [ + { id: "sendUserHasBeenRemoved", action: OperationAction.SEND, source: "Production Kafka Broker", selected: true }, + { id: "sendUserSignedUp", action: OperationAction.SEND, source: "Production Kafka Broker" }, +] export const WithOperationSelected: Story = { args: { - service: { position: { x: 300, y: 400 }, component: }, - operations: operationsSelected, - servers: new Map(Array.from(servers.entries()).slice(0, 1)) + service, + addServerButtonPosition, + operations: withOperationSelected, + servers: [servers[0]], }, } - -const operationsTooMany = new Array(20).fill("").reduce((acc, _, i) => { - console.log(i) - acc.set(`sendUserHasBeenRemoved${i}`, { type: 'send', source: 'Production Kafka Broker' }); - return acc; -}, new Map()) - -console.log(operationsTooMany) +const operationsTooMany = new Array(20).fill("").map((_, i) => ({ id: `sendUserHasBeenRemoved${i}`, action: i% 3 === 0 ? OperationAction.RECEIVE : OperationAction.SEND, source: 'Production Kafka Broker' })); export const WithTooManyOperations: Story = { args: { - service: { position: { x: 300, y: 400 }, component: }, + service, operations: operationsTooMany, - servers: new Map(Array.from(servers.entries()).slice(0, 1)) + addServerButtonPosition, + servers: [servers[0]], }, } diff --git a/packages/ui/components/FlowDiagrams/ConnectionLines/FloatingConnectionLine.tsx b/packages/ui/components/FlowDiagrams/ConnectionLines/FloatingConnectionLine.tsx new file mode 100644 index 000000000..098aa5e24 --- /dev/null +++ b/packages/ui/components/FlowDiagrams/ConnectionLines/FloatingConnectionLine.tsx @@ -0,0 +1,63 @@ + +// THIS FILE IS COPY-PASTED FROM: https://reactflow.dev/examples/edges/floating-edges + + +import React from 'react'; +import { getBezierPath } from 'reactflow'; + +import { getEdgeParams } from '../Edges/Utils'; + +type FloatingConnectionLineProps = { + toX: number; + toY: number; + fromPosition: string; + toPosition: string; + fromNode?: any; +}; +function FloatingConnectionLine({ toX, toY, fromPosition, toPosition, fromNode }: FloatingConnectionLineProps) { + if (!fromNode) { + return null; + } + + const targetNode = { + id: 'connection-target', + width: 1, + height: 1, + positionAbsolute: { x: toX, y: toY } + }; + + const { sx, sy } = getEdgeParams(fromNode, targetNode); + const [edgePath] = getBezierPath({ + sourceX: sx, + sourceY: sy, + // @ts-ignore + sourcePosition: fromPosition, + // @ts-ignore + targetPosition: toPosition, + targetX: toX, + targetY: toY + }); + + return ( + + + + + ); + +} + +export default FloatingConnectionLine; diff --git a/packages/ui/components/FlowDiagrams/Edges/OperationEdge.tsx b/packages/ui/components/FlowDiagrams/Edges/OperationEdge.tsx index 9529855f8..66bd62eaf 100644 --- a/packages/ui/components/FlowDiagrams/Edges/OperationEdge.tsx +++ b/packages/ui/components/FlowDiagrams/Edges/OperationEdge.tsx @@ -1,52 +1,202 @@ -import { BaseEdge, Edge as ReactFlowEdge, EdgeLabelRenderer, EdgeProps, getStraightPath, useEdges } from "reactflow" -import EdgeTextLabel from './OperationEdgeTextLabel' -import EdgeButtonLabel from './OperationEdgeButtonLabel' +import { + BaseEdge, + Edge as ReactFlowEdge, + EdgeLabelRenderer, + EdgeProps, + getStraightPath, + useEdges, + useStore, + getBezierPath, + Position, +} from "reactflow" +import EdgeTextLabel from "./OperationEdgeTextLabel" +import EdgeButtonLabel from "./OperationEdgeButtonLabel" +import { useCallback } from "react" +import { getEdgeParams } from "./Utils" - - -export type OperationEdge = Omit & { +export type OperationEdge = Omit & { data: { index: number - operationType: "send" | "receive", + operationType: "send" | "receive" serverId: string } } - const EDGE_SPACING = 20 const calculateEdgeOffset = (index: number, numberOfEdges: number) => { - const centerOffset = (numberOfEdges - 1) * EDGE_SPACING / 2 + const centerOffset = ((numberOfEdges - 1) * EDGE_SPACING) / 2 return index * EDGE_SPACING - centerOffset } -const doesBelongToTheCurrentGroup = (edge: ReactFlowEdge, source: any, target: any) => - (edge.source === source && edge.target === target) || - (edge.source === target && edge.target === source) +const isOppositePosition = (sourcePos: Position, targetPos: Position) => { + const opposites = { + [Position.Top]: Position.Bottom, + [Position.Bottom]: Position.Top, + [Position.Left]: Position.Right, + [Position.Right]: Position.Left, + } + return opposites[sourcePos] === targetPos +} -export function OperationEdge({ sourceX, targetX, sourceY, targetY, selected, style, label, target, source, data, ...rest }: EdgeProps) { - const index = data.index - const numberOfEdges = useEdges().filter(edge => doesBelongToTheCurrentGroup(edge, source, target)).length - const offset = calculateEdgeOffset(index, numberOfEdges) - const X = data.operationType && data.operationType === "receive" ? sourceX + offset : targetX + offset - const [edgePath, labelX, labelY] = getStraightPath({ - sourceX: X, - sourceY, - targetX: X, - targetY, - }) +const doesBelongToTheCurrentGroup = (edge: ReactFlowEdge, source: any, target: any) => + (edge.source === source && edge.target === target) || (edge.source === target && edge.target === source) + +const getMiddleEdgeParams = ({ sx, sy, tx, ty, sourcePos, targetPos }: any, offset: number, operationType: string) => { + const fromNodeOffset = 50 + let offsetX = offset, + offsetY = 0, + offsetSX = 0, + offsetSY = 0, + offsetTX = 0, + offsetTY = 0 + + const isSendOperation = operationType === "send" + const relevantPos = isSendOperation ? sourcePos : targetPos + + // Determine offset direction based on position + switch (relevantPos) { + case Position.Top: + offsetSY += fromNodeOffset * (isSendOperation ? -1 : 1) + offsetTY += fromNodeOffset * (isSendOperation ? 1 : -1) + break + case Position.Bottom: + offsetSY += fromNodeOffset * (isSendOperation ? 1 : -1) + offsetTY += fromNodeOffset * (isSendOperation ? -1 : 1) + break + case Position.Left: + offsetSX += fromNodeOffset * (isSendOperation ? -1 : 1) + offsetTX += fromNodeOffset * (isSendOperation ? 1 : -1) + offsetX = 0 + offsetY = offset + break + case Position.Right: + offsetSX += fromNodeOffset * (isSendOperation ? 1 : -1) + offsetTX += fromNodeOffset * (isSendOperation ? -1 : 1) + offsetX = 0 + offsetY = offset + break + } + + return { + sx: sx + offsetSX + offsetX, + sy: sy + offsetSY + offsetY, + tx: tx + offsetTX + offsetX, + ty: ty + offsetTY + offsetY, + sourcePos, + targetPos, + } +} + +export function OperationEdge({ + sourceX, + targetX, + sourceY, + targetY, + selected, + style, + label, + target, + source, + data, + ...rest +}: EdgeProps): JSX.Element | null { + // // Fetch nodes associated with the source and target from the state store. + const sourceNode = useStore(useCallback((store) => store.nodeInternals.get(source), [source])) + const targetNode = useStore(useCallback((store) => store.nodeInternals.get(target), [target])) + + // Calculate offset and a percentage of it for positioning the edge. + const numberOfEdges = useEdges().filter((edge) => doesBelongToTheCurrentGroup(edge, source, target)).length + const offset = calculateEdgeOffset(data.index, numberOfEdges) + const TenPercentOfOffset = offset * 0.1 + + if (!sourceNode || !targetNode) { + return null + } + + let { tx, ty, sx, sy, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode) + + if (!isOppositePosition(sourcePos, targetPos)) return null + + // Determine operation characteristics: send operation and orientation. + const isSendOperation = data.operationType === "send" + const isHorizontal = sourcePos === Position.Right || sourcePos === Position.Left + if (isSendOperation) + if (isHorizontal) { + ty += TenPercentOfOffset + sy = ty + } else { + tx += TenPercentOfOffset + sx = tx + } + else if (isHorizontal) { + sy += TenPercentOfOffset + ty = sy + } else { + sx += TenPercentOfOffset + tx = sx + } + + const middleEdgeParams = getMiddleEdgeParams({ sx, sy, tx, ty, sourcePos, targetPos }, offset, data.operationType) + + // Use Bezier and straight paths to draw the edge. + const [targetPath, sourcePath, edgePath, labelX, labelY] = constructEdgePaths(middleEdgeParams, tx, ty, sx, sy) const color = selected ? "#ec4899" : "#cbd5e1" + const orientation = + middleEdgeParams.sourcePos === Position.Top || middleEdgeParams.targetPos === Position.Top + ? "vertical" + : "horizontal" + return ( <> - + - {typeof label === "string" - ? - : - } + {typeof label === "string" ? ( + + ) : ( + + )} ) } + +function constructEdgePaths(middleEdgeParams: any, tx: number, ty: number, sx: number, sy: number) { + const [targetPath] = getBezierPath({ + sourceX: middleEdgeParams.tx, + sourceY: middleEdgeParams.ty, + sourcePosition: middleEdgeParams.sourcePos, + targetX: tx, + targetY: ty, + targetPosition: middleEdgeParams.targetPos, + }) + + const [sourcePath] = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: middleEdgeParams.sourcePos, + targetX: middleEdgeParams.sx, + targetY: middleEdgeParams.sy, + targetPosition: middleEdgeParams.targetPos, + }) + + const [edgePath, labelX, labelY] = getStraightPath({ + sourceX: middleEdgeParams.sx, + sourceY: middleEdgeParams.sy, + targetX: middleEdgeParams.tx, + targetY: middleEdgeParams.ty, + }) + return [targetPath, sourcePath, edgePath, labelX, labelY] +} diff --git a/packages/ui/components/FlowDiagrams/Edges/OperationEdgeTextLabel.tsx b/packages/ui/components/FlowDiagrams/Edges/OperationEdgeTextLabel.tsx index ac6635813..c07e58369 100644 --- a/packages/ui/components/FlowDiagrams/Edges/OperationEdgeTextLabel.tsx +++ b/packages/ui/components/FlowDiagrams/Edges/OperationEdgeTextLabel.tsx @@ -7,14 +7,15 @@ interface EdgeLabelProps { labelX: number labelY: number className?: string + orientation?: "horizontal" | "vertical" } -function EdgeTextLabel({ selected, label, labelX, labelY, className }: EdgeLabelProps) { +function EdgeTextLabel({ selected, label, labelX, labelY, className, orientation = "horizontal" }: EdgeLabelProps) { return (
{label} diff --git a/packages/ui/components/FlowDiagrams/Edges/Utils.ts b/packages/ui/components/FlowDiagrams/Edges/Utils.ts new file mode 100644 index 000000000..863958edd --- /dev/null +++ b/packages/ui/components/FlowDiagrams/Edges/Utils.ts @@ -0,0 +1,104 @@ +// @ts-nocheck +// THIS FILE IS COPY-PASTED FROM: https://reactflow.dev/examples/edges/floating-edges +import { Position, MarkerType } from 'reactflow'; + +// this helper function returns the intersection point +// of the line between the center of the intersectionNode and the target node +function getNodeIntersection(intersectionNode, targetNode) { + // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a + const { + width: intersectionNodeWidth, + height: intersectionNodeHeight, + positionAbsolute: intersectionNodePosition, + } = intersectionNode; + const targetPosition = targetNode.positionAbsolute; + + const w = intersectionNodeWidth / 2; + const h = intersectionNodeHeight / 2; + + const x2 = intersectionNodePosition.x + w; + const y2 = intersectionNodePosition.y + h; + const x1 = targetPosition.x + targetNode.width / 2; + const y1 = targetPosition.y + targetNode.height / 2; + + const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h); + const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h); + const a = 1 / (Math.abs(xx1) + Math.abs(yy1)); + const xx3 = a * xx1; + const yy3 = a * yy1; + const x = w * (xx3 + yy3) + x2; + const y = h * (-xx3 + yy3) + y2; + + return { x, y }; +} + +// returns the position (top,right,bottom or right) passed node compared to the intersection point +function getEdgePosition(node, intersectionPoint) { + const n = { ...node.positionAbsolute, ...node }; + const nx = Math.round(n.x); + const ny = Math.round(n.y); + const px = Math.round(intersectionPoint.x); + const py = Math.round(intersectionPoint.y); + + if (px <= nx + 1) { + return Position.Left; + } + if (px >= nx + n.width - 1) { + return Position.Right; + } + if (py <= ny + 1) { + return Position.Top; + } + if (py >= n.y + n.height - 1) { + return Position.Bottom; + } + + return Position.Top; +} + +// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge +export function getEdgeParams(source, target) { + const sourceIntersectionPoint = getNodeIntersection(source, target); + const targetIntersectionPoint = getNodeIntersection(target, source); + + const sourcePos = getEdgePosition(source, sourceIntersectionPoint); + const targetPos = getEdgePosition(target, targetIntersectionPoint); + + return { + sx: sourceIntersectionPoint.x, + sy: sourceIntersectionPoint.y, + tx: targetIntersectionPoint.x, + ty: targetIntersectionPoint.y, + sourcePos, + targetPos, + }; +} + +export function createNodesAndEdges() { + const nodes = []; + const edges = []; + const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; + + nodes.push({ id: 'target', data: { label: 'Target' }, position: center }); + + for (let i = 0; i < 8; i++) { + const degrees = i * (360 / 8); + const radians = degrees * (Math.PI / 180); + const x = 250 * Math.cos(radians) + center.x; + const y = 250 * Math.sin(radians) + center.y; + + nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } }); + + edges.push({ + id: `edge-${i}`, + target: 'target', + source: `${i}`, + type: 'floating', + markerEnd: { + type: MarkerType.Arrow, + }, + }); + } + + return { nodes, edges }; +} diff --git a/packages/ui/components/FlowDiagrams/Nodes/ServiceServerNode.tsx b/packages/ui/components/FlowDiagrams/Nodes/ServiceServerNode.tsx index 81af86737..2633f0808 100644 --- a/packages/ui/components/FlowDiagrams/Nodes/ServiceServerNode.tsx +++ b/packages/ui/components/FlowDiagrams/Nodes/ServiceServerNode.tsx @@ -4,7 +4,7 @@ import { Handle, Node, Position } from "reactflow" type Data = { ui: ReactNode } -export type ServiceServerNode = Omit & { +export type ServiceServerNode = Omit & { data: Data } export function ServiceServerNode({ data }: {data: Data}) { @@ -16,6 +16,10 @@ export function ServiceServerNode({ data }: {data: Data}) { + + + +
) diff --git a/packages/ui/components/FlowDiagrams/ServiceFlowDiagram.tsx b/packages/ui/components/FlowDiagrams/ServiceFlowDiagram.tsx index 015c76edd..03025949d 100644 --- a/packages/ui/components/FlowDiagrams/ServiceFlowDiagram.tsx +++ b/packages/ui/components/FlowDiagrams/ServiceFlowDiagram.tsx @@ -1,53 +1,52 @@ -import React, { useCallback, useState, ReactElement } from 'react'; -import ReactFlow, { Edge, Node } from 'reactflow'; -import 'reactflow/dist/style.css'; -import { AddIcon } from '../icons'; -import { OperationEdge } from './Edges'; -import { ServiceServerNode } from './Nodes'; - -export interface FlowDiagramOperation { - type: 'send' | 'receive'; - source: string; - selected?: boolean; -} +"use client" +import React, { ReactElement, useEffect, useMemo } from "react" +import ReactFlow, { Edge, EdgeTypes, Node, useEdgesState, useNodesState } from "reactflow" +import "reactflow/dist/style.css" -export type ServerPlacement = "top-left" | "top-right" | "bottom-left" | "bottom-right"; +import { OperationEdge } from "./Edges/OperationEdge" +import { ServiceServerNode } from "./Nodes/ServiceServerNode" -export interface FlowDiagramServer { - id: string; - component: ReactElement; +export enum OperationAction { + SEND = "send", + RECEIVE = "receive", +} +interface Operation { + id: string + action: OperationAction + source: string + selected?: boolean +} + +interface Server { + id: string + position: Point + component: ReactElement } interface Point { - x: number; - y: number; + x: number + y: number } interface Service { - position: Point; - component: ReactElement; + position: Point + component: ReactElement } interface ServiceFlowDiagramProps { - operations: Map; - servers: Map; - service: Service; - onServiceClick?: () => void; - onServerClick?: (serverId: string) => void; - onOperationClick?: (operationId: string) => void; - onAddServer?: () => void; - onAddOperation?: (serverId: string) => void; + operations: Operation[] + servers: Server[] + service: Service + addServerButtonPosition: Point + onServiceClick?: () => void + onServerClick?: (serverId: string) => void + onOperationClick?: (operationId: string) => void + onAddServer?: () => void + onAddOperation?: (serverId: string) => void } -const SERVICE_SERVER = 'service-server'; -const OPERATION = 'operation' - -const edgeTypes = { [OPERATION]: OperationEdge }; -const nodeTypes = { [SERVICE_SERVER]: ServiceServerNode }; - -const serviceServerSpacing = 200; -const serverWidth = 290; -const serverHeight = 50; +const SERVICE_SERVER = "service-server" +const OPERATION = "operation" const CONSTANTS = { RIGHT: "right", @@ -56,160 +55,140 @@ const CONSTANTS = { SERVICE_ID: "service", RECEIVE: "receive", SEND: "send", -}; - -const calculateServerPosition = (servicePosition: Point, serviceHeight: number, placement: ServerPlacement) => { - const adjustX = placement.includes(CONSTANTS.RIGHT) ? serverWidth - 15 : -35; - const adjustY = placement.includes(CONSTANTS.TOP) ? -serverHeight - serviceServerSpacing : serviceHeight + serviceServerSpacing; - return { - x: servicePosition.x + adjustX, - y: servicePosition.y + adjustY - }; -}; - -function calculateAddServerPosition(service: Service, servers: Map): Point { - const serversAtTop = Array.from(servers.keys()).filter(key => key.includes(CONSTANTS.TOP)).length; - return { x: service.position.x + serverWidth * serversAtTop, y: service.position.y - serviceServerSpacing - (serverHeight / 1.5 ) } } -export const ServiceFlowDiagram: React.FC = ({ operations, servers, service, onAddOperation, onAddServer, onOperationClick, onServerClick, onServiceClick }) => { - const [serviceDimensions, setServiceDimensions] = useState({ width: 0, height: 0 }); - - const measuredRef = useCallback((node: any) => { - if (node !== null) { - setServiceDimensions({ - width: node.offsetWidth, - height: node.offsetHeight, - }); - } - }, []); - - const serviceWithRef = React.cloneElement(service.component, { ref: measuredRef }); - - // Adding nodes for servers - const nodes: ServiceServerNode[] = Array.from(servers.entries()).map(([placement, server]) => ({ - id: server.id, - position: calculateServerPosition(service.position, serviceDimensions.height, placement), - type: SERVICE_SERVER, - data: { ui: server.component }, - })); - - // Adding the service node (there is only one service per graph) - nodes.push({ - id: CONSTANTS.SERVICE_ID, - position: service.position, - type: SERVICE_SERVER, - data: { ui: serviceWithRef }, - }); +import FloatingConnectionLine from "./ConnectionLines/FloatingConnectionLine" +import { AddIcon } from "../icons" + +const nodeTypes = { [SERVICE_SERVER]: ServiceServerNode } +const edgeTypes = { operation: OperationEdge } + +const ServiceFlowDiagram: React.FC = ({ + operations, + servers, + service, + addServerButtonPosition, + onAddOperation, + onAddServer, + onOperationClick, + onServerClick, + onServiceClick, +}) => { + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [edges, setEdges, onEdgesChange] = useEdgesState([]) + + useEffect(() => { + // Adding nodes for servers + const nodes: ServiceServerNode[] = servers.map((server) => ({ + id: server.id, + position: server.position, + type: SERVICE_SERVER, + data: { ui: server.component }, + })) + + // Adding the service node (there is only one service per graph) + nodes.push({ + id: CONSTANTS.SERVICE_ID, + position: service.position, + type: SERVICE_SERVER, + data: { ui: service.component }, + }) // Adding the add "CREATE SERVER" node - const addServerNode = ; - nodes.push({ - id: "server-add", - position: calculateAddServerPosition(service, servers), - type: SERVICE_SERVER, - data: { ui: addServerNode }, - }); - - - // Adding edges for operations - const edges: OperationEdge[]= []; - Array.from(servers.entries()).forEach(([placement, server]) => { - const serverOperations = Array.from(operations.entries()).filter(([,op]) => server.id === op.source); - serverOperations.forEach(([id, operation], index) => { - const [placement] = Array.from(servers.entries()).find((([,server]) => server.id === operation.source)) ?? []; - let sourceHandle = CONSTANTS.BOTTOM; - let targetHandle = CONSTANTS.TOP; - let source = CONSTANTS.SERVICE_ID; - let target = operation.source; - const isReceive = operation.type === CONSTANTS.RECEIVE; - const isServerTop = placement?.includes(CONSTANTS.TOP); - if (isReceive) { - [sourceHandle, targetHandle] = [targetHandle, sourceHandle]; - [source, target] = [target, source]; - } - if (isServerTop) { - [sourceHandle, targetHandle] = [targetHandle, sourceHandle]; - } + nodes.push({ + id: "server-add", + position: addServerButtonPosition, + type: SERVICE_SERVER, + data: { ui: }, + }) + + // Adding edges for operations + const edges: OperationEdge[] = [] + servers.forEach((server) => { + const serverOperations = operations.filter((op) => op.source === server.id) + serverOperations.forEach((operation, index) => { + let source = CONSTANTS.SERVICE_ID + let target = operation.source + const shouldReverse = operation.action === CONSTANTS.RECEIVE + if (shouldReverse) { + [source, target] = [target, source] + } + edges.push({ + id: operation.id, + type: OPERATION, + data: { + operationType: operation.action, + index: index, + serverId: operation.source, + }, + source, + target, + animated: true, + selected: operation.selected, + label: operation.id, + }) + }) + }) + + // Adding edges for "Add Operation" + servers.forEach((server) => { + const edgeIndex = operations.filter((op) => server.id === op.source).length edges.push({ - id: id, - type: OPERATION, - data: { - operationType: operation.type, - index: index, - serverId: operation.source, - }, - source, - target, - sourceHandle, - targetHandle, - animated: true, - selected: operation.selected, - label: id, - })}) - }); - - // Adding edges for "Add" icon to create new operations - servers.forEach((server, placement) => { - const edgeIndex = Array.from(operations.values()).filter(op => server.id === op.source).length; - let sourceHandle = CONSTANTS.BOTTOM; - let targetHandle = CONSTANTS.TOP; - const shouldReverse = placement.includes(CONSTANTS.BOTTOM); - if (shouldReverse) { - [sourceHandle, targetHandle] = [targetHandle, sourceHandle]; - } - edges.push({ - id: `${server.id}-add`, - source: server.id, - target: CONSTANTS.SERVICE_ID, - sourceHandle: sourceHandle, - targetHandle: targetHandle, - data: { - operationType: "receive", - index: edgeIndex, - serverId: server.id, - }, - type: OPERATION, - label: (), - style: { strokeDasharray: 2, stroke: "#6b7280" }, - }); - }); + id: `${server.id}-add`, + source: server.id, + target: CONSTANTS.SERVICE_ID, + data: { + operationType: "receive", + index: edgeIndex, + serverId: server.id, + }, + type: OPERATION, + label: , + style: { strokeDasharray: 2, stroke: "#6b7280" }, + }) + }) + + setNodes(nodes) + setEdges(edges) + }, []) // event handlers const onEdgeClick = (event: React.MouseEvent, edge: Edge) => { - const operationEdge = edge as OperationEdge; + const operationEdge = edge as OperationEdge if (operationEdge.id.endsWith("-add")) { - onAddOperation && onAddOperation(operationEdge.data.serverId); + onAddOperation && onAddOperation(operationEdge.data.serverId) } else { - onOperationClick && onOperationClick(operationEdge.id); + onOperationClick && onOperationClick(operationEdge.id) } - }; + } const onNodeClick = (event: React.MouseEvent, node: Node) => { - const serviceServerNode = node as ServiceServerNode; + const serviceServerNode = node as ServiceServerNode if (serviceServerNode.id.endsWith("-add")) { - onAddServer && onAddServer(); - } else if(serviceServerNode.id === CONSTANTS.SERVICE_ID) { - onServiceClick && onServiceClick(); + onAddServer && onAddServer() + } else if (serviceServerNode.id === CONSTANTS.SERVICE_ID) { + onServiceClick && onServiceClick() } else { - onServerClick && onServerClick(serviceServerNode.id); + onServerClick && onServerClick(serviceServerNode.id) } - }; - + } return ( -
+
- ); + ) } - +export { ServiceFlowDiagram }