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 }