Skip to content

Commit

Permalink
Merge pull request #526
Browse files Browse the repository at this point in the history
feat(25160): Add devices and brokers to the workspace

* feat(25160): add mock for bi-directional

* feat(25160): add type for DEVICE node

* feat(25160): add custom node for DEVICE

* refactor(25160): remove the show host option

* refactor(25160): add device-related handle to adapter

* feat(25160): add device node and link to adapter to the workspace

* test(25160): add tests

* test(25160): fix tests

* test(25160): add tests

* test(25160): fix tests and rename the file

* refactor(25160): updated type
  • Loading branch information
vanch3d committed Sep 10, 2024
1 parent 4773826 commit 3598f53
Show file tree
Hide file tree
Showing 18 changed files with 181 additions and 21 deletions.
11 changes: 9 additions & 2 deletions hivemq-edge/src/frontend/src/__test-utils__/react-flow/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NodeProps, Position } from 'reactflow'
import { mockAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__'
import { mockAdapter, mockProtocolAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__'
import { mockBridge } from '@/api/hooks/useGetBridges/__handlers__'
import { Listener } from '@/api/__generated__'
import { Listener, ProtocolAdapter } from '@/api/__generated__'
import { mockMqttListener } from '@/api/hooks/useGateway/__handlers__'
import { Group, NodeTypes } from '@/modules/Workspace/types.ts'

Expand Down Expand Up @@ -53,3 +53,10 @@ export const MOCK_NODE_GROUP: NodeProps<Group> = {
data: { childrenNodeIds: ['idAdapter', 'idBridge'], title: 'The group title', isOpen: true },
...MOCK_DEFAULT_NODE,
}

export const MOCK_NODE_DEVICE: NodeProps<ProtocolAdapter> = {
id: 'idDevice',
type: NodeTypes.DEVICE_NODE,
data: mockProtocolAdapter,
...MOCK_DEFAULT_NODE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const mockProtocolAdapter: ProtocolAdapter = {
configSchema: mockJSONSchema,
uiSchema: mockUISchema,
installed: true,
capabilities: ['READ', 'DISCOVER'],
category: {
description: 'Industrial, typically field bus protocols.',
displayName: 'Industrial',
Expand Down
1 change: 0 additions & 1 deletion hivemq-edge/src/frontend/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,6 @@
"showTopics": "Topics",
"showStatus": "Status indicators",
"showMonitoringOnEdge": "Observability from the links",
"showHosts": "Hosts",
"showGateway": "Gateways"
},
"layout": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('useGetAdapterInfo', () => {
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy()
})
expect(result.current.isDiscoverable).toBeFalsy()
expect(result.current.isDiscoverable).toBeTruthy()
expect(result.current.adapter).toStrictEqual(
expect.objectContaining({
id: 'my-adapter',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
NodeGroup,
NodeListener,
NodeHost,
NodeDevice,
} from '@/modules/Workspace/components/nodes'

const ReactFlowWrapper = () => {
Expand All @@ -35,6 +36,7 @@ const ReactFlowWrapper = () => {
[NodeTypes.BRIDGE_NODE]: NodeBridge,
[NodeTypes.LISTENER_NODE]: NodeListener,
[NodeTypes.HOST_NODE]: NodeHost,
[NodeTypes.DEVICE_NODE]: NodeDevice,
}),
[]
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CONFIG_ADAPTER_WIDTH } from '../../utils/nodes-utils.ts'
import { useEdgeFlowContext } from '../../hooks/useEdgeFlowContext.ts'
import { useContextMenu } from '../../hooks/useContextMenu.ts'
import { TopicFilter } from '../../types.ts'
import { isBidirectional } from '@/modules/Workspace/utils/adapter.utils.ts'

const NodeAdapter: FC<NodeProps<Adapter>> = ({ id, data: adapter, selected }) => {
const { data: protocols } = useGetAdapterTypes()
Expand All @@ -25,6 +26,7 @@ const NodeAdapter: FC<NodeProps<Adapter>> = ({ id, data: adapter, selected }) =>
}, [adapter.config, adapterProtocol])
const { onContextMenu } = useContextMenu(id, selected, `/edge-flow/node/adapter/${adapter.type}`)

const HACK_BIDIRECTIONAL = isBidirectional(adapterProtocol)
return (
<>
<NodeWrapper
Expand All @@ -50,6 +52,7 @@ const NodeAdapter: FC<NodeProps<Adapter>> = ({ id, data: adapter, selected }) =>
</VStack>
</NodeWrapper>
<Handle type="source" position={Position.Bottom} id="Bottom" isConnectable={false} />
{HACK_BIDIRECTIONAL && <Handle type="source" position={Position.Top} id="Top" isConnectable={false} />}
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { MOCK_NODE_DEVICE } from '@/__test-utils__/react-flow/nodes.ts'
import { mockReactFlow } from '@/__test-utils__/react-flow/providers.tsx'

import { NodeDevice } from '@/modules/Workspace/components/nodes/index.ts'

describe('NodeDevice', () => {
beforeEach(() => {
cy.viewport(400, 400)
})

it.only('should render properly', () => {
cy.mountWithProviders(mockReactFlow(<NodeDevice {...MOCK_NODE_DEVICE} />))

cy.getByTestId('device-description')
.should('have.text', 'Simulation')
.find('svg')
.should('have.attr', 'data-type', 'INDUSTRIAL')

cy.getByTestId('device-capabilities').find('svg').as('capabilities').should('have.length', 2)
cy.get('@capabilities').eq(0).should('have.attr', 'data-type', 'READ')
cy.get('@capabilities').eq(1).should('have.attr', 'data-type', 'DISCOVER')
})

it('should be accessible', () => {
cy.injectAxe()
cy.mountWithProviders(mockReactFlow(<NodeDevice {...MOCK_NODE_DEVICE} />))
cy.checkAccessibility()
cy.percySnapshot('Component: NodeDevice')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FC } from 'react'
import { Handle, Position, NodeProps } from 'reactflow'
import { HStack, Icon, Text, VStack } from '@chakra-ui/react'

import { ProtocolAdapter } from '@/api/__generated__'
import NodeWrapper from '@/modules/Workspace/components/parts/NodeWrapper.tsx'
import { deviceCapabilityIcon, deviceCategoryIcon } from '@/modules/Workspace/utils/adapter.utils.ts'

const NodeDevice: FC<NodeProps<ProtocolAdapter>> = ({ selected, data }) => {
const { category, capabilities } = data
return (
<>
<NodeWrapper
isSelected={selected}
wordBreak="break-word"
maxW={200}
textAlign="center"
p={3}
borderTopRadius={30}
>
<VStack>
<HStack w="100%" justifyContent="flex-end" gap={1} data-testid="device-capabilities">
{capabilities?.map((capability) => (
<Icon key={capability} boxSize={4} as={deviceCapabilityIcon[capability]} data-type={capability} />
))}
</HStack>
<HStack w="100%" data-testid="device-description">
<Icon as={deviceCategoryIcon[category?.name || 'SIMULATION']} data-type={category?.name} />
<Text>{data.protocol}</Text>
</HStack>
</VStack>
</NodeWrapper>
<Handle type="target" position={Position.Bottom} isConnectable={false} />
</>
)
}

export default NodeDevice
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import NodeBridge from './NodeBridge.tsx'
import NodeEdge from './NodeEdge.tsx'
import NodeListener from './NodeListener.tsx'
import NodeHost from './NodeHost.tsx'
import NodeDevice from './NodeDevice.tsx'

export { NodeGroup, NodeAdapter, NodeBridge, NodeEdge, NodeListener, NodeHost }
export { NodeGroup, NodeAdapter, NodeBridge, NodeEdge, NodeListener, NodeHost, NodeDevice }
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { render, act } from '@testing-library/react'
import { EdgeFlowContext, EdgeFlowProvider } from './FlowContext.tsx'
import { EdgeFlowOptions } from '@/modules/Workspace/types.ts'

const optionKeys = ['showTopics', 'showStatus', 'showMetrics', 'showGateway', 'showHosts']
const optionKeys = ['showTopics', 'showStatus', 'showMetrics', 'showGateway']

const ProviderTestingMock = () => {
const context = useContext(EdgeFlowContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const defaultEdgeFlowContext: EdgeFlowOptions = {
showTopics: true,
showStatus: true,
showGateway: false,
showHosts: false,
}

const defaultEdgeFlowGrouping: EdgeFlowGrouping = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ describe('useEdgeFlowContext', () => {
const { result } = renderHook(() => useEdgeFlowContext(), { wrapper })
expect(result.current.options).toEqual<EdgeFlowOptions>({
showGateway: false,
showHosts: false,
showTopics: true,
showStatus: true,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,10 @@ describe('useGetFlowElements', () => {
})

it.each<[Partial<EdgeFlowOptions>, number, number]>([
[{}, 3, 2],
[{ showGateway: true }, 4, 3],
[{ showGateway: false }, 3, 2],
[{ showHosts: true }, 4, 3],
[{ showHosts: false }, 3, 2],
[{ showGateway: true, showHosts: true }, 5, 4],
[{}, 4, 3],
[{ showGateway: true }, 5, 4],
[{ showGateway: false }, 4, 3],
[{ showGateway: true }, 5, 4],
])('should consider %s for %s nodes and %s edges', async (defaults, countNode, countEdge) => {
const wrapper: React.JSXElementConstructor<{ children: React.ReactElement }> = ({ children }) => (
<SimpleWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,15 @@ const useGetFlowElements = () => {
)
nodes.push(nodeBridge)
edges.push(edgeConnector)
if (options.showHosts) {
nodes.push(nodeHost)
edges.push(hostConnector)
}

nodes.push(nodeHost)
edges.push(hostConnector)
})

adapters.forEach((adapter, incAdapterNb) => {
const type = adapterTypes?.items?.find((e) => e.id === adapter.type)

const { nodeAdapter, edgeConnector } = createAdapterNode(
const { nodeAdapter, edgeConnector, nodeDevice, deviceConnector } = createAdapterNode(
type as ProtocolAdapter,
adapter,
incAdapterNb,
Expand All @@ -71,6 +70,11 @@ const useGetFlowElements = () => {
)
nodes.push(nodeAdapter)
edges.push(edgeConnector)

if (nodeDevice && deviceConnector) {
nodes.push(nodeDevice)
edges.push(deviceConnector)
}
})

setNodes([nodeEdge, ...applyLayout(nodes, groups)])
Expand Down
3 changes: 2 additions & 1 deletion hivemq-edge/src/frontend/src/modules/Workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Edge, Node, OnEdgesChange, OnNodesChange, NodeAddChange, EdgeAddChange,
export interface EdgeFlowOptions {
showTopics: boolean
showStatus: boolean
showHosts: boolean
showGateway: boolean
}

Expand All @@ -24,6 +23,7 @@ export enum NodeTypes {
LISTENER_NODE = 'LISTENER_NODE',
CLUSTER_NODE = 'CLUSTER_NODE',
HOST_NODE = 'HOST_NODE',
DEVICE_NODE = 'DEVICE_NODE',
}

export enum EdgeTypes {
Expand All @@ -35,6 +35,7 @@ export enum IdStubs {
BRIDGE_NODE = 'bridge',
ADAPTER_NODE = 'adapter',
HOST_NODE = 'host',
DEVICE_NODE = 'device',
GROUP_NODE = 'group',
LISTENER_NODE = 'listener',
CONNECTOR = 'connect',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { expect } from 'vitest'
import { isBidirectional } from '@/modules/Workspace/utils/adapter.utils.ts'
import { mockProtocolAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__'

describe('isBidirectional', () => {
it('should return the layout characteristics of a group', async () => {
expect(isBidirectional(mockProtocolAdapter)).toStrictEqual(false)
expect(isBidirectional({ ...mockProtocolAdapter, id: 'opc-ua-client' })).toStrictEqual(true)
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ProtocolAdapter } from '@/api/__generated__'
import { IconType } from 'react-icons'
import { TbSettingsAutomation } from 'react-icons/tb'
import { FaIndustry } from 'react-icons/fa6'
import { GrConnectivity } from 'react-icons/gr'
import { AiFillExperiment } from 'react-icons/ai'
import { MdInput, MdOutlineFindInPage, MdOutput } from 'react-icons/md'

/**
* @deprecated This is a mock, replacing the missing WRITE capability from the adapters
Expand All @@ -7,3 +13,27 @@ import { ProtocolAdapter } from '@/api/__generated__'
export const isBidirectional = (adapter: ProtocolAdapter | undefined) => {
return Boolean(adapter?.id?.includes('opc-ua-client'))
}

/**
* @deprecated This is a mock, mapping should be based on ProtocolAdapterCategory and image property
* @see ProtocolAdapterCategory
*/
export const deviceCategoryIcon: Record<string, IconType> = {
BUILDING_AUTOMATION: TbSettingsAutomation,
INDUSTRIAL: FaIndustry,
CONNECTIVITY: GrConnectivity,
SIMULATION: AiFillExperiment,
}

type ArrayElement<ArrayType extends Array<unknown>> = ArrayType[number]
type CapabilitiesArray = NonNullable<ProtocolAdapter['capabilities']>
type CapabilityType = ArrayElement<CapabilitiesArray> | 'WRITE'

/**
* @deprecated This is a mock, replacing the missing WRITE capability from the adapters
*/
export const deviceCapabilityIcon: Record<CapabilityType, IconType> = {
['READ']: MdOutput,
['DISCOVER']: MdOutlineFindInPage,
['WRITE']: MdInput,
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Adapter, Bridge, Status, Listener, ProtocolAdapter } from '@/api/__gene
import { EdgeTypes, IdStubs, NodeTypes } from '../types.ts'
import { getBridgeTopics, discoverAdapterTopics } from '../utils/topics-utils.ts'
import { getThemeForStatus } from '@/modules/Workspace/utils/status-utils.ts'
import { isBidirectional } from '@/modules/Workspace/utils/adapter.utils.ts'

export const CONFIG_ADAPTER_WIDTH = 245

Expand Down Expand Up @@ -190,7 +191,44 @@ export const createAdapterNode = (
},
}

return { nodeAdapter, edgeConnector }
let nodeDevice: Node<ProtocolAdapter, NodeTypes.DEVICE_NODE> | undefined = undefined
let deviceConnector: Edge | undefined = undefined

const HACK_BIDIRECTIONAL = isBidirectional(type)
if (HACK_BIDIRECTIONAL) {
const idBAdapterDevice = `${IdStubs.DEVICE_NODE}@${idAdapter}`
nodeDevice = {
id: idBAdapterDevice,
type: NodeTypes.DEVICE_NODE,
targetPosition: Position.Top,
data: type,
position: positionStorage?.[idBAdapterDevice] ?? {
x: nodeAdapter.position.x + 48,
y: nodeAdapter.position.y - 250,
},
}

deviceConnector = {
id: `${IdStubs.CONNECTOR}-${IdStubs.DEVICE_NODE}@${idAdapter}`,
target: idBAdapterDevice,
sourceHandle: 'Top',
source: idAdapter,
focusable: false,
markerEnd: {
type: MarkerType.ArrowClosed,
width: 20,
height: 20,
color: getThemeForStatus(theme, adapter.status),
},
animated: isConnected && !!topics.length,
style: {
strokeWidth: isConnected ? 1.5 : 0.5,
stroke: getThemeForStatus(theme, adapter.status),
},
}
}

return { nodeAdapter, edgeConnector, nodeDevice, deviceConnector }
}

export const getDefaultMetricsFor = (node: Node): string[] => {
Expand Down

0 comments on commit 3598f53

Please sign in to comment.