From bcd5dfbe25d13df35d4e2eb0a6bee69b719e01b1 Mon Sep 17 00:00:00 2001 From: Nicolas Van Labeke Date: Fri, 10 Jan 2025 15:57:17 +0000 Subject: [PATCH] Merge pull request #732 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(28974): refactor the group metadata editor * feat(28974): add list of nodes in the group * refactor(28974): refactor event log to allow fine-grained configuratiā€¦ * refactor(28974): export the adapter status component * refactor(28974): change the rendering options for unknown status * refactor(28974): change the option for the local event log * feat(28974): redesign the group side panel * feat(28974): update translations * fix(28974): fix bug with undefined filtered node * refactor(28974): fix pagination bar * refactor(28974): fix action toolbars * refactor(28974): fix translations * teat(28974): fix teats * chore(28974): a bit of cleaning * refactor(28974): fix translations * test(28974): add tests * fix(28974): fix rendering of node type * test(28974): add tests * test(28974): add tests * fix(28974): fix block * fix(28974): fix the magic code * fix(28974): fix a bug with the metrics container * test(28974): fix tests --- .../PaginatedTable/PaginatedTable.spec.cy.tsx | 2 +- .../PaginatedTable/PaginatedTable.tsx | 1 + .../frontend/src/locales/en/translation.json | 23 ++++ .../table/EventLogTable.spec.cy.tsx | 4 +- .../components/table/EventLogTable.tsx | 35 ++++-- .../AdapterStatusContainer.spec.cy.tsx | 20 +++ .../adapters/AdapterStatusContainer.tsx | 11 ++ .../components/panels/ProtocolAdapters.tsx | 11 +- .../src/modules/Theme/foundations/colors.ts | 1 + .../drawers/GroupPropertyDrawer.spec.cy.tsx | 44 ++++++- .../drawers/GroupPropertyDrawer.tsx | 115 ++++++++++++++---- .../components/drawers/NodePropertyDrawer.tsx | 6 +- .../parts/GroupContentEditor.spec.cy.tsx | 75 ++++++++++++ .../components/parts/GroupContentEditor.tsx | 104 ++++++++++++++++ .../parts/GroupMetadataEditor.spec.cy.tsx | 46 +++++++ .../components/parts/GroupMetadataEditor.tsx | 93 +++++--------- .../modules/Workspace/utils/adapter.utils.ts | 2 +- 17 files changed, 484 insertions(+), 109 deletions(-) create mode 100644 hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.spec.cy.tsx create mode 100644 hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.tsx create mode 100644 hivemq-edge/src/frontend/src/modules/Workspace/components/parts/GroupContentEditor.spec.cy.tsx create mode 100644 hivemq-edge/src/frontend/src/modules/Workspace/components/parts/GroupContentEditor.tsx create mode 100644 hivemq-edge/src/frontend/src/modules/Workspace/components/parts/GroupMetadataEditor.spec.cy.tsx diff --git a/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.spec.cy.tsx b/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.spec.cy.tsx index a6755d7f77..029480aa09 100644 --- a/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.spec.cy.tsx @@ -41,7 +41,7 @@ describe('PaginatedTable', () => { cy.get('th').should('have.length', 2) cy.get('th').eq(0).should('contain.text', 'item') cy.get('th').eq(1).should('contain.text', 'value') - cy.get('tr').should('have.length', 10 + 1) + cy.get('tr').should('have.length', 5 + 1) cy.get('[aria-label="Go to the first page"]').should('be.visible') }) diff --git a/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.tsx b/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.tsx index 792c93624a..59cfcb1f90 100644 --- a/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.tsx +++ b/hivemq-edge/src/frontend/src/components/PaginatedTable/PaginatedTable.tsx @@ -75,6 +75,7 @@ const PaginatedTable = ({ const table = useReactTable({ data: data, columns, + initialState: { pagination: { pageSize: 5 } }, state: { columnFilters, globalFilter, diff --git a/hivemq-edge/src/frontend/src/locales/en/translation.json b/hivemq-edge/src/frontend/src/locales/en/translation.json index 28fffd30d7..06730a54fc 100755 --- a/hivemq-edge/src/frontend/src/locales/en/translation.json +++ b/hivemq-edge/src/frontend/src/locales/en/translation.json @@ -817,6 +817,29 @@ "ungroup": "Ungroup" }, "editor": { + "tabs": { + "config": "Configuration", + "events": "Events", + "metrics": "Metrics" + }, + "content": { + "header": { + "type": "Type", + "status": "Status", + "id": "Id", + "actions": "Actions" + }, + "actions": { + "ungroup": "Remove from group", + "startAll": "Start all", + "stopAll": "Stop all", + "restartAll": "Restart all" + } + }, + "eventLog": { + "header": "The 5 most recent events for devices in the group", + "showMore": "Show more in the Event Log" + }, "title": "Configure the group", "input-title": "Title", "input-color": "Group colour", diff --git a/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.spec.cy.tsx index 32c45e8527..a3f218dde3 100644 --- a/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.spec.cy.tsx @@ -28,10 +28,10 @@ describe('EventLogTable', () => { identifier: { identifier: 'EVENT-0', type: TypeIdentifier.type.EVENT }, } as Partial) - cy.getByAriaLabel('View details of event').eq(7).click() + cy.getByAriaLabel('View details of event').eq(4).click() cy.get('@onOpen').should('have.been.calledWithMatch', { - identifier: { identifier: 'EVENT-7', type: TypeIdentifier.type.EVENT }, + identifier: { identifier: 'EVENT-4', type: TypeIdentifier.type.EVENT }, } as Partial) }) diff --git a/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.tsx b/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.tsx index 5876811388..b569c7a268 100644 --- a/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.tsx +++ b/hivemq-edge/src/frontend/src/modules/EventLog/components/table/EventLogTable.tsx @@ -22,24 +22,34 @@ import SeverityBadge from '../SeverityBadge.tsx' interface EventLogTableProps { onOpen?: (t: Event) => void - globalSourceFilter?: string + globalSourceFilter?: string[] + maxEvents?: number variant?: 'full' | 'summary' + isSingleSource?: boolean } -const EventLogTable: FC = ({ onOpen, globalSourceFilter, variant = 'full' }) => { +const EventLogTable: FC = ({ + onOpen, + globalSourceFilter, + variant = 'full', + maxEvents = 5, + isSingleSource = false, +}) => { const { t } = useTranslation() const { data, isLoading, isFetching, error, refetch } = useGetEvents() const safeData = useMemo(() => { if (!data || !data?.items) return [...mockEdgeEvent(5)] if (globalSourceFilter) { - return data.items.filter((e: Event) => e.source?.identifier === globalSourceFilter).slice(0, 5) + return data.items + .filter((e: Event) => globalSourceFilter.includes(e.source?.identifier || '')) + .slice(0, maxEvents) } return data.items - }, [data, globalSourceFilter]) + }, [data, globalSourceFilter, maxEvents]) - const columns = useMemo[]>(() => { + const allColumns = useMemo[]>(() => { return [ { accessorKey: 'identifier.identifier', @@ -109,6 +119,13 @@ const EventLogTable: FC = ({ onOpen, globalSourceFilter, var ] }, [isLoading, onOpen, t]) + const displayColumns = useMemo(() => { + const [, createdColumn, severityColumn, idColumn, messageColumn] = allColumns + if (variant === 'full') return allColumns + if (isSingleSource) return [createdColumn, severityColumn, messageColumn] + else return [createdColumn, idColumn, severityColumn, messageColumn] + }, [allColumns, isSingleSource, variant]) + if (error) { return ( @@ -120,9 +137,6 @@ const EventLogTable: FC = ({ onOpen, globalSourceFilter, var ) } - // TODO[NVL] Not the best approach; destructure within memo - const [, a, b, , c] = columns - return ( <> {variant === 'full' && ( @@ -142,8 +156,9 @@ const EventLogTable: FC = ({ onOpen, globalSourceFilter, var aria-label={t('eventLog.title')} data={safeData} - columns={variant === 'full' ? columns : [a, b, c]} - enablePagination={variant === 'full'} + columns={displayColumns} + enablePaginationGoTo={variant === 'full'} + enablePaginationSizes={variant === 'full'} enableColumnFilters={variant === 'full'} // getRowStyles={(row) => { // return { backgroundColor: theme.colors.blue[50] } diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.spec.cy.tsx new file mode 100644 index 0000000000..c538f5fc82 --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.spec.cy.tsx @@ -0,0 +1,20 @@ +import { AdapterStatusContainer } from '@/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.tsx' +import { mockAdapterConnectionStatus } from '@/api/hooks/useConnection/__handlers__' +import { MOCK_ADAPTER_ID } from '@/__test-utils__/mocks.ts' + +describe('AdapterStatusContainer', () => { + beforeEach(() => { + cy.viewport(800, 900) + cy.intercept('api/v1/management/protocol-adapters/status', { items: [mockAdapterConnectionStatus] }).as('getStatus') + }) + + it('should render properly an existing adapter', () => { + cy.mountWithProviders() + cy.getByTestId('connection-status').should('have.text', 'Connected') + }) + + it('should render unknown if not an adapter', () => { + cy.mountWithProviders() + cy.getByTestId('connection-status').should('have.text', 'Unknown') + }) +}) diff --git a/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.tsx b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.tsx new file mode 100644 index 0000000000..de9006b121 --- /dev/null +++ b/hivemq-edge/src/frontend/src/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.tsx @@ -0,0 +1,11 @@ +import { FC } from 'react' +import { useGetAdaptersStatus } from '@/api/hooks/useConnection/useGetAdaptersStatus.ts' +import { ConnectionStatusBadge } from '@/components/ConnectionStatusBadge' + +export const AdapterStatusContainer: FC<{ id: string }> = ({ id }) => { + const { data: connections } = useGetAdaptersStatus() + + const connection = connections?.items?.find((e) => e.id === id) + + return +} 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 8b727ea9d9..7c12d5fbf7 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 @@ -8,7 +8,6 @@ import { useLocation, useNavigate } from 'react-router-dom' import { Adapter, ApiError, ProtocolAdapter } from '@/api/__generated__' import { useListProtocolAdapters } from '@/api/hooks/useProtocolAdapters/useListProtocolAdapters.ts' import { useDeleteProtocolAdapter } from '@/api/hooks/useProtocolAdapters/useDeleteProtocolAdapter.ts' -import { useGetAdaptersStatus } from '@/api/hooks/useConnection/useGetAdaptersStatus.ts' import { ProblemDetails } from '@/api/types/http-problem-details.ts' import { useGetAdapterTypes } from '@/api/hooks/useProtocolAdapters/useGetAdapterTypes.ts' import { mockAdapter } from '@/api/hooks/useProtocolAdapters/__handlers__' @@ -17,7 +16,6 @@ import AdapterEmptyLogo from '@/assets/app/adaptor-empty.svg' import ErrorMessage from '@/components/ErrorMessage.tsx' import WarningMessage from '@/components/WarningMessage.tsx' -import { ConnectionStatusBadge } from '@/components/ConnectionStatusBadge' import ConfirmationDialog from '@/components/Modal/ConfirmationDialog.tsx' import PaginatedTable from '@/components/PaginatedTable/PaginatedTable.tsx' import { WorkspaceIcon } from '@/components/Icons/TopicIcon.tsx' @@ -36,17 +34,10 @@ import { useEdgeToast } from '@/hooks/useEdgeToast/useEdgeToast.tsx' import AdapterActionMenu from '../adapters/AdapterActionMenu.tsx' import { compareStatus } from '../../utils/pagination-utils.ts' import IconButton from '@/components/Chakra/IconButton.tsx' +import { AdapterStatusContainer } from '@/modules/ProtocolAdapters/components/adapters/AdapterStatusContainer.tsx' const DEFAULT_PER_PAGE = 10 -const AdapterStatusContainer: FC<{ id: string }> = ({ id }) => { - const { data: connections } = useGetAdaptersStatus() - - const connection = connections?.items?.find((e) => e.id === id) - - return -} - const AdapterTypeContainer: FC = (adapter) => { return ( diff --git a/hivemq-edge/src/frontend/src/modules/Theme/foundations/colors.ts b/hivemq-edge/src/frontend/src/modules/Theme/foundations/colors.ts index af9ec1cedd..460500abe9 100644 --- a/hivemq-edge/src/frontend/src/modules/Theme/foundations/colors.ts +++ b/hivemq-edge/src/frontend/src/modules/Theme/foundations/colors.ts @@ -2,6 +2,7 @@ import { theme as baseTheme } from '@chakra-ui/react' const colors = { status: { + unknown: baseTheme.colors.gray, error: baseTheme.colors.red, connected: baseTheme.colors.green, disconnected: baseTheme.colors.orange, diff --git a/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.spec.cy.tsx b/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.spec.cy.tsx index 82d1996746..b5e29fc2ab 100644 --- a/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.spec.cy.tsx +++ b/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.spec.cy.tsx @@ -7,6 +7,7 @@ import { MOCK_METRICS } from '@/api/hooks/useGetMetrics/__handlers__' import { MetricList } from '@/api/__generated__' import { MOCK_NODE_ADAPTER } from '@/__test-utils__/react-flow/nodes.ts' import { MOCK_ADAPTER_ID, MOCK_ADAPTER_ID2 } from '@/__test-utils__/mocks.ts' +import { mockEdgeEvent } from '@/api/hooks/useEvents/__handlers__' const mockNode: Node = { position: { x: 0, y: 0 }, @@ -37,7 +38,7 @@ describe('GroupPropertyDrawer', () => { cy.intercept('/api/v1/metrics/**', []).as('getMetricForX') }) - it('should render properly', () => { + it('should render the minimal metrics properly', () => { const onClose = cy.stub().as('onClose') const onEditEntity = cy.stub() cy.mountWithProviders( @@ -67,6 +68,47 @@ describe('GroupPropertyDrawer', () => { cy.getByTestId('metrics-toggle').should('be.visible') }) + it.only('should render the full config tabs properly', () => { + cy.intercept('/api/v1/management/events?*', { items: [...mockEdgeEvent(150)] }) + const onClose = cy.stub().as('onClose') + const onEditEntity = cy.stub() + cy.mountWithProviders( + + ) + + cy.wait('@getMetricForX') + + // check the panel header + cy.getByTestId('group-panel-title').should('contain.text', 'Group Overview') + + // check the panel control + cy.get('@onClose').should('not.have.been.called') + cy.getByAriaLabel('Close').click() + cy.get('@onClose').should('have.been.calledOnce') + + // check the panel tabs + cy.get('[role="tablist"] [role="tab"]').should('have.length', 3) + cy.get('[role="tablist"] [role="tab"]').eq(0).should('have.text', 'Configuration') + cy.get('[role="tablist"] [role="tab"]').eq(1).should('have.text', 'Events') + cy.get('[role="tablist"] [role="tab"]').eq(2).should('have.text', 'Metrics') + + cy.get('[role="tablist"] + div > [role="tabpanel"]').should('have.length', 3) + cy.get('[role="tablist"] + div > [role="tabpanel"]').eq(0).should('not.have.attr', 'hidden') + cy.get('[role="tablist"] + div > [role="tabpanel"]').eq(1).should('have.attr', 'hidden') + cy.get('[role="tablist"] + div > [role="tabpanel"]').eq(2).should('have.attr', 'hidden') + + cy.getByTestId('group-metadata-header').should('be.visible') + cy.getByTestId('group-content-header').should('be.visible') + }) + it('should be accessible', () => { cy.injectAxe() cy.mountWithProviders( diff --git a/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.tsx b/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.tsx index e900539b75..febc029c3e 100644 --- a/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.tsx +++ b/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/GroupPropertyDrawer.tsx @@ -1,15 +1,28 @@ -import { FC } from 'react' +import { FC, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Node } from 'reactflow' +import { Link as RouterLink } from 'react-router-dom' import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, Drawer, DrawerBody, DrawerCloseButton, DrawerContent, DrawerHeader, DrawerOverlay, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, Text, + VStack, } from '@chakra-ui/react' +import { MdOutlineEventNote } from 'react-icons/md' import MetricsContainer from '@/modules/Metrics/MetricsContainer.tsx' @@ -19,6 +32,8 @@ import { getDefaultMetricsFor } from '../../utils/nodes-utils.ts' import GroupMetadataEditor from '../parts/GroupMetadataEditor.tsx' import { ChartType, MetricsFilter } from '@/modules/Metrics/types.ts' import NodeNameCard from '@/modules/Workspace/components/parts/NodeNameCard.tsx' +import GroupContentEditor from '@/modules/Workspace/components/parts/GroupContentEditor.tsx' +import EventLogTable from '@/modules/EventLog/components/table/EventLogTable.tsx' interface GroupPropertyDrawerProps { nodeId: string @@ -44,10 +59,86 @@ const GroupPropertyDrawer: FC = ({ const adapterIDs = selectedNode.data.childrenNodeIds.map((e) => nodes.find((x) => x.id === e)) const metrics = adapterIDs.map((x) => (x ? getDefaultMetricsFor(x) : [])).flat() + const linkEventLog = useMemo(() => { + const searchParams = new URLSearchParams() + for (const node of adapterIDs) { + if (node) searchParams.append('source', node.data.id) + } + return `/event-logs?${searchParams.toString()}` + }, [adapterIDs]) + const panelTitle = showConfig ? t('workspace.property.header', { context: selectedNode.type }) : t('workspace.observability.header', { context: selectedNode.type }) + const renderMetricsContainer = () => ( + ((acc, cur) => { + if (cur && cur.type === NodeTypes.ADAPTER_NODE) { + acc.push({ id: cur.data.id, type: `com.hivemq.edge.protocol-adapters.${cur.data.type}` }) + } + return acc + }, [])} + initMetrics={metrics} + defaultChartType={showConfig ? ChartType.SAMPLE : undefined} + /> + ) + + const renderGroupTabs = () => ( + + + {t('workspace.grouping.editor.tabs.config')} + {t('workspace.grouping.editor.tabs.events')} + {t('workspace.grouping.editor.tabs.metrics')} + + + + + { + onGroupSetData(nodeId, group) + }} + /> + + + + + + {t('workspace.grouping.editor.eventLog.header')} + + + e?.data.id)} + variant="summary" + maxEvents={10} + isSingleSource={false} + /> + + + + + + + + {renderMetricsContainer()} + + + + ) + return ( @@ -63,26 +154,8 @@ const GroupPropertyDrawer: FC = ({ /> - {showConfig && ( - { - onGroupSetData(nodeId, group) - }} - /> - )} - ((acc, cur) => { - if (cur && cur.type === NodeTypes.ADAPTER_NODE) { - acc.push({ id: cur.data.id, type: `com.hivemq.edge.protocol-adapters.${cur.data.type}` }) - } - return acc - }, [])} - initMetrics={metrics} - defaultChartType={showConfig ? ChartType.SAMPLE : undefined} - /> + {showConfig && renderGroupTabs()} + {!showConfig && renderMetricsContainer()} diff --git a/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/NodePropertyDrawer.tsx b/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/NodePropertyDrawer.tsx index f65c453613..3571ba04e2 100644 --- a/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/NodePropertyDrawer.tsx +++ b/hivemq-edge/src/frontend/src/modules/Workspace/components/drawers/NodePropertyDrawer.tsx @@ -86,7 +86,11 @@ const NodePropertyDrawer: FC = ({ nodeId, isOpen, selec - + - - - - - + + {t('workspace.grouping.editor.input-color')} + { + const { value, onChange, ...rest } = field + return onChange(value)} {...rest} /> + }} + control={control} + /> + + + + + + ) } diff --git a/hivemq-edge/src/frontend/src/modules/Workspace/utils/adapter.utils.ts b/hivemq-edge/src/frontend/src/modules/Workspace/utils/adapter.utils.ts index bbaf7d4a6a..188974b046 100644 --- a/hivemq-edge/src/frontend/src/modules/Workspace/utils/adapter.utils.ts +++ b/hivemq-edge/src/frontend/src/modules/Workspace/utils/adapter.utils.ts @@ -47,7 +47,7 @@ export const deviceCapabilityIcon: Record = { export const statusMapping = { [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.UNKNOWN]: { text: 'UNKNOWN', color: 'status.unknown' }, [Status.connection.CONNECTED]: { text: 'CONNECTED', color: 'status.connected' }, [Status.connection.DISCONNECTED]: { text: 'DISCONNECTED', color: 'status.disconnected' }, [Status.connection.STATELESS]: { text: 'STATELESS', color: 'status.stateless' },