diff --git a/hivemq-edge/src/frontend/package.json b/hivemq-edge/src/frontend/package.json index 0044bfd567..a6afa92729 100644 --- a/hivemq-edge/src/frontend/package.json +++ b/hivemq-edge/src/frontend/package.json @@ -11,7 +11,7 @@ "lint:prettier:write": "prettier --write .", "lint:stylelint": "stylelint '{src}/**/*.{css}'", "lint:all": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && prettier --check .", - "dev:openAPI": "openapi --input '../../../../hivemq-edge/ext/hivemq-edge-openapi-2023.6.yaml' -o ./src/api/__generated__ -c axios --name HiveMqClient --exportSchemas true", + "dev:openAPI": "openapi --input '../../../../hivemq-edge/ext/hivemq-edge-openapi-2023.7.yaml' -o ./src/api/__generated__ -c axios --name HiveMqClient --exportSchemas true", "cypress:open": "cypress open", "cypress:open:component": "cypress open --component --browser chrome", "cypress:open:e2e": "cypress open --e2e --browser chrome", diff --git a/hivemq-edge/src/frontend/src/api/__generated__/HiveMqClient.ts b/hivemq-edge/src/frontend/src/api/__generated__/HiveMqClient.ts index 5b200a0330..f47bf50a5c 100644 --- a/hivemq-edge/src/frontend/src/api/__generated__/HiveMqClient.ts +++ b/hivemq-edge/src/frontend/src/api/__generated__/HiveMqClient.ts @@ -37,7 +37,7 @@ export class HiveMqClient { constructor(config?: Partial, HttpRequest: HttpRequestConstructor = AxiosHttpRequest) { this.request = new HttpRequest({ BASE: config?.BASE ?? '', - VERSION: config?.VERSION ?? '2023.6', + VERSION: config?.VERSION ?? '2023.7', WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false, CREDENTIALS: config?.CREDENTIALS ?? 'include', TOKEN: config?.TOKEN, diff --git a/hivemq-edge/src/frontend/src/api/__generated__/core/OpenAPI.ts b/hivemq-edge/src/frontend/src/api/__generated__/core/OpenAPI.ts index 7f83b65b95..8da2350c13 100644 --- a/hivemq-edge/src/frontend/src/api/__generated__/core/OpenAPI.ts +++ b/hivemq-edge/src/frontend/src/api/__generated__/core/OpenAPI.ts @@ -21,7 +21,7 @@ export type OpenAPIConfig = { export const OpenAPI: OpenAPIConfig = { BASE: '', - VERSION: '2023.6', + VERSION: '2023.7', WITH_CREDENTIALS: false, CREDENTIALS: 'include', TOKEN: undefined, diff --git a/hivemq-edge/src/frontend/src/api/__generated__/index.ts b/hivemq-edge/src/frontend/src/api/__generated__/index.ts index 247372a2f3..6ca98666f0 100644 --- a/hivemq-edge/src/frontend/src/api/__generated__/index.ts +++ b/hivemq-edge/src/frontend/src/api/__generated__/index.ts @@ -44,6 +44,7 @@ export type { ProtocolAdaptersList } from './models/ProtocolAdaptersList'; export { Status } from './models/Status'; export type { StatusList } from './models/StatusList'; export { StatusTransitionCommand } from './models/StatusTransitionCommand'; +export { StatusTransitionResult } from './models/StatusTransitionResult'; export type { TlsConfiguration } from './models/TlsConfiguration'; export type { UsernamePasswordCredentials } from './models/UsernamePasswordCredentials'; export type { ValuesTree } from './models/ValuesTree'; @@ -82,6 +83,7 @@ export { $ProtocolAdaptersList } from './schemas/$ProtocolAdaptersList'; export { $Status } from './schemas/$Status'; export { $StatusList } from './schemas/$StatusList'; export { $StatusTransitionCommand } from './schemas/$StatusTransitionCommand'; +export { $StatusTransitionResult } from './schemas/$StatusTransitionResult'; export { $TlsConfiguration } from './schemas/$TlsConfiguration'; export { $UsernamePasswordCredentials } from './schemas/$UsernamePasswordCredentials'; export { $ValuesTree } from './schemas/$ValuesTree'; diff --git a/hivemq-edge/src/frontend/src/api/__generated__/models/StatusTransitionResult.ts b/hivemq-edge/src/frontend/src/api/__generated__/models/StatusTransitionResult.ts new file mode 100644 index 0000000000..9bd05843eb --- /dev/null +++ b/hivemq-edge/src/frontend/src/api/__generated__/models/StatusTransitionResult.ts @@ -0,0 +1,37 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type StatusTransitionResult = { + /** + * The callback timeout specifies the minimum amount of time (in milliseconds) that the API advises the client to backoff before rechecking the (runtime or connection) status of this object. This is only applicable when the status is 'PENDING'. + */ + callbackTimeoutMillis?: number; + /** + * The identifier of the object in transition + */ + identifier?: string; + /** + * The status to perform on the target connection. + */ + status?: StatusTransitionResult.status; + /** + * The type of the object in transition + */ + type?: string; +}; + +export namespace StatusTransitionResult { + + /** + * The status to perform on the target connection. + */ + export enum status { + PENDING = 'PENDING', + COMPLETE = 'COMPLETE', + } + + +} + diff --git a/hivemq-edge/src/frontend/src/api/__generated__/schemas/$StatusTransitionResult.ts b/hivemq-edge/src/frontend/src/api/__generated__/schemas/$StatusTransitionResult.ts new file mode 100644 index 0000000000..cfb8b3ef4f --- /dev/null +++ b/hivemq-edge/src/frontend/src/api/__generated__/schemas/$StatusTransitionResult.ts @@ -0,0 +1,24 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $StatusTransitionResult = { + properties: { + callbackTimeoutMillis: { + type: 'number', + description: `The callback timeout specifies the minimum amount of time (in milliseconds) that the API advises the client to backoff before rechecking the (runtime or connection) status of this object. This is only applicable when the status is 'PENDING'.`, + format: 'int32', + }, + identifier: { + type: 'string', + description: `The identifier of the object in transition`, + }, + status: { + type: 'Enum', + }, + type: { + type: 'string', + description: `The type of the object in transition`, + }, + }, +} as const; diff --git a/hivemq-edge/src/frontend/src/api/__generated__/services/BridgesService.ts b/hivemq-edge/src/frontend/src/api/__generated__/services/BridgesService.ts index f7c59be08a..c2ffd4d24d 100644 --- a/hivemq-edge/src/frontend/src/api/__generated__/services/BridgesService.ts +++ b/hivemq-edge/src/frontend/src/api/__generated__/services/BridgesService.ts @@ -7,6 +7,7 @@ import type { BridgeList } from '../models/BridgeList'; import type { Status } from '../models/Status'; import type { StatusList } from '../models/StatusList'; import type { StatusTransitionCommand } from '../models/StatusTransitionCommand'; +import type { StatusTransitionResult } from '../models/StatusTransitionResult'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; @@ -144,13 +145,13 @@ export class BridgesService { * Transition the connection status of a bridge. * @param bridgeId The id of the bridge whose runtime-status will change. * @param requestBody The command to transition the bridge runtime status. - * @returns any Success + * @returns StatusTransitionResult Success * @throws ApiError */ public changeStatus( bridgeId: string, requestBody: StatusTransitionCommand, - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'PUT', url: '/api/v1/management/bridges/{bridgeId}/status', diff --git a/hivemq-edge/src/frontend/src/api/__generated__/services/ProtocolAdaptersService.ts b/hivemq-edge/src/frontend/src/api/__generated__/services/ProtocolAdaptersService.ts index 80923a5d93..8a73c36ef3 100644 --- a/hivemq-edge/src/frontend/src/api/__generated__/services/ProtocolAdaptersService.ts +++ b/hivemq-edge/src/frontend/src/api/__generated__/services/ProtocolAdaptersService.ts @@ -8,6 +8,7 @@ import type { ProtocolAdaptersList } from '../models/ProtocolAdaptersList'; import type { Status } from '../models/Status'; import type { StatusList } from '../models/StatusList'; import type { StatusTransitionCommand } from '../models/StatusTransitionCommand'; +import type { StatusTransitionResult } from '../models/StatusTransitionResult'; import type { ValuesTree } from '../models/ValuesTree'; import type { CancelablePromise } from '../core/CancelablePromise'; @@ -145,13 +146,13 @@ export class ProtocolAdaptersService { * Transition the runtime status of an adapter. * @param adapterId The id of the adapter whose runtime status will change. * @param requestBody The command to transition the adapter runtime status. - * @returns any Success + * @returns StatusTransitionResult Success * @throws ApiError */ public changeStatus1( adapterId: string, requestBody: StatusTransitionCommand, - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'PUT', url: '/api/v1/management/protocol-adapters/adapters/{adapterId}/status', diff --git a/hivemq-edge/src/frontend/src/api/hooks/useGetBridges/useSetConnectionStatus.tsx b/hivemq-edge/src/frontend/src/api/hooks/useGetBridges/useSetConnectionStatus.tsx index 87b59b32d8..b08971dca9 100644 --- a/hivemq-edge/src/frontend/src/api/hooks/useGetBridges/useSetConnectionStatus.tsx +++ b/hivemq-edge/src/frontend/src/api/hooks/useGetBridges/useSetConnectionStatus.tsx @@ -1,5 +1,5 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' -import { ApiError, StatusTransitionCommand } from '../../__generated__' +import { ApiError, StatusTransitionCommand, StatusTransitionResult } from '../../__generated__' import { useHttpClient } from '@/api/hooks/useHttpClient/useHttpClient.ts' import { QUERY_KEYS } from '@/api/utils.ts' @@ -17,7 +17,7 @@ export const useSetConnectionStatus = () => { return appClient.bridges.changeStatus(name, requestBody) } - return useMutation(setConnectionStatus, { + return useMutation(setConnectionStatus, { onSuccess: () => { // queryClient.invalidateQueries(['bridges', variables.name, QUERY_KEYS.CONNECTION_STATUS]) queryClient.invalidateQueries([QUERY_KEYS.BRIDGES]) diff --git a/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/__handlers__/index.ts b/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/__handlers__/index.ts index 494a2f181e..dc5146d2b5 100644 --- a/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/__handlers__/index.ts +++ b/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/__handlers__/index.ts @@ -130,6 +130,14 @@ export const handlers = [ return res(ctx.json({}), ctx.status(200)) }), + rest.put('*/protocol-adapters/adapters/:adapterId/status', (_, res, ctx) => { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { adapterType } = req.params + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return res(ctx.json({}), ctx.status(200)) + }), + rest.put('*/protocol-adapters/adapters/:adapterType', (_, res, ctx) => { // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/useSetConnectionStatus.tsx b/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/useSetConnectionStatus.tsx new file mode 100644 index 0000000000..f0ad9baada --- /dev/null +++ b/hivemq-edge/src/frontend/src/api/hooks/useProtocolAdapters/useSetConnectionStatus.tsx @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { ApiError, StatusTransitionCommand, StatusTransitionResult } from '../../__generated__' + +import { useHttpClient } from '@/api/hooks/useHttpClient/useHttpClient.ts' +import { QUERY_KEYS } from '@/api/utils.ts' + +interface SetConnectionStatusProps { + adapterId: string + requestBody: StatusTransitionCommand +} + +export const useSetConnectionStatus = () => { + const appClient = useHttpClient() + const queryClient = useQueryClient() + + const changeStatus = ({ adapterId, requestBody }: SetConnectionStatusProps) => { + return appClient.protocolAdapters.changeStatus1(adapterId, requestBody) + } + + return useMutation(changeStatus, { + onSuccess: () => { + queryClient.invalidateQueries([QUERY_KEYS.ADAPTERS]) + }, + }) +} diff --git a/hivemq-edge/src/frontend/src/api/types/api-devices.ts b/hivemq-edge/src/frontend/src/api/types/api-devices.ts new file mode 100644 index 0000000000..bab9875c95 --- /dev/null +++ b/hivemq-edge/src/frontend/src/api/types/api-devices.ts @@ -0,0 +1,4 @@ +export enum DeviceTypes { + BRIDGE = 'BRIDGE', + ADAPTER = 'ADAPTER', +} diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/ConnectionController.spec.tsx b/hivemq-edge/src/frontend/src/components/ConnectionController/ConnectionController.spec.tsx new file mode 100644 index 0000000000..071e8e755b --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/ConnectionController.spec.tsx @@ -0,0 +1,107 @@ +import { describe, expect, vi } from 'vitest' +import { render, screen, waitFor } from '@testing-library/react' +import { QueryClientProvider } from '@tanstack/react-query' + +import '@/config/i18n.config.ts' + +import { StatusTransitionResult } from '@/api/__generated__' +import { DeviceTypes } from '@/api/types/api-devices.ts' +import queryClient from '@/api/queryClient.ts' + +import { AuthProvider } from '@/modules/Auth/AuthProvider.tsx' + +import ConnectionController from './ConnectionController.tsx' + +const { mutateAdapterAsync, mutateBridgeAsync, successToast, errorToast } = vi.hoisted(() => { + return { + mutateAdapterAsync: vi.fn().mockResolvedValue({}), + mutateBridgeAsync: vi.fn().mockResolvedValue({}), + successToast: vi.fn(), + errorToast: vi.fn(), + } +}) + +vi.mock('@/api/hooks/useProtocolAdapters/useSetConnectionStatus.tsx', () => { + return { useSetConnectionStatus: () => ({ mutateAsync: mutateAdapterAsync }) } +}) + +vi.mock('@/api/hooks/useGetBridges/useSetConnectionStatus.tsx', () => { + return { useSetConnectionStatus: () => ({ mutateAsync: mutateBridgeAsync }) } +}) + +vi.mock('@/hooks/useEdgeToast/useEdgeToast.tsx', () => { + return { useEdgeToast: () => ({ successToast: successToast, errorToast: errorToast }) } +}) + +const wrapper: React.JSXElementConstructor<{ children: React.ReactElement }> = ({ children }) => ( + + {children} + +) + +const mockID = 'my-id' +const mockStatusTransitionResult: StatusTransitionResult = { + type: 'adapter', + identifier: mockID, + status: StatusTransitionResult.status.PENDING, + callbackTimeoutMillis: 2000, +} + +describe('ConnectionController', () => { + it('should trigger a correct mutation on a state change', async () => { + mutateAdapterAsync.mockResolvedValue(mockStatusTransitionResult) + render(, { wrapper }) + + expect(screen.getByTestId('device-action-start')).toBeDefined() + + await waitFor(() => { + screen.getByTestId('device-action-start').click() + + expect(mutateAdapterAsync).toHaveBeenCalledWith({ + adapterId: mockID, + requestBody: { + command: 'START', + }, + }) + + expect(successToast).toHaveBeenCalledWith({ + description: "We've successfully started the adapter. It might take up to 2 seconds to update the status.", + title: 'Connection updating', + }) + }) + }) + + it('should trigger an error when mutation not resolved', async () => { + const err = [ + { + fieldName: 'command', + title: 'Invalid user supplied data', + }, + ] + + mutateBridgeAsync.mockRejectedValue({ + errors: err, + }) + render(, { wrapper }) + + screen.getByTestId('device-action-start').click() + await waitFor(() => { + expect(mutateBridgeAsync).toHaveBeenCalledWith({ + name: mockID, + requestBody: { + command: 'START', + }, + }) + + expect(errorToast).toHaveBeenCalledWith( + { + title: 'Connection updating', + description: 'There was a problem trying to reconnect the bridge', + }, + { + errors: err, + } + ) + }) + }) +}) diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/ConnectionController.tsx b/hivemq-edge/src/frontend/src/components/ConnectionController/ConnectionController.tsx new file mode 100644 index 0000000000..0abda20efd --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/ConnectionController.tsx @@ -0,0 +1,80 @@ +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { ApiError, Status, StatusTransitionCommand } from '@/api/__generated__' +import { DeviceTypes } from '@/api/types/api-devices.ts' +import { useSetConnectionStatus as useSetAdapterConnectionStatus } from '@/api/hooks/useProtocolAdapters/useSetConnectionStatus.tsx' +import { useSetConnectionStatus as useSetBridgeConnectionStatus } from '@/api/hooks/useGetBridges/useSetConnectionStatus.tsx' + +import { useEdgeToast } from '@/hooks/useEdgeToast/useEdgeToast.tsx' + +import ConnectionButton from './components/ConnectionButton.tsx' +import ConnectionMenu from './components/ConnectionMenu.tsx' +import { ConnectionElementProps } from '@/components/ConnectionController/types.ts' + +interface ConnectionControllerProps { + type: DeviceTypes + id: string + status?: Status + variant?: 'button' | 'menuItem' +} + +const ConnectionController: FC = ({ type, id, status, variant = 'button' }) => { + const updateAdapterStatus = useSetAdapterConnectionStatus() + const updateBridgeStatus = useSetBridgeConnectionStatus() + const { successToast, errorToast } = useEdgeToast() + const { t } = useTranslation() + const [isLoading, setIsLoading] = useState(0) + + const As: React.FC = variant === 'button' ? ConnectionButton : ConnectionMenu + + const isRunning = status?.runtime === Status.runtime.STARTED + + useEffect(() => { + if (isLoading) { + const timer = setTimeout(() => { + setIsLoading(0) + }, isLoading) + + return () => { + clearTimeout(timer) + } + } + }, [isLoading]) + + const handleOnStatusChange = (eventId: string, status: StatusTransitionCommand.command) => { + const statusPromise = + type === DeviceTypes.BRIDGE + ? updateBridgeStatus.mutateAsync({ name: eventId, requestBody: { command: status } }) + : updateAdapterStatus.mutateAsync({ adapterId: eventId, requestBody: { command: status } }) + + statusPromise + .then((results) => { + const { callbackTimeoutMillis } = results + if (callbackTimeoutMillis) setIsLoading(callbackTimeoutMillis) + successToast({ + title: t('protocolAdapter.toast.status.title'), + description: t('protocolAdapter.toast.status.description', { + action: status, + device: type, + callbackTimeoutMillis: callbackTimeoutMillis ? callbackTimeoutMillis / 1000 : 0, + }), + }) + }) + .catch((err: ApiError) => + errorToast( + { + title: t('protocolAdapter.toast.status.title'), + description: t('protocolAdapter.toast.status.error', { + device: type, + }), + }, + err + ) + ) + } + + return +} + +export default ConnectionController diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionButton.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionButton.spec.cy.tsx new file mode 100644 index 0000000000..8f84caf35f --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionButton.spec.cy.tsx @@ -0,0 +1,60 @@ +/// + +import ConnectionButton from './ConnectionButton.tsx' +import { StatusTransitionCommand } from '@/api/__generated__' + +const MOCK_ID = 'my-id' + +describe('ConnectionButton', () => { + beforeEach(() => { + cy.viewport(400, 150) + }) + + it('should be accessible', () => { + cy.injectAxe() + cy.mountWithProviders() + cy.checkAccessibility() + cy.percySnapshot('Component: ConnectionButton') + }) + + it('should render stop CTAs when device is running', () => { + const onChangeStatus = cy.stub().as('onChangeStatus') + cy.mountWithProviders() + + cy.getByTestId('device-action-start').should('not.exist') + cy.getByTestId('device-action-stop').should('have.attr', 'aria-label', 'Stop') + cy.getByTestId('device-action-stop').click() + cy.get('@onChangeStatus').should('have.been.calledWith', MOCK_ID, StatusTransitionCommand.command.STOP) + + cy.getByTestId('device-action-restart').should('have.attr', 'aria-label', 'Restart') + cy.getByTestId('device-action-restart').click() + cy.get('@onChangeStatus').should('have.been.calledWith', MOCK_ID, StatusTransitionCommand.command.RESTART) + }) + + it('should render start CTAs when device is not running', () => { + const onChangeStatus = cy.stub().as('onChangeStatus') + cy.mountWithProviders() + + cy.getByTestId('device-action-stop').should('not.exist') + cy.getByTestId('device-action-start').should('have.attr', 'aria-label', 'Start') + cy.getByTestId('device-action-start').click() + cy.get('@onChangeStatus').should('have.been.calledWith', MOCK_ID, StatusTransitionCommand.command.START) + + cy.getByTestId('device-action-restart').should('have.attr', 'aria-label', 'Restart') + cy.getByTestId('device-action-restart').should('be.disabled') + }) + + it('should render disabled states when isLoading (and running)', () => { + cy.mountWithProviders() + + cy.getByTestId('device-action-stop').should('be.disabled') + cy.getByTestId('device-action-restart').should('be.disabled') + }) + + it('should render disabled states when isLoading (and not running)', () => { + cy.mountWithProviders() + + cy.getByTestId('device-action-start').should('be.disabled') + cy.getByTestId('device-action-restart').should('be.disabled') + }) +}) diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionButton.tsx b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionButton.tsx new file mode 100644 index 0000000000..bdec2d8912 --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionButton.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react' +import { ButtonGroup, IconButton } from '@chakra-ui/react' +import { MdPlayArrow, MdRestartAlt, MdStop } from 'react-icons/md' +import { StatusTransitionCommand } from '@/api/__generated__' +import { ConnectionElementProps } from '@/components/ConnectionController/types.ts' +import { useTranslation } from 'react-i18next' + +const ConnectionButton: FC = ({ id, isRunning, onChangeStatus, isLoading }) => { + const { t } = useTranslation() + + return ( + + {!isRunning && ( + } + onClick={() => onChangeStatus?.(id, StatusTransitionCommand.command.START)} + /> + )} + {isRunning && ( + } + onClick={() => onChangeStatus?.(id, StatusTransitionCommand.command.STOP)} + /> + )} + } + onClick={() => onChangeStatus?.(id, StatusTransitionCommand.command.RESTART)} + /> + + ) +} + +export default ConnectionButton diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionMenu.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionMenu.spec.cy.tsx new file mode 100644 index 0000000000..92c5e17856 --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionMenu.spec.cy.tsx @@ -0,0 +1,120 @@ +/// + +import { StatusTransitionCommand } from '@/api/__generated__' +import ConnectionMenu from '@/components/ConnectionController/components/ConnectionMenu.tsx' +import { Menu, MenuButton, MenuList } from '@chakra-ui/react' +import { FC, PropsWithChildren } from 'react' + +const MOCK_ID = 'my-id' + +const Wrapper: FC = ({ children }) => ( + + my custom menu + {children} + +) + +describe('ConnectionMenu', () => { + beforeEach(() => { + cy.viewport(400, 300) + }) + + it('should be accessible', () => { + cy.injectAxe() + cy.mountWithProviders( + + + + ) + cy.checkAccessibility() + cy.getByTestId('mock-trigger').click() + cy.percySnapshot('Component: ConnectionMenu') + }) + + it('should render stop CTAs when device is running', () => { + const onChangeStatus = cy.stub().as('onChangeStatus') + + cy.mountWithProviders( + + + + ) + + cy.getByTestId('mock-trigger').click() + + cy.getByTestId('device-action-start').should('not.exist') + cy.getByTestId('device-action-stop').should('have.text', 'Stop') + cy.getByTestId('device-action-stop').click() + cy.get('@onChangeStatus').should('have.been.calledWith', MOCK_ID, StatusTransitionCommand.command.STOP) + }) + + it('should render start CTAs when device is not running', () => { + const onChangeStatus = cy.stub().as('onChangeStatus') + + cy.mountWithProviders( + + + + ) + + cy.getByTestId('mock-trigger').click() + + cy.getByTestId('device-action-stop').should('not.exist') + cy.getByTestId('device-action-start').should('have.text', 'Start') + cy.getByTestId('device-action-start').click() + cy.get('@onChangeStatus').should('have.been.calledWith', MOCK_ID, StatusTransitionCommand.command.START) + }) + + it('should render restart CTA (not running)', () => { + const onChangeStatus = cy.stub().as('onChangeStatus') + + cy.mountWithProviders( + + + + ) + + cy.getByTestId('mock-trigger').click() + + cy.getByTestId('device-action-restart').should('have.text', 'Restart') + cy.getByTestId('device-action-restart').should('be.disabled') + }) + + it('should render restart CTA (running)', () => { + const onChangeStatus = cy.stub().as('onChangeStatus') + + cy.mountWithProviders( + + + + ) + + cy.getByTestId('mock-trigger').click() + + cy.getByTestId('device-action-restart').should('have.text', 'Restart') + cy.getByTestId('device-action-restart').click() + cy.get('@onChangeStatus').should('have.been.calledWith', MOCK_ID, StatusTransitionCommand.command.RESTART) + }) + + it('should render disable states when isLoading (running)', () => { + cy.mountWithProviders( + + + + ) + + cy.getByTestId('device-action-stop').should('be.disabled') + cy.getByTestId('device-action-restart').should('be.disabled') + }) + + it('should render disable states when isLoading (not running)', () => { + cy.mountWithProviders( + + + + ) + + cy.getByTestId('device-action-start').should('be.disabled') + cy.getByTestId('device-action-restart').should('be.disabled') + }) +}) diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionMenu.tsx b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionMenu.tsx new file mode 100644 index 0000000000..0b6b82c4b0 --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/components/ConnectionMenu.tsx @@ -0,0 +1,43 @@ +import { FC } from 'react' +import { ConnectionElementProps } from '@/components/ConnectionController/types.ts' +import { MenuItem } from '@chakra-ui/react' +import { StatusTransitionCommand } from '@/api/__generated__' +import { useTranslation } from 'react-i18next' + +const ConnectionMenu: FC = ({ id, isRunning, isLoading, onChangeStatus }) => { + const { t } = useTranslation() + + return ( + <> + {!isRunning && ( + onChangeStatus?.(id, StatusTransitionCommand.command.START)} + > + {t('action.start')} + + )} + + {isRunning && ( + onChangeStatus?.(id, StatusTransitionCommand.command.STOP)} + > + {t('action.stop')} + + )} + + onChangeStatus?.(id, StatusTransitionCommand.command.RESTART)} + > + {t('action.restart')} + + + ) +} + +export default ConnectionMenu diff --git a/hivemq-edge/src/frontend/src/components/ConnectionController/types.ts b/hivemq-edge/src/frontend/src/components/ConnectionController/types.ts new file mode 100644 index 0000000000..704edc313b --- /dev/null +++ b/hivemq-edge/src/frontend/src/components/ConnectionController/types.ts @@ -0,0 +1,8 @@ +import { StatusTransitionCommand } from '@/api/__generated__' + +export interface ConnectionElementProps { + id: string + isRunning: boolean + isLoading?: boolean + onChangeStatus?: (id: string, status: StatusTransitionCommand.command) => void +} diff --git a/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.spec.cy.tsx index b5812cb400..1f399af153 100644 --- a/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.spec.cy.tsx @@ -10,19 +10,22 @@ describe('ConnectionStatusBadge', () => { cy.viewport(800, 250) }) - const selectors = [ - { status: undefined }, - { status: Status.connection.CONNECTED }, - { status: Status.connection.DISCONNECTED }, - { status: Status.connection.STATELESS }, - { status: Status.connection.UNKNOWN }, - { status: Status.connection.ERROR }, + const testCases: Status[] = [ + { runtime: Status.runtime.STOPPED }, + { connection: Status.connection.CONNECTED }, + { connection: Status.connection.DISCONNECTED }, + { connection: Status.connection.STATELESS }, + { connection: Status.connection.ERROR }, + { connection: Status.connection.UNKNOWN }, + { connection: undefined }, + { runtime: undefined }, ] - it.each(selectors)( - (selector) => `should render and be accessible for ${selector.status}`, + + it.each(testCases)( + (status) => `should render and be accessible for ${status.connection || status.runtime}`, (selector) => { cy.injectAxe() - cy.mountWithProviders() + cy.mountWithProviders() cy.checkAccessibility() } ) diff --git a/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.tsx b/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.tsx index fe7564e418..be4c2847a0 100644 --- a/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.tsx +++ b/hivemq-edge/src/frontend/src/components/ConnectionStatusBadge/ConnectionStatusBadge.tsx @@ -1,28 +1,35 @@ import { FC } from 'react' -import { Status } from '@/api/__generated__' +import { useTranslation } from 'react-i18next' import { Badge } from '@chakra-ui/react' +import { Status } from '@/api/__generated__' + const statusMapping = { - [Status.connection.ERROR]: { text: 'error', color: 'status.error' }, - [Status.connection.UNKNOWN]: { text: 'Disconnecting', color: 'status.error' }, - [Status.connection.CONNECTED]: { text: 'Connected', color: 'status.connected' }, - [Status.connection.DISCONNECTED]: { text: 'Disconnected', color: 'status.disconnected' }, - [Status.connection.STATELESS]: { text: 'Stateless', color: 'status.stateless' }, + [Status.runtime.STOPPED]: { text: 'STOPPED', color: 'status.error' }, + [Status.connection.ERROR]: { text: 'ERROR', color: 'status.error' }, + [Status.connection.UNKNOWN]: { text: 'UNKNOWN', color: 'status.error' }, + [Status.connection.CONNECTED]: { text: 'CONNECTED', color: 'status.connected' }, + [Status.connection.DISCONNECTED]: { text: 'DISCONNECTED', color: 'status.disconnected' }, + [Status.connection.STATELESS]: { text: 'STATELESS', color: 'status.stateless' }, } interface ConnectionStatusBadgeProps { - status?: Status.connection + status?: Status } const ConnectionStatusBadge: FC = ({ status }) => { + const { t } = useTranslation() + + const mapping = + statusMapping[ + status?.runtime === Status.runtime.STOPPED + ? Status.runtime.STOPPED + : status?.connection || Status.connection.UNKNOWN + ] + return ( - - {statusMapping[status || 'ERROR'].text} + + {t('hivemq.connection.status', { context: mapping.text })} ) } diff --git a/hivemq-edge/src/frontend/src/hooks/useEdgeToast/useEdgeToast.tsx b/hivemq-edge/src/frontend/src/hooks/useEdgeToast/useEdgeToast.tsx index 94615589bd..059b040d91 100644 --- a/hivemq-edge/src/frontend/src/hooks/useEdgeToast/useEdgeToast.tsx +++ b/hivemq-edge/src/frontend/src/hooks/useEdgeToast/useEdgeToast.tsx @@ -23,10 +23,10 @@ export const useEdgeToast = () => { <> {options?.description} {!body && {err.message}} - {body.message && {body.message}} + {body && body.message && {body.message}} {body?.errors?.map((e: ProblemDetailsExtended) => ( - {e.fieldName as string} : {e.detail} + {e.fieldName as string} : {e.detail || e.title} ))} diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 5aae77aacc..307eea7d6f 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -9,10 +9,22 @@ "cancel": "Cancel", "delete": "Delete", "previous": "Previous", - "next": "Next" + "next": "Next", + "start": "Start", + "stop": "Stop", + "restart": "Restart" }, "hivemq": { - "topic": "Topic" + "topic": "Topic", + "connection": { + "status_STOPPED": "Off", + "status_STARTED": "On", + "status_CONNECTED": "Connected", + "status_DISCONNECTED": "Disconnected", + "status_STATELESS": "Stateless", + "status_UNKNOWN": "Unknown", + "status_ERROR": "Error" + } }, "navigation": { "mainPage": "Main content", @@ -126,8 +138,6 @@ "add": "Add bridge connection", "edit": "Edit", "delete": "Delete", - "start": "Start", - "stop": "Stop", "create": "Create the bridge", "update": "Update the bridge", "submitting": "Submitting" @@ -418,15 +428,27 @@ }, "actions": { "label": "Actions", - "connect": "Connect", - "workspace": "View on workspace", - "disconnect": "Disconnect", "create": "Create similar", "edit": "Edit", - "delete": "Delete" + "delete": "Delete", + "workspace": "View on workspace" } }, "toast": { + "status": { + "title": "Connection updating", + "action_STOP": "stopped", + "action_START": "started", + "action_RESTART": "restarted", + "device": "the device", + "device_BRIDGE": "the bridge", + "device_ADAPTER": "the adapter", + "callbackTimeoutMillis_zero": "a little bit of time", + "callbackTimeoutMillis_one": "up to {{count}} second", + "callbackTimeoutMillis_other": "up to {{count}} seconds", + "description": "We've successfully $t(protocolAdapter.toast.status.action, {'context': '{{action}}' }) $t(protocolAdapter.toast.status.device, {'context': '{{device}}' }). It might take $t(protocolAdapter.toast.status.callbackTimeoutMillis, {'count': {{callbackTimeoutMillis}} }) to update the status.", + "error": "There was a problem trying to reconnect $t(protocolAdapter.toast.status.device, {'context': '{{device}}' })" + }, "delete": { "title": "Deleting Protocol Adapter", "description": "We've successfully deleted the Protocol Adapter for you.", diff --git a/hivemq-edge/src/frontend/src/modules/Bridges/Bridges.tsx b/hivemq-edge/src/frontend/src/modules/Bridges/Bridges.tsx index 7cea4d5340..8ffc09946d 100644 --- a/hivemq-edge/src/frontend/src/modules/Bridges/Bridges.tsx +++ b/hivemq-edge/src/frontend/src/modules/Bridges/Bridges.tsx @@ -1,17 +1,18 @@ import { FC, useMemo } from 'react' import { Box, Flex, SimpleGrid } from '@chakra-ui/react' import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' import BridgeEmptyLogo from '@/assets/app/bridge-empty.svg' import { ProblemDetails } from '@/api/types/http-problem-details.ts' import { useListBridges } from '@/api/hooks/useGetBridges/useListBridges.tsx' import { mockBridge } from '@/api/hooks/useGetBridges/__handlers__' + import ErrorMessage from '@/components/ErrorMessage.tsx' +import WarningMessage from '@/components/WarningMessage.tsx' import BridgeCard from '@/modules/Bridges/components/overview/BridgeCard.tsx' -import WarningMessage from '@/components/WarningMessage.tsx' -import { useNavigate } from 'react-router-dom' const Bridges: FC = () => { const { data, isLoading, isError, error } = useListBridges() diff --git a/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeCard.tsx b/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeCard.tsx index 88b6bea5bd..d265ad03cd 100644 --- a/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeCard.tsx +++ b/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeCard.tsx @@ -1,40 +1,45 @@ import { FC, useMemo } from 'react' import { useTranslation } from 'react-i18next' - import { + Box, Card, CardBody, CardFooter, CardHeader, + Flex, Heading, + HStack, + IconButton, Image, + Skeleton, Stack, - IconButton, - Box, - HStack, Text, - Skeleton, } from '@chakra-ui/react' import { EditIcon } from '@chakra-ui/icons' -import { Bridge } from '@/api/__generated__' import BridgeLogo from '@/assets/app/bridges.svg' -import ConnectionSummary from './ConnectionSummary.tsx' +import { Bridge } from '@/api/__generated__' import { useGetBridgesStatus } from '@/api/hooks/useConnection/useGetBridgesStatus.tsx' +import { DeviceTypes } from '@/api/types/api-devices.ts' + import { ConnectionStatusBadge } from '@/components/ConnectionStatusBadge' +import ConnectionController from '@/components/ConnectionController/ConnectionController.tsx' + +import ConnectionSummary from './ConnectionSummary.tsx' + +interface BridgeCardProps extends Bridge { + isLoading?: boolean + onNavigate?: (route: string) => void +} -const BridgeCard: FC void }> = ({ - isLoading, - onNavigate, - ...props -}) => { +const BridgeCard: FC = ({ isLoading, onNavigate, ...props }) => { const { t } = useTranslation() // const { isFetching } = useGetBridgeConnectionStatus(props.id) const { isFetching, data: connections } = useGetBridgesStatus() - const data = useMemo( + const status = useMemo( () => connections?.items?.find((connection) => connection.id === props.id && connection.type === 'bridge'), [connections, props.id] ) @@ -69,7 +74,7 @@ const BridgeCard: FC - + {t('bridge.status.label')} - + + + + diff --git a/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeOverview.tsx b/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeOverview.tsx index f594de6183..5e4b67ed7c 100644 --- a/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeOverview.tsx +++ b/hivemq-edge/src/frontend/src/modules/Bridges/components/overview/BridgeOverview.tsx @@ -29,6 +29,11 @@ import SubscriptionStats from '@/modules/Bridges/components/overview/Subscriptio import ConnectionSummary from './ConnectionSummary.tsx' +/** + * @deprecated + * @param props + * @constructor + */ const BridgeOverview: FC = (props) => { const { t } = useTranslation() const { isLoading, isError, mutateAsync } = useSetConnectionStatus() diff --git a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/drawers/NodePropertyDrawer.tsx b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/drawers/NodePropertyDrawer.tsx index 67bdb9ec6e..04b6cf051b 100644 --- a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/drawers/NodePropertyDrawer.tsx +++ b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/drawers/NodePropertyDrawer.tsx @@ -1,7 +1,7 @@ import { FC, useEffect } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { useNodes } from 'reactflow' +import { Node, useNodes } from 'reactflow' import { Button, Drawer, @@ -18,21 +18,28 @@ import { } from '@chakra-ui/react' import { EditIcon } from '@chakra-ui/icons' -import { Adapter } from '@/api/__generated__' +import { Adapter, Bridge } from '@/api/__generated__' +import { DeviceTypes } from '@/api/types/api-devices.ts' + +import ConnectionController from '@/components/ConnectionController/ConnectionController.tsx' + import Metrics from '@/modules/Welcome/components/Metrics.tsx' import { ProtocolAdapterTabIndex } from '@/modules/ProtocolAdapters/ProtocolAdapterPage.tsx' +import { NodeTypes } from '@/modules/EdgeVisualisation/types.ts' import { getDefaultMetricsFor } from '../../utils/nodes-utils.ts' const NodePropertyDrawer: FC = () => { const { t } = useTranslation() - const nodes = useNodes() - const { nodeId } = useParams() - const selected = nodes.find((e) => e.id === nodeId) const navigate = useNavigate() - const { isOpen, onOpen, onClose } = useDisclosure() + const nodes = useNodes() + const { nodeId } = useParams() + const selected = nodes.find( + (e) => e.id === nodeId && (e.type === NodeTypes.BRIDGE_NODE || e.type === NodeTypes.ADAPTER_NODE) + ) as Node | undefined + useEffect(() => { if (!nodes.length) return if (!selected || !nodeId) { @@ -47,7 +54,8 @@ const NodePropertyDrawer: FC = () => { navigate('/edge-flow') } - if (!selected) return null + // TODO[NVL] Needs warning / error + if (!selected || !selected.type) return null return ( @@ -63,7 +71,7 @@ const NodePropertyDrawer: FC = () => { - + + diff --git a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.tsx b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.tsx index 74c7957f05..b2cf4755d9 100644 --- a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.tsx +++ b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeAdapter.tsx @@ -48,7 +48,7 @@ const NodeAdapter: FC> = ({ id, data: adapter, selected }) => {options.showStatus && ( - + )} {options.showTopics && } diff --git a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.tsx b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.tsx index 12b95277b6..320817e31f 100644 --- a/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.tsx +++ b/hivemq-edge/src/frontend/src/modules/EdgeVisualisation/components/nodes/NodeBridge.tsx @@ -31,7 +31,7 @@ const NodeBridge: FC> = ({ data: bridge }) => { {options.showStatus && ( - + )} {options.showTopics && } diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.spec.cy.tsx index c9a10495e5..4f05aeae06 100644 --- a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.spec.cy.tsx @@ -13,23 +13,23 @@ describe('AdapterActionMenu', () => { cy.mountWithProviders() cy.getByAriaLabel('Actions').click() - cy.getByTestId('adapter-action-connect').should('be.visible') + cy.getByTestId('device-action-start').should('be.visible') cy.getByTestId('adapter-action-create').should('be.visible') cy.getByTestId('adapter-action-edit').should('be.visible') cy.getByTestId('adapter-action-delete').should('be.visible') cy.get('body').click(0, 0) - cy.getByTestId('adapter-action-connect').should('not.be.visible') + cy.getByTestId('device-action-start').should('not.be.visible') }) it('should render connected status properly', () => { cy.mountWithProviders() cy.getByAriaLabel('Actions').click() - cy.getByTestId('adapter-action-connect').should('be.visible').should('have.text', 'Disconnect') + cy.getByTestId('device-action-start').should('be.visible').should('have.text', 'Start') }) - it('should render disconnected status properly', () => { + it('should render start status properly', () => { const adapter: Adapter = { ...mockAdapter, status: { @@ -40,7 +40,7 @@ describe('AdapterActionMenu', () => { cy.mountWithProviders() cy.getByAriaLabel('Actions').click() - cy.getByTestId('adapter-action-connect').should('be.visible').should('have.text', 'Connect') + cy.getByTestId('device-action-start').should('be.visible').should('have.text', 'Start') }) it('should trigger actions', () => { diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.tsx b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.tsx index 0c7cc049ff..ec3e42f73f 100644 --- a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.tsx +++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterActionMenu.tsx @@ -3,7 +3,10 @@ import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Text } f import { ChevronDownIcon } from '@chakra-ui/icons' import { useTranslation } from 'react-i18next' -import { Adapter, Status } from '@/api/__generated__' +import { Adapter } from '@/api/__generated__' +import { DeviceTypes } from '@/api/types/api-devices.ts' + +import ConnectionController from '@/components/ConnectionController/ConnectionController.tsx' interface AdapterActionMenuProps { adapter: Adapter @@ -16,7 +19,7 @@ interface AdapterActionMenuProps { const AdapterActionMenu: FC = ({ adapter, onCreate, onEdit, onDelete, onViewWorkspace }) => { const { t } = useTranslation() - const { type, id, status: { connection } = {} } = adapter + const { type, id, status } = adapter return ( = ({ adapter, onCreate, onEd aria-label={t('protocolAdapter.table.actions.label') as string} /> - - {connection !== Status.connection.CONNECTED - ? t('protocolAdapter.table.actions.connect') - : t('protocolAdapter.table.actions.disconnect')} - + + onViewWorkspace?.(id, type as string)}> {t('protocolAdapter.table.actions.workspace')} diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/panels/ProtocolAdapters.tsx b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/panels/ProtocolAdapters.tsx index 2df592c576..02b5b0d52c 100644 --- a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/panels/ProtocolAdapters.tsx +++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/panels/ProtocolAdapters.tsx @@ -34,7 +34,7 @@ const AdapterStatusContainer: FC<{ id: string }> = ({ id }) => { const connection = connections?.items?.find((e) => e.id === id) - return + return } const AdapterTypeContainer: FC = (adapter) => { diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/hooks/useGetUISchema.ts b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/hooks/useGetUISchema.ts index 28a7ad83da..d87801edf0 100644 --- a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/hooks/useGetUISchema.ts +++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/hooks/useGetUISchema.ts @@ -9,7 +9,7 @@ const useGetUiSchema = (isNewAdapter = true) => { { id: 'coreFields', title: t('protocolAdapter.uiSchema.groups.coreFields'), - properties: ['id', 'port', 'host', 'uri', 'url','timeout'], + properties: ['id', 'port', 'host', 'uri', 'url', 'timeout'], }, { id: 'subFields',