diff --git a/hivemq-edge/src/frontend/.env b/hivemq-edge/src/frontend/.env
index dc092c5956..d2ee9e9725 100644
--- a/hivemq-edge/src/frontend/.env
+++ b/hivemq-edge/src/frontend/.env
@@ -5,3 +5,4 @@ VITE_APP_ISSUES=https://hivemq.kanbanize.com/ctrl_board/57/
VITE_FLAG_MOCK_SERVER=false
VITE_FLAG_FACET_SEARCH=true
VITE_FLAG_WORKSPACE_FLOW_PANEL=true
+VITE_FLAG_TOPIC_EDITOR_SHOW_BRANCHES=false
diff --git a/hivemq-edge/src/frontend/.env.example b/hivemq-edge/src/frontend/.env.example
index 580b60e8f0..8a82e4d448 100644
--- a/hivemq-edge/src/frontend/.env.example
+++ b/hivemq-edge/src/frontend/.env.example
@@ -1,3 +1,6 @@
VITE_API_BASE_URL="the base URL of the API server"
PERCY_TOKEN="The token from Percy project"
+PERCY_BRANCH=local
+PERCY_PARALLEL_TOTAL=-1
+PERCY_PARALLEL_NONCE=1234
diff --git a/hivemq-edge/src/frontend/README.md b/hivemq-edge/src/frontend/README.md
index ef943b0bec..4773434816 100644
--- a/hivemq-edge/src/frontend/README.md
+++ b/hivemq-edge/src/frontend/README.md
@@ -4,14 +4,19 @@ HiveMQ Edge Edition
## Libraries
-| Library | Description | Link |
-| ----------- | ----------------------------------------------------------------------- | -------------------------------------------- |
-| React | Main UI Framework to build reactive frontend applications | https://react.dev/ |
-| React Query | Data-fetching library for web applications | https://tanstack.com/query/latest/docs/react |
-| Axios | Use modern network handler to send, resend or intercept network request | https://github.com/axios/axios |
-| Chakra UI | Simple, modular and accessible UI component library | https://chakra-ui.com/ |
-| React icons | Icons with ES6 imports | https://react-icons.github.io/react-icons/ |
-| i18next | Internationalisation framework | https://react.i18next.com/ |
+| Library | Description | Link |
+| --------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------- |
+| React | Main UI Framework to build reactive frontend applications | https://react.dev/ |
+| React Query | Data-fetching library for web applications | https://tanstack.com/query/latest/docs/react |
+| Axios | Use modern network handler to send, resend or intercept network request | https://github.com/axios/axios |
+| Chakra UI | Simple, modular and accessible UI component library | https://chakra-ui.com/ |
+| React icons | Icons with ES6 imports | https://react-icons.github.io/react-icons/ |
+| i18next | Internationalisation framework | https://react.i18next.com/ |
+| react-jsonschema-form | A React component for building Web forms from JSON Schema.
Supports ChakraUI | https://github.com/rjsf-team/react-jsonschema-form/ |
+| chakra-react-select | A Chakra UI themed wrapper for the popular library React Select | https://github.com/csandman/chakra-react-select |
+| reactflow | Highly customizable library for building interactive node-based UI | https://reactflow.dev/ |
+| d3 | The JavaScript library for bespoke data visualization | https://d3js.org/ |
+| luxon | A powerful, modern, and friendly wrapper for JavaScript dates and times. | https://moment.github.io/luxon/#/ |
## Development
@@ -22,6 +27,13 @@ Install the following dependencies to start the development of the project.
| PNPM | Node package manager to install frontend dependencies | https://pnpm.io/installation |
| NVM | Node.js version manager | https://github.com/nvm-sh/nvm#installing-and-updating |
+On a MacOS, you can simply use the following two commands
+
+```shell
+brew install pnpm
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
+```
+
Then you can install the node.js version of the project by running
```shell
@@ -36,16 +48,6 @@ pnpm install --frozen-lockfile
**That's it happy development** 🎉
-### Run
-
-Start the application locally by running
-
-```shell
-pnpm dev
-```
-
-The web app will be running on `http://localhost:5173/
-
### Configuration
The application uses the "dotenv" pattern for configuring environment variables, see https://vitejs.dev/guide/env-and-mode.html.
@@ -61,12 +63,23 @@ Additional environment variables are loaded from the following files:
The `.env` file contains the main variables safe to be committed in git.
A `env.example` file has been created with all the values expected to be created
either in a `.env.local` or `.env.production.local`, both of them excluded from git.
-They are :
+
+Add the following keys into your `.env.local` :
```dotenv
VITE_API_BASE_URL="the base URL of the API server, ie http://localhost:8080"
```
+### Run
+
+Start the application locally by running
+
+```shell
+pnpm dev
+```
+
+The web app will be running on http://localhost:3000/app/login
+
### Deployment
To build a version ready for deployment, run the command
@@ -87,16 +100,17 @@ pnpm run dev:openAPI
```
```shell
-openapi --input /hivemq-edge-openapi.yaml \
+openapi --input '../../../../hivemq-edge/ext/hivemq-edge-openapi-2023.7.yaml' \
-o ./src/api/__generated__ \
- -c axios \
- --name HiveMqClient
+ -c axios \
+ --name HiveMqClient \
+ --exportSchemas true
```
with the following options:
- the `OpenAPI` spec is given as a local path, assuming deployment of the backend locally. It could also be a URL or even the string content.
- The current spec can be found in [hivemq/hivemq-edge-test](https://github.com/hivemq/hivemq-edge-test/blob/master/src/test/resources/hivemq-edge-openapi.yaml)
+ The current spec can be found in `/hivemq-edge/ext/hivemq-edge-openapi-2023.7.yaml`)
- all files are created in the `./src/api/__generated__` folder and are not expected to be modified manually (safe from `eslint` and `prettier`)
- `axios` is used for the HTTP client
- a custom client, to configure individual instances, is created as `HiveMqClient`
diff --git a/hivemq-edge/src/frontend/cypress/support/commands.ts b/hivemq-edge/src/frontend/cypress/support/commands.ts
index 1c6af337d0..97acec1a17 100644
--- a/hivemq-edge/src/frontend/cypress/support/commands.ts
+++ b/hivemq-edge/src/frontend/cypress/support/commands.ts
@@ -28,6 +28,7 @@
import { getByTestId } from './commands/getByTestId.ts'
import { getByAriaLabel } from './commands/getByAriaLabel.ts'
import { checkAccessibility } from './commands/checkAccessibility.ts'
+import { clearInterceptList } from './commands/clearInterceptList.ts'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@@ -36,6 +37,7 @@ declare global {
checkAccessibility: typeof checkAccessibility
getByTestId: typeof getByTestId
getByAriaLabel: typeof getByAriaLabel
+ clearInterceptList: typeof clearInterceptList
}
}
}
@@ -43,3 +45,4 @@ declare global {
Cypress.Commands.add('getByTestId', getByTestId)
Cypress.Commands.add('getByAriaLabel', getByAriaLabel)
Cypress.Commands.add('checkAccessibility', checkAccessibility)
+Cypress.Commands.add('clearInterceptList', clearInterceptList)
diff --git a/hivemq-edge/src/frontend/cypress/support/commands/clearInterceptList.ts b/hivemq-edge/src/frontend/cypress/support/commands/clearInterceptList.ts
new file mode 100644
index 0000000000..9d1a5c28d5
--- /dev/null
+++ b/hivemq-edge/src/frontend/cypress/support/commands/clearInterceptList.ts
@@ -0,0 +1,12 @@
+// @ts-ignore an import is not working
+import { Interception } from 'cypress/types/net-stubbing'
+
+export const clearInterceptList = (interceptAlias: string): void => {
+ cy.get(interceptAlias + '.all').then((browserRequests: Interception) => {
+ for (const request of browserRequests) {
+ if (!request.requestWaited && request.state !== 'Received') {
+ cy.wait(interceptAlias)
+ }
+ }
+ })
+}
diff --git a/hivemq-edge/src/frontend/cypress/support/component.tsx b/hivemq-edge/src/frontend/cypress/support/component.tsx
index 481bf42069..1e8c7f01ab 100644
--- a/hivemq-edge/src/frontend/cypress/support/component.tsx
+++ b/hivemq-edge/src/frontend/cypress/support/component.tsx
@@ -24,11 +24,10 @@ import './commands'
import { mount, MountOptions, MountReturn } from 'cypress/react18'
import { MemoryRouterProps, MemoryRouter } from 'react-router-dom'
import { ChakraProvider, VisuallyHidden } from '@chakra-ui/react'
-import { QueryClientProvider } from '@tanstack/react-query'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AuthProvider } from '@/modules/Auth/AuthProvider.tsx'
import { themeHiveMQ } from '@/modules/Theme/themeHiveMQ.ts'
-import queryClient from '@/api/queryClient.ts'
import '@/config/i18n.config.ts'
// Augment the Cypress namespace to include type definitions for
@@ -53,7 +52,7 @@ Cypress.Commands.add('mountWithProviders', (component, options = {}) => {
const { routerProps = { initialEntries: ['/'] }, ...mountOptions } = options
const wrapped = (
-
+
diff --git a/hivemq-edge/src/frontend/src/components/MQTT/Topic.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/MQTT/Topic.spec.cy.tsx
index 3a0a45df8d..8ecf9c95ae 100644
--- a/hivemq-edge/src/frontend/src/components/MQTT/Topic.spec.cy.tsx
+++ b/hivemq-edge/src/frontend/src/components/MQTT/Topic.spec.cy.tsx
@@ -2,6 +2,8 @@
import { MOCK_TOPIC_REF1 } from '@/__test-utils__/react-flow/topics.ts'
+import { formatTopicString } from '@/components/MQTT/topic-utils.ts'
+
import Topic from './Topic.tsx'
describe('Topic', () => {
@@ -12,7 +14,7 @@ describe('Topic', () => {
it('should render', () => {
cy.mountWithProviders()
- cy.getByTestId('topic-wrapper').should('contain.text', 'root/topic/ref/1')
+ cy.getByTestId('topic-wrapper').should('contain.text', formatTopicString('root/topic/ref/1'))
})
it('should be accessible', () => {
diff --git a/hivemq-edge/src/frontend/src/components/MQTT/Topic.tsx b/hivemq-edge/src/frontend/src/components/MQTT/Topic.tsx
index 6bbb7953db..4556ec1e72 100644
--- a/hivemq-edge/src/frontend/src/components/MQTT/Topic.tsx
+++ b/hivemq-edge/src/frontend/src/components/MQTT/Topic.tsx
@@ -1,17 +1,20 @@
-import { FC } from 'react'
-import { Tag, TagLabel } from '@chakra-ui/react'
+import { FC, ReactNode } from 'react'
+import { Tag, TagLabel, TagProps } from '@chakra-ui/react'
import TopicIcon from '@/components/Icons/TopicIcon.tsx'
+import { formatTopicString } from '@/components/MQTT/topic-utils.ts'
-interface TopicProps {
- topic: string
+// TODO[NVL] Not sure adding ReactNode as possible children is a good move.
+interface TopicProps extends TagProps {
+ topic: ReactNode
}
-const Topic: FC = ({ topic }) => {
+const Topic: FC = ({ topic, ...rest }) => {
+ const expandedTopic = typeof topic === 'string' ? formatTopicString(topic) : topic
return (
-
+
- {topic}
+ {typeof topic === 'string' ? {expandedTopic} : topic}
)
}
diff --git a/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.spec.cy.tsx
index 4ca5dfd5b9..54b907dac9 100644
--- a/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.spec.cy.tsx
+++ b/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.spec.cy.tsx
@@ -1,10 +1,12 @@
///
-import TopicCreatableSelect from './TopicCreatableSelect.tsx'
+import { MultiTopicsCreatableSelect, SingleTopicCreatableSelect } from './TopicCreatableSelect.tsx'
+import { mockAdapter, mockProtocolAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__'
+import { mockBridge } from '@/api/hooks/useGetBridges/__handlers__'
const MOCK_ID = 'my-id'
-describe('TopicCreatableSelect', () => {
+describe('SingleTopicCreatableSelect', () => {
beforeEach(() => {
cy.viewport(450, 250)
})
@@ -13,7 +15,7 @@ describe('TopicCreatableSelect', () => {
const mockOnChange = cy.stub().as('onChange')
cy.mountWithProviders(
-
+
)
cy.get('#my-id').click()
@@ -25,7 +27,7 @@ describe('TopicCreatableSelect', () => {
const mockOptions = ['topic/1', 'topic/2']
cy.mountWithProviders(
- {
cy.injectAxe()
cy.mountWithProviders(
- {
cy.percySnapshot('Component: TopicCreatableSelect')
})
})
+
+describe.only('MultiTopicsCreatableSelect', () => {
+ beforeEach(() => {
+ cy.viewport(450, 250)
+ })
+
+ it('should render an empty component', () => {
+ const mockOnChange = cy.stub().as('onChange')
+ cy.intercept('/api/v1/management/protocol-adapters/types', { items: [] }).as('getConfig1')
+ cy.intercept('/api/v1/management/protocol-adapters/adapters', { items: [] }).as('getConfig2')
+ cy.intercept('/api/v1/management/bridges', { items: [] }).as('getConfig3')
+
+ cy.mountWithProviders()
+
+ cy.wait(['@getConfig1', '@getConfig2', '@getConfig3'])
+ cy.get('#my-id').click()
+ cy.get('#react-select-2-listbox').contains('No topic loaded')
+ })
+
+ it('should render a single topic', () => {
+ const mockOnChange = cy.stub().as('onChange')
+ cy.intercept('/api/v1/management/protocol-adapters/types', { items: [mockProtocolAdapter] }).as('getConfig1')
+ cy.intercept('/api/v1/management/protocol-adapters/adapters', { items: [mockAdapter] }).as('getConfig2')
+ cy.intercept('/api/v1/management/bridges', { items: [mockBridge] }).as('getConfig3')
+
+ cy.mountWithProviders()
+ cy.wait(['@getConfig1', '@getConfig2', '@getConfig3'])
+
+ cy.get('#my-id').click()
+
+ cy.get('#react-select-3-listbox').contains('#')
+
+ cy.get('#my-id').type('123')
+ cy.get('#react-select-3-listbox').contains('Add the topic ... 123')
+ // cy.get('#my-id').type('{Enter}')
+
+ cy.get('#react-select-3-option-4').click()
+
+ cy.get('@onChange').should('have.been.calledWith', ['123'])
+ })
+
+ it('should render multiple topics', () => {
+ const mockOnChange = cy.stub().as('onChange')
+ cy.intercept('/api/v1/management/protocol-adapters/types', { items: [mockProtocolAdapter] }).as('getConfig1')
+ cy.intercept('/api/v1/management/protocol-adapters/adapters', { items: [mockAdapter] }).as('getConfig2')
+ cy.intercept('/api/v1/management/bridges', { items: [mockBridge] }).as('getConfig3')
+
+ cy.mountWithProviders()
+ cy.wait(['@getConfig1', '@getConfig2', '@getConfig3'])
+
+ cy.get('#my-id').contains('old topic')
+ cy.get('#my-id').click()
+
+ cy.get('#react-select-4-listbox').contains('#')
+
+ cy.get('#my-id').type('123')
+ cy.get('#react-select-4-listbox').contains('Add the topic ... 123')
+ // cy.get('#my-id').type('{Enter}')
+
+ cy.get('#react-select-4-option-4').click()
+
+ cy.get('@onChange').should('have.been.calledWith', ['old topic', '123'])
+
+ cy.clearInterceptList('@getConfig3')
+ })
+})
diff --git a/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.tsx b/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.tsx
index e437a50b72..4fd7973a79 100644
--- a/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.tsx
+++ b/hivemq-edge/src/frontend/src/components/MQTT/TopicCreatableSelect.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react'
+import { RefAttributes } from 'react'
import {
CreatableSelect,
createFilter,
@@ -6,10 +6,15 @@ import {
SingleValue,
SelectComponentsConfig,
GroupBase,
+ CreatableProps,
+ SelectInstance,
chakraComponents,
} from 'chakra-react-select'
import { useTranslation } from 'react-i18next'
+
import TopicIcon from '@/components/Icons/TopicIcon.tsx'
+import Topic from '@/components/MQTT/Topic.tsx'
+import { useGetEdgeTopics } from '@/hooks/useGetEdgeTopics/useGetEdgeTopics.tsx'
interface TopicOption extends OptionBase {
label: string
@@ -17,19 +22,8 @@ interface TopicOption extends OptionBase {
iconColor: string
}
-interface TopicSelectProps {
- id: string
- options: string[]
- isLoading: boolean
- value: string
- onChange: (value: string | undefined) => void
-}
-
-const filterConfig = {
- trim: false,
-}
-
-const customComponents: SelectComponentsConfig> = {
+const customComponents = (isMulti: boolean): SelectComponentsConfig> => ({
+ DropdownIndicator: null,
Option: ({ children, ...props }) => (
{props.data.iconColor && }
@@ -39,20 +33,50 @@ const customComponents: SelectComponentsConfig (
-
+ {!isMulti && }
{children}
),
+
+ // @ts-ignore Check why sx is not recognised by TS
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ MultiValueContainer: ({ children, sx, ...props }) => {
+ return (
+
+
+
+ )
+ },
+})
+
+// Recreating the type for the CreatableSelect component
+type TopicCreatableSelect = CreatableProps> &
+ RefAttributes>>
+
+interface TopicCreatableSelectProps
+ extends Partial, 'options'>> {
+ id: string
+ options: string[]
}
-const TopicCreatableSelect: FC = ({ id, options, isLoading, value, onChange }) => {
+const AbstractTopicCreatableSelect = ({
+ id,
+ options,
+ isLoading,
+ isMulti,
+ ...rest
+}: TopicCreatableSelectProps) => {
const topicOptions = Array.from(new Set([...options]))
.sort()
.map((e) => ({ label: e, value: e, iconColor: 'brand.500' }))
const { t } = useTranslation('components')
+ const filterConfig = {
+ trim: false,
+ }
+
return (
- >
aria-label={t('topicCreate.label') as string}
placeholder={t('topicCreate.placeholder') as string}
noOptionsMessage={() => t('topicCreate.options.noOptionsMessage')}
@@ -61,19 +85,50 @@ const TopicCreatableSelect: FC = ({ id, options, isLoading, va
id={id}
isClearable
isSearchable
- isMulti={false}
+ isMulti={isMulti}
options={topicOptions}
- value={value ? { label: value, value: value, iconColor: 'brand.200' } : undefined}
- onChange={(value) => {
- const newValue = value as SingleValue
- onChange(newValue?.label)
- }}
- components={customComponents}
+ components={isMulti === undefined ? undefined : customComponents(isMulti)}
filterOption={createFilter(filterConfig)}
// @ts-ignore TODO[NVL] Bug with CRS, see https://github.com/csandman/chakra-react-select/issues/273
selectedOptionStyle="check"
+ {...rest}
/>
)
}
-export default TopicCreatableSelect
+interface SingleTopicCreatableSelectProps extends Omit, 'value' | 'onChange'> {
+ value: string
+ onChange: (value: string | undefined) => void
+}
+
+export const SingleTopicCreatableSelect = ({ value, onChange, ...props }: SingleTopicCreatableSelectProps) => (
+
+ {...props}
+ isMulti={false}
+ value={value ? { label: value, value: value, iconColor: 'brand.200' } : undefined}
+ onChange={(value) => {
+ const newValue = value as SingleValue
+ onChange(newValue?.label)
+ }}
+ />
+)
+
+interface MultiTopicsCreatableSelectProps
+ extends Omit, 'value' | 'onChange' | 'options'> {
+ value: string[]
+ onChange: (value: string[] | undefined) => void
+}
+
+export const MultiTopicsCreatableSelect = ({ value, onChange, ...props }: MultiTopicsCreatableSelectProps) => {
+ const { data, isSuccess } = useGetEdgeTopics({ publishOnly: false })
+ return (
+
+ {...props}
+ options={data}
+ isLoading={!isSuccess}
+ isMulti={true}
+ value={value.map((e) => ({ label: e, value: e, iconColor: 'brand.200' }))}
+ onChange={(m) => onChange(m.map((e) => e.value))}
+ />
+ )
+}
diff --git a/hivemq-edge/src/frontend/src/components/MQTT/topic-utils.spec.ts b/hivemq-edge/src/frontend/src/components/MQTT/topic-utils.spec.ts
new file mode 100644
index 0000000000..bc62f38a44
--- /dev/null
+++ b/hivemq-edge/src/frontend/src/components/MQTT/topic-utils.spec.ts
@@ -0,0 +1,19 @@
+import { expect } from 'vitest'
+import { formatTopicString } from '@/components/MQTT/topic-utils.ts'
+
+interface TestEachSuite {
+ topic: string
+ expected: string
+}
+
+describe('formatTopicString', () => {
+ it.each([
+ { topic: '', expected: '' },
+ { topic: '12345', expected: '12345' },
+ { topic: '123/45', expected: '123 / 45' },
+ { topic: '12/3/45', expected: '12 / 3 / 45' },
+ { topic: '12/3/45/', expected: '12 / 3 / 45 / ' },
+ ])('should returns $expected with $topic', ({ topic, expected }) => {
+ expect(formatTopicString(topic)).toStrictEqual(expected)
+ })
+})
diff --git a/hivemq-edge/src/frontend/src/components/MQTT/topic-utils.ts b/hivemq-edge/src/frontend/src/components/MQTT/topic-utils.ts
new file mode 100644
index 0000000000..fce24b48fc
--- /dev/null
+++ b/hivemq-edge/src/frontend/src/components/MQTT/topic-utils.ts
@@ -0,0 +1,3 @@
+const TOPIC_SEPARATOR = ' / '
+
+export const formatTopicString = (topic: string) => topic.split('/').join(TOPIC_SEPARATOR)
diff --git a/hivemq-edge/src/frontend/src/modules/Bridges/components/panels/SubscriptionsPanel.tsx b/hivemq-edge/src/frontend/src/modules/Bridges/components/panels/SubscriptionsPanel.tsx
index 01d7017773..79747249a6 100644
--- a/hivemq-edge/src/frontend/src/modules/Bridges/components/panels/SubscriptionsPanel.tsx
+++ b/hivemq-edge/src/frontend/src/modules/Bridges/components/panels/SubscriptionsPanel.tsx
@@ -30,6 +30,8 @@ import { AddIcon, DeleteIcon } from '@chakra-ui/icons'
import { $BridgeSubscription } from '@/api/__generated__'
import { useValidationRules } from '@/api/hooks/useValidationRules/useValidationRules.ts'
+import { MultiTopicsCreatableSelect } from '@/components/MQTT/TopicCreatableSelect.tsx'
+
import CustomUserProperties from './CustomUserProperties.tsx'
import { BridgeSubscriptionsProps } from '../../types.ts'
@@ -54,7 +56,7 @@ const SubscriptionsPanel: FC = ({ form, type }) => {
-
+
= ({ form, type }) => {
name={`${type}.${index}.filters`}
render={({ field }) => {
const { value, onChange, ...rest } = field
- const formatValue = value.map((e) => ({ value: e, label: e }))
return (
- onChange(values.map((item) => item.value))}
+ value={value}
+ onChange={(values) => onChange(values?.map((item) => item))}
inputId={`${type}.${index}.filters`}
- isClearable={true}
- isMulti={true}
- components={{
- DropdownIndicator: null,
- }}
+ id={`${type}.${index}.container`}
/>
)
}}
diff --git a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.spec.cy.tsx
index 8796df14f4..eb29395221 100644
--- a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.spec.cy.tsx
+++ b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.spec.cy.tsx
@@ -4,7 +4,9 @@ import { mockReactFlow } from '@/__test-utils__/react-flow/providers.tsx'
import { MOCK_NODE_ADAPTER } from '@/__test-utils__/react-flow/nodes.ts'
import { MOCK_TOPIC_REF1, MOCK_TOPIC_REF2 } from '@/__test-utils__/react-flow/topics.ts'
import { MOCK_ADAPTER_ID } from '@/__test-utils__/mocks.ts'
+
import { mockProtocolAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__'
+import { formatTopicString } from '@/components/MQTT/topic-utils.ts'
import NodeAdapter from './NodeAdapter.tsx'
@@ -21,8 +23,8 @@ describe('NodeAdapter', () => {
cy.getByTestId('connection-status').should('contain.text', 'Connected')
cy.getByTestId('topics-container')
.should('be.visible')
- .should('contain.text', MOCK_TOPIC_REF1)
- .should('contain.text', MOCK_TOPIC_REF2)
+ .should('contain.text', formatTopicString(MOCK_TOPIC_REF1))
+ .should('contain.text', formatTopicString(MOCK_TOPIC_REF2))
})
it('should be accessible', () => {
diff --git a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.spec.cy.tsx
index 5751c37bfa..a3283d7161 100644
--- a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.spec.cy.tsx
+++ b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.spec.cy.tsx
@@ -2,10 +2,12 @@
import { MOCK_NODE_BRIDGE } from '@/__test-utils__/react-flow/nodes.ts'
import { mockReactFlow } from '@/__test-utils__/react-flow/providers.tsx'
+import { MOCK_TOPIC_ACT1, MOCK_TOPIC_ALL } from '@/__test-utils__/react-flow/topics.ts'
import { mockProtocolAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__'
+import { formatTopicString } from '@/components/MQTT/topic-utils.ts'
+
import NodeBridge from './NodeBridge.tsx'
-import { MOCK_TOPIC_ACT1, MOCK_TOPIC_ALL } from '@/__test-utils__/react-flow/topics.ts'
describe('NodeBridge', () => {
beforeEach(() => {
@@ -24,8 +26,14 @@ describe('NodeBridge', () => {
// .should('contain.text', MOCK_TOPIC_REF2)
cy.getByTestId('topics-container').should('have.length', 2)
- cy.getByTestId('topics-container').eq(0).should('be.visible').should('contain.text', MOCK_TOPIC_ACT1)
- cy.getByTestId('topics-container').eq(1).should('be.visible').should('contain.text', MOCK_TOPIC_ALL)
+ cy.getByTestId('topics-container')
+ .eq(0)
+ .should('be.visible')
+ .should('contain.text', formatTopicString(MOCK_TOPIC_ACT1))
+ cy.getByTestId('topics-container')
+ .eq(1)
+ .should('be.visible')
+ .should('contain.text', formatTopicString(MOCK_TOPIC_ALL))
})
it('should be accessible', () => {
diff --git a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/parts/TopicsContainer.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/parts/TopicsContainer.spec.cy.tsx
index f320f61491..c14b9fb843 100644
--- a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/parts/TopicsContainer.spec.cy.tsx
+++ b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/parts/TopicsContainer.spec.cy.tsx
@@ -1,8 +1,10 @@
///
+import { formatTopicString } from '@/components/MQTT/topic-utils.ts'
+
import TopicsContainer from './TopicsContainer.tsx'
-describe('NodeListener', () => {
+describe('TopicsContainer', () => {
beforeEach(() => {
cy.viewport(400, 400)
})
@@ -22,8 +24,8 @@ describe('NodeListener', () => {
)
cy.getByTestId('topic-wrapper').should('have.length', 2)
- cy.getByTestId('topic-wrapper').eq(0).should('contain.text', 'my/first/topic')
- cy.getByTestId('topic-wrapper').eq(1).should('contain.text', 'my/second/topic')
+ cy.getByTestId('topic-wrapper').eq(0).should('contain.text', formatTopicString('my/first/topic'))
+ cy.getByTestId('topic-wrapper').eq(1).should('contain.text', formatTopicString('my/second/topic'))
cy.getByTestId('topics-show-more').should('not.exist')
})
@@ -46,8 +48,8 @@ describe('NodeListener', () => {
)
cy.getByTestId('topic-wrapper').should('have.length', 2)
- cy.getByTestId('topic-wrapper').eq(0).should('contain.text', 'my/first/topic')
- cy.getByTestId('topic-wrapper').eq(1).should('contain.text', 'my/second/topic')
+ cy.getByTestId('topic-wrapper').eq(0).should('contain.text', formatTopicString('my/first/topic'))
+ cy.getByTestId('topic-wrapper').eq(1).should('contain.text', formatTopicString('my/second/topic'))
cy.getByTestId('topics-show-more').should('be.visible').should('contain.text', '+1')
})
diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/templates/__internals/TopicInputTemplate.tsx b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/templates/__internals/TopicInputTemplate.tsx
index e1d844c90b..a532d1c2af 100644
--- a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/templates/__internals/TopicInputTemplate.tsx
+++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/templates/__internals/TopicInputTemplate.tsx
@@ -2,7 +2,7 @@ import { FC } from 'react'
import { BaseInputTemplateProps } from '@rjsf/utils'
import { FormControl, FormLabel } from '@chakra-ui/react'
-import TopicCreatableSelect from '@/components/MQTT/TopicCreatableSelect.tsx'
+import { SingleTopicCreatableSelect } from '@/components/MQTT/TopicCreatableSelect.tsx'
import { useGetEdgeTopics } from '@/hooks/useGetEdgeTopics/useGetEdgeTopics.tsx'
import config from '@/config'
@@ -23,7 +23,7 @@ export const TopicInputTemplate: FC = (props) => {
isInvalid={rawErrors && rawErrors.length > 0}
>
{label}
-
+
)
}