Skip to content

Commit

Permalink
Merge pull request #152
Browse files Browse the repository at this point in the history
feat(139): Custom editing field for topics in the Bridge form

* refactor(139): refactor custom component to allow support for multi t…

* refactor(139): refactor custom component to allow support for multi t…

* feat(139): add component for single and multiple topics editing

* refactor(139): renaming dependencies

* refactor(139): refactor topic component to allow custom styles and co…

* feat(139): add custom renderer for the multiple values

* feat(139): add existing topics as options to the components

* refactor(139): refactor form to use the topic editor

* test(139): add cypress intercept cleaning command

* test(139): replace the QueryClient of the wrapper to ensure new conte…

* test(139): add tests for multiple topics

* refactor(139): improve rendering of the topic string

* chore(139): update README and dotenv files

* test(139): fix test for topic rendering
  • Loading branch information
vanch3d authored Oct 12, 2023
1 parent e3c8d5d commit 00bd037
Show file tree
Hide file tree
Showing 17 changed files with 277 additions and 86 deletions.
1 change: 1 addition & 0 deletions hivemq-edge/src/frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions hivemq-edge/src/frontend/.env.example
Original file line number Diff line number Diff line change
@@ -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
60 changes: 37 additions & 23 deletions hivemq-edge/src/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br/>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

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -87,16 +100,17 @@ pnpm run dev:openAPI
```

```shell
openapi --input <path>/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`
Expand Down
3 changes: 3 additions & 0 deletions hivemq-edge/src/frontend/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,10 +37,12 @@ declare global {
checkAccessibility: typeof checkAccessibility
getByTestId: typeof getByTestId
getByAriaLabel: typeof getByAriaLabel
clearInterceptList: typeof clearInterceptList
}
}
}

Cypress.Commands.add('getByTestId', getByTestId)
Cypress.Commands.add('getByAriaLabel', getByAriaLabel)
Cypress.Commands.add('checkAccessibility', checkAccessibility)
Cypress.Commands.add('clearInterceptList', clearInterceptList)
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
5 changes: 2 additions & 3 deletions hivemq-edge/src/frontend/cypress/support/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,7 +52,7 @@ Cypress.Commands.add('mountWithProviders', (component, options = {}) => {
const { routerProps = { initialEntries: ['/'] }, ...mountOptions } = options

const wrapped = (
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={new QueryClient()}>
<ChakraProvider theme={themeHiveMQ}>
<AuthProvider>
<MemoryRouter {...routerProps}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -12,7 +14,7 @@ describe('Topic', () => {
it('should render', () => {
cy.mountWithProviders(<Topic topic={MOCK_TOPIC_REF1} />)

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', () => {
Expand Down
17 changes: 10 additions & 7 deletions hivemq-edge/src/frontend/src/components/MQTT/Topic.tsx
Original file line number Diff line number Diff line change
@@ -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<TopicProps> = ({ topic }) => {
const Topic: FC<TopicProps> = ({ topic, ...rest }) => {
const expandedTopic = typeof topic === 'string' ? formatTopicString(topic) : topic
return (
<Tag data-testid={'topic-wrapper'}>
<Tag data-testid={'topic-wrapper'} {...rest} letterSpacing={'-0.05rem'}>
<TopicIcon boxSize="12px" mr={2} />
<TagLabel>{topic}</TagLabel>
{typeof topic === 'string' ? <TagLabel>{expandedTopic}</TagLabel> : topic}
</Tag>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/// <reference types="cypress" />

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)
})
Expand All @@ -13,7 +15,7 @@ describe('TopicCreatableSelect', () => {
const mockOnChange = cy.stub().as('onChange')

cy.mountWithProviders(
<TopicCreatableSelect options={[]} isLoading={false} id={MOCK_ID} value={''} onChange={mockOnChange} />
<SingleTopicCreatableSelect options={[]} isLoading={false} id={MOCK_ID} value={''} onChange={mockOnChange} />
)

cy.get('#my-id').click()
Expand All @@ -25,7 +27,7 @@ describe('TopicCreatableSelect', () => {
const mockOptions = ['topic/1', 'topic/2']

cy.mountWithProviders(
<TopicCreatableSelect
<SingleTopicCreatableSelect
options={mockOptions}
isLoading={false}
id={MOCK_ID}
Expand All @@ -46,7 +48,7 @@ describe('TopicCreatableSelect', () => {
cy.injectAxe()

cy.mountWithProviders(
<TopicCreatableSelect
<SingleTopicCreatableSelect
options={mockOptions}
isLoading={false}
id={MOCK_ID}
Expand All @@ -59,3 +61,69 @@ describe('TopicCreatableSelect', () => {
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(<MultiTopicsCreatableSelect id={MOCK_ID} value={[]} onChange={mockOnChange} />)

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(<MultiTopicsCreatableSelect id={MOCK_ID} value={[]} onChange={mockOnChange} />)
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(<MultiTopicsCreatableSelect id={MOCK_ID} value={['old topic']} onChange={mockOnChange} />)
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')
})
})
Loading

0 comments on commit 00bd037

Please sign in to comment.