Skip to content

Commit

Permalink
Merge pull request #390
Browse files Browse the repository at this point in the history
feat(21481): Create nodes from dropping an edge from a source

* fix(21481): fix handle name

* feat(21481): add support for creating nodes on drop

* feat(21481): add mapping for valid dropped nodes

* feat(21481): add visual feedback for the dropped node

* fix(21481): remove optional chaining

* fix(21481): fix rebase
  • Loading branch information
vanch3d authored Apr 29, 2024
1 parent 5752ba6 commit b78726a
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FC, useMemo } from 'react'
import { ConnectionLineComponentProps, getSimpleBezierPath } from 'reactflow'
import { Tag } from '@chakra-ui/react'

import { getConnectedNodeFrom } from '@datahub/utils/node.utils.ts'
import { NodeIcon } from '@datahub/components/helpers'

const ConnectionLine: FC<ConnectionLineComponentProps> = ({
fromHandle,
fromNode,
fromX,
fromY,
toY,
toX,
...props
}) => {
const droppedNode = useMemo(() => {
return getConnectedNodeFrom(fromNode?.type, fromHandle?.id)
}, [fromHandle?.id, fromNode?.type])

const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: props.fromPosition,
targetX: toX,
targetY: toY,
targetPosition: props.toPosition,
}

const [d] = getSimpleBezierPath(pathParams)

return (
<g>
<path fill="none" stroke="grey" strokeWidth={1.5} className="animated" d={d} />
{props.connectionStatus === null && (
<foreignObject x={toX} y={toY - 20} width="50px" height="50px">
<Tag size="lg" colorScheme="gray" borderRadius="full" variant="outline">
<NodeIcon type={droppedNode?.type} />
</Tag>
</foreignObject>
)}
</g>
)
}

export default ConnectionLine
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import React, { FC, useCallback, useMemo, useRef, useState } from 'react'
import ReactFlow, { Connection, Node, ReactFlowInstance, ReactFlowProvider, XYPosition } from 'reactflow'
import ReactFlow, {
Connection,
HandleType,
Node,
NodeAddChange,
ReactFlowInstance,
ReactFlowProvider,
XYPosition,
} from 'reactflow'
import { Outlet } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Box } from '@chakra-ui/react'
Expand All @@ -8,19 +16,30 @@ import styles from './PolicyEditor.module.scss'

import { CustomNodeTypes } from '@datahub/designer/mappings.tsx'
import useDataHubDraftStore from '@datahub/hooks/useDataHubDraftStore.ts'
import { getNodeId, getNodePayload, isValidPolicyConnection } from '@datahub/utils/node.utils.ts'
import { getConnectedNodeFrom, getNodeId, getNodePayload, isValidPolicyConnection } from '@datahub/utils/node.utils.ts'
import CanvasControls from '@datahub/components/controls/CanvasControls.tsx'
import Minimap from '@datahub/components/controls/Minimap.tsx'
import DesignerToolbox from '@datahub/components/controls/DesignerToolbox.tsx'
import ToolboxSelectionListener from '@datahub/components/controls/ToolboxSelectionListener.tsx'
import { CopyPasteListener } from '@datahub/components/controls/CopyPasteListener.tsx'
import CopyPasteStatus from '@datahub/components/controls/CopyPasteStatus.tsx'
import ConnectionLine from '@datahub/components/nodes/ConnectionLine.tsx'

export type OnConnectStartParams = {
nodeId: string | null
handleId: string | null
handleType: HandleType | null
}
interface OnConnectStartParamsNode extends OnConnectStartParams {
type: string | undefined
}

const PolicyEditor: FC = () => {
const { t } = useTranslation('datahub')
const reactFlowWrapper = useRef(null)
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance | null>(null)
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onAddNodes } = useDataHubDraftStore()
const edgeConnectStart = useRef<OnConnectStartParamsNode | undefined>(undefined)

const nodeTypes = useMemo(() => CustomNodeTypes, [])

Expand Down Expand Up @@ -64,6 +83,60 @@ const PolicyEditor: FC = () => {
[onAddNodes, reactFlowInstance]
)

const onConnectStart = useCallback(
(_: unknown, params: OnConnectStartParams) => {
const nodeFound = nodes.find((e) => e.id === params.nodeId)
edgeConnectStart.current = undefined
if (nodeFound) {
edgeConnectStart.current = { ...params, type: nodeFound.type }
}
},
[nodes]
)

const onConnectEnd = useCallback(
(event: MouseEvent | TouchEvent) => {
const targetElement = event.target as Element
const isTargetCanvas = targetElement.classList.contains('react-flow__pane')

if (isTargetCanvas && edgeConnectStart.current && reactFlowInstance) {
const { type, handleId, nodeId } = edgeConnectStart.current

const droppedNode = getConnectedNodeFrom(type, handleId)
if (droppedNode) {
const id = getNodeId()
const newNode: Node = {
id,
position: reactFlowInstance.screenToFlowPosition({
x: (event as MouseEvent).clientX || (event as TouchEvent).touches[0].clientX,
y: (event as MouseEvent).clientY || (event as TouchEvent).touches[0].clientY,
}),
type: droppedNode.type,
data: getNodePayload(droppedNode.type),
}

const edgeConnection: Connection = droppedNode.isSource
? {
target: nodeId,
targetHandle: handleId,
source: id,
sourceHandle: droppedNode.handle,
}
: {
source: nodeId,
sourceHandle: handleId,
target: id,
targetHandle: droppedNode.handle,
}

onAddNodes([{ item: newNode, type: 'add' } as NodeAddChange])
onConnect(edgeConnection)
}
}
},
[onAddNodes, onConnect, reactFlowInstance]
)

return (
<>
<ReactFlowProvider>
Expand All @@ -76,15 +149,15 @@ const PolicyEditor: FC = () => {
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
// onEdgeUpdate={onEdgeUpdate}
// onConnectStart={onConnectStart}
// onConnectEnd={onConnectEnd}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
onConnect={onConnect}
connectionLineComponent={ConnectionLine}
onInit={setReactFlowInstance}
fitView
snapToGrid
snapGrid={[25, 25]}
className={styles.dataHubFlow}
// nodesConnectable
onDragOver={onDragOver}
onDrop={onDrop}
isValidConnection={checkValidity}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const BehaviorPolicyNode: FC<NodeProps<BehaviorPolicyData>> = (props) =>
</VStack>
</NodeWrapper>
<CustomHandle type="target" position={Position.Left} id={BehaviorPolicyData.Handle.CLIENT_FILTER} />
<CustomHandle type="source" position={Position.Right} id="transitions" />
<CustomHandle type="source" position={Position.Right} id={BehaviorPolicyData.Handle.TRANSITIONS} />
</>
)
}
1 change: 1 addition & 0 deletions hivemq-edge/src/frontend/src/extensions/datahub/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ export interface BehaviorPolicyData extends DataHubNodeData {
export namespace BehaviorPolicyData {
export enum Handle {
CLIENT_FILTER = 'clientFilter',
TRANSITIONS = 'transitions',
}
}

Expand Down
133 changes: 133 additions & 0 deletions hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,136 @@ export const isDataPolicyNodeType = (node: Node): node is Node<DataPolicyData> =

export const isBehaviorPolicyNodeType = (node: Node): node is Node<BehaviorPolicyData> =>
node.type === DataHubNodeType.BEHAVIOR_POLICY

interface ValidDropConnection {
type: DataHubNodeType
handle: string | null
isSource: boolean
}

// TODO[NVL] Would a map object->Object make this process easier and more performant?
export const getConnectedNodeFrom = (node?: string, handle?: string | null): ValidDropConnection | undefined => {
if (node === DataHubNodeType.TOPIC_FILTER) {
return {
type: DataHubNodeType.DATA_POLICY,
handle: DataPolicyData.Handle.TOPIC_FILTER,
isSource: false,
}
}
if (node === DataHubNodeType.CLIENT_FILTER) {
return {
type: DataHubNodeType.BEHAVIOR_POLICY,
handle: BehaviorPolicyData.Handle.CLIENT_FILTER,
isSource: false,
}
}
if (node === DataHubNodeType.DATA_POLICY && handle === DataPolicyData.Handle.TOPIC_FILTER) {
return {
type: DataHubNodeType.TOPIC_FILTER,
handle: null,
isSource: true,
}
}
if (node === DataHubNodeType.DATA_POLICY && handle === DataPolicyData.Handle.VALIDATION) {
return {
type: DataHubNodeType.VALIDATOR,
handle: null,
isSource: true,
}
}
if (
node === DataHubNodeType.DATA_POLICY &&
(handle === DataPolicyData.Handle.ON_SUCCESS || handle === DataPolicyData.Handle.ON_ERROR)
) {
return {
type: DataHubNodeType.OPERATION,
handle: null,
isSource: false,
}
}
if (node === DataHubNodeType.VALIDATOR && handle === 'source') {
return {
type: DataHubNodeType.DATA_POLICY,
handle: DataPolicyData.Handle.VALIDATION,
isSource: false,
}
}
if (node === DataHubNodeType.VALIDATOR && handle === 'target') {
return {
type: DataHubNodeType.SCHEMA,
handle: null,
isSource: true,
}
}
if (node === DataHubNodeType.SCHEMA) {
// There are two possibilities: DataHubNodeType.VALIDATOR and DataHubNodeType.OPERATION (transform)
// We need some form of feedback
return undefined
}
if (node === DataHubNodeType.OPERATION && handle === OperationData.Handle.OUTPUT) {
return {
type: DataHubNodeType.OPERATION,
handle: OperationData.Handle.INPUT,
isSource: false,
}
}
if (node === DataHubNodeType.OPERATION && handle === OperationData.Handle.INPUT) {
return {
type: DataHubNodeType.OPERATION,
handle: OperationData.Handle.OUTPUT,
isSource: true,
}
}
if (
node === DataHubNodeType.OPERATION &&
(handle === OperationData.Handle.SCHEMA ||
handle === OperationData.Handle.SERIALISER ||
handle === OperationData.Handle.DESERIALISER)
) {
return {
type: DataHubNodeType.SCHEMA,
handle: null,
isSource: true,
}
}
if (node === DataHubNodeType.OPERATION && handle === OperationData.Handle.FUNCTION) {
return {
type: DataHubNodeType.FUNCTION,
handle: null,
isSource: true,
}
}
if (node === DataHubNodeType.FUNCTION) {
// This is mapping to a specific data content of the OPERATION node. Not supported yet
return undefined
}
if (node === DataHubNodeType.BEHAVIOR_POLICY && handle === BehaviorPolicyData.Handle.CLIENT_FILTER) {
return {
type: DataHubNodeType.CLIENT_FILTER,
handle: null,
isSource: true,
}
}
if (node === DataHubNodeType.BEHAVIOR_POLICY && handle === BehaviorPolicyData.Handle.TRANSITIONS) {
return {
type: DataHubNodeType.TRANSITION,
handle: null,
isSource: false,
}
}
if (node === DataHubNodeType.TRANSITION && handle === TransitionData.Handle.BEHAVIOR_POLICY) {
return {
type: DataHubNodeType.BEHAVIOR_POLICY,
handle: BehaviorPolicyData.Handle.TRANSITIONS,
isSource: true,
}
}
if (node === DataHubNodeType.TRANSITION && handle === TransitionData.Handle.OPERATION) {
return {
type: DataHubNodeType.OPERATION,
handle: null,
isSource: false,
}
}
return undefined
}

0 comments on commit b78726a

Please sign in to comment.