diff --git a/hivemq-edge/src/frontend/src/components/Chakra/ShortcutRenderer.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/Chakra/ShortcutRenderer.spec.cy.tsx
index c315adc13c..8b544b931d 100644
--- a/hivemq-edge/src/frontend/src/components/Chakra/ShortcutRenderer.spec.cy.tsx
+++ b/hivemq-edge/src/frontend/src/components/Chakra/ShortcutRenderer.spec.cy.tsx
@@ -19,8 +19,10 @@ describe('ShortcutRenderer', () => {
it('should render multiple shortcuts', () => {
cy.mountWithProviders()
+ const cmd = Cypress.platform === 'darwin' ? 'Command' : 'Ctrl'
+ console.log('XXXXXX', cmd)
- cy.get('[role="term"]').should('contain.text', 'CTRL + C , Command + V , ESC')
+ cy.get('[role="term"]').should('contain.text', `CTRL + C , ${cmd} + V , ESC`)
cy.get('kbd').should('have.length', 5)
})
diff --git a/hivemq-edge/src/frontend/src/extensions/datahub/components/pages/PolicyEditor.tsx b/hivemq-edge/src/frontend/src/extensions/datahub/components/pages/PolicyEditor.tsx
index ef3a8f0db7..bbf81e5b27 100644
--- a/hivemq-edge/src/frontend/src/extensions/datahub/components/pages/PolicyEditor.tsx
+++ b/hivemq-edge/src/frontend/src/extensions/datahub/components/pages/PolicyEditor.tsx
@@ -24,7 +24,10 @@ const PolicyEditor: FC = () => {
const nodeTypes = useMemo(() => CustomNodeTypes, [])
- const checkValidity = useCallback((connection: Connection) => isValidPolicyConnection(connection, nodes), [nodes])
+ const checkValidity = useCallback(
+ (connection: Connection) => isValidPolicyConnection(connection, nodes, edges),
+ [edges, nodes]
+ )
const onDragOver = useCallback((event: React.DragEvent | undefined) => {
if (event) {
diff --git a/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.spec.ts b/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.spec.ts
index 92237a4bde..2702dc2a79 100644
--- a/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.spec.ts
+++ b/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.spec.ts
@@ -2,7 +2,7 @@ import { describe, expect } from 'vitest'
import { DataHubNodeType, DataPolicyData, OperationData, SchemaType, StrategyType, ValidatorType } from '../types.ts'
import { getNodeId, getNodePayload, isValidPolicyConnection } from './node.utils.ts'
import { MOCK_JSONSCHEMA_SCHEMA } from '../__test-utils__/schema.mocks.ts'
-import { Node } from 'reactflow'
+import { Edge, Node } from 'reactflow'
import { MOCK_DEFAULT_NODE } from '@/__test-utils__/react-flow/nodes.ts'
describe('getNodeId', () => {
@@ -70,7 +70,7 @@ describe('getNodePayload', () => {
describe('isValidPolicyConnection', () => {
it('should not be a valid connection with an unknown source', async () => {
expect(
- isValidPolicyConnection({ source: 'source', target: 'target', sourceHandle: null, targetHandle: null }, [])
+ isValidPolicyConnection({ source: 'source', target: 'target', sourceHandle: null, targetHandle: null }, [], [])
).toBeFalsy()
})
@@ -82,9 +82,11 @@ describe('isValidPolicyConnection', () => {
position: { x: 0, y: 0 },
}
expect(
- isValidPolicyConnection({ source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null }, [
- MOCK_NODE_DATA_POLICY,
- ])
+ isValidPolicyConnection(
+ { source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null },
+ [MOCK_NODE_DATA_POLICY],
+ []
+ )
).toBeFalsy()
})
@@ -97,9 +99,11 @@ describe('isValidPolicyConnection', () => {
position: { x: 0, y: 0 },
}
expect(
- isValidPolicyConnection({ source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null }, [
- MOCK_NODE_DATA_POLICY,
- ])
+ isValidPolicyConnection(
+ { source: 'node-id', target: 'target', sourceHandle: null, targetHandle: null },
+ [MOCK_NODE_DATA_POLICY],
+ []
+ )
).toBeFalsy()
})
@@ -120,10 +124,11 @@ describe('isValidPolicyConnection', () => {
position: { x: 0, y: 0 },
}
expect(
- isValidPolicyConnection({ source: 'node-id', target: 'node-operation', sourceHandle: null, targetHandle: null }, [
- MOCK_NODE_DATA_POLICY,
- MOCK_NODE_OPERATION,
- ])
+ isValidPolicyConnection(
+ { source: 'node-id', target: 'node-operation', sourceHandle: null, targetHandle: null },
+ [MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION],
+ []
+ )
).toBeTruthy()
})
@@ -151,7 +156,8 @@ describe('isValidPolicyConnection', () => {
sourceHandle: null,
targetHandle: OperationData.Handle.SCHEMA,
},
- [MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION]
+ [MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION],
+ []
)
).toBeTruthy()
@@ -163,7 +169,75 @@ describe('isValidPolicyConnection', () => {
sourceHandle: null,
targetHandle: DataPolicyData.Handle.ON_SUCCESS,
},
- [MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION]
+ [MOCK_NODE_DATA_POLICY, MOCK_NODE_OPERATION],
+ []
+ )
+ ).toBeFalsy()
+ })
+
+ it('should not be a valid connection if self-referencing', async () => {
+ const MOCK_NODE_OPERATION: Node = {
+ id: 'node-operation',
+ type: DataHubNodeType.OPERATION,
+ data: {},
+ ...MOCK_DEFAULT_NODE,
+ position: { x: 0, y: 0 },
+ }
+ expect(
+ isValidPolicyConnection(
+ {
+ source: 'node-operation',
+ target: 'node-operation',
+ sourceHandle: null,
+ targetHandle: null,
+ },
+ [MOCK_NODE_OPERATION],
+ []
+ )
+ ).toBeFalsy()
+ })
+
+ it('should not be a valid connection if creating a cycle', async () => {
+ const MOCK_NODE_OPERATION: Node = {
+ id: 'node-operation',
+ type: DataHubNodeType.OPERATION,
+ data: {},
+ ...MOCK_DEFAULT_NODE,
+ position: { x: 0, y: 0 },
+ }
+
+ const nodes: Node[] = [
+ MOCK_NODE_OPERATION,
+ { ...MOCK_NODE_OPERATION, id: 'node-operation-1' },
+ { ...MOCK_NODE_OPERATION, id: 'node-operation-2' },
+ ]
+ const edges: Edge[] = [
+ { id: '1', source: 'node-operation', target: 'node-operation-1', sourceHandle: null, targetHandle: null },
+ { id: '1', source: 'node-operation-1', target: 'node-operation-2', sourceHandle: null, targetHandle: null },
+ ]
+
+ expect(
+ isValidPolicyConnection(
+ {
+ source: 'node-operation-1',
+ target: 'node-operation',
+ sourceHandle: null,
+ targetHandle: null,
+ },
+ nodes,
+ edges
+ )
+ ).toBeFalsy()
+ expect(
+ isValidPolicyConnection(
+ {
+ source: 'node-operation-2',
+ target: 'node-operation',
+ sourceHandle: null,
+ targetHandle: null,
+ },
+ nodes,
+ edges
)
).toBeFalsy()
})
diff --git a/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.ts b/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.ts
index 25ef980707..4962ef16d6 100644
--- a/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.ts
+++ b/hivemq-edge/src/frontend/src/extensions/datahub/utils/node.utils.ts
@@ -1,4 +1,4 @@
-import { Connection, Node } from 'reactflow'
+import { Connection, Edge, getOutgoers, Node } from 'reactflow'
import { v4 as uuidv4 } from 'uuid'
import { MOCK_JSONSCHEMA_SCHEMA } from '../__test-utils__/schema.mocks.ts'
@@ -78,18 +78,42 @@ export const validConnections: ConnectionValidity = {
[DataHubNodeType.FUNCTION]: [[DataHubNodeType.OPERATION, OperationData.Handle.FUNCTION]],
}
-export const isValidPolicyConnection = (connection: Connection, nodes: Node[]) => {
+export const isValidPolicyConnection = (connection: Connection, nodes: Node[], edges: Edge[]) => {
const source = nodes.find((node) => node.id === connection.source)
const destination = nodes.find((node) => node.id === connection.target)
- if (!source) {
+ const hasCycle = (node: Node, visited = new Set()) => {
+ if (visited.has(node.id)) return false
+
+ visited.add(node.id)
+
+ for (const outgoer of getOutgoers(node, nodes, edges)) {
+ if (outgoer.id === connection.source) return true
+ if (hasCycle(outgoer, visited)) return true
+ }
+ return false
+ }
+
+ if (!source || !destination) {
+ return false
+ }
+
+ if (!source.type) {
+ // node that are not Data Hub types are illegal
return false
}
- const { type } = source
- if (!type) {
+
+ if (destination.id === source.id) {
+ // self-connection are illegal
return false
}
- const connectionValidators = validConnections[type]
+
+ if (hasCycle(destination)) {
+ // cycle are illegal
+ return false
+ }
+
+ const connectionValidators = validConnections[source.type]
if (!connectionValidators) {
return false
}