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} - + ) }