diff --git a/.env b/.env deleted file mode 100644 index dbd8145..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -VITE_UCENTRALSEC_URL="https://ucentral.dpaas.arilia.com:16001" diff --git a/helm/values.yaml b/helm/values.yaml index a228840..5cf2f12 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -8,7 +8,7 @@ fullnameOverride: "" images: owgwui: repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui - tag: main + tag: v3.2.0 pullPolicy: Always services: diff --git a/package-lock.json b/package-lock.json index f54d16d..85d05e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "3.1.0(5)", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "3.1.0(5)", + "version": "3.2.0", "license": "ISC", "dependencies": { "@chakra-ui/anatomy": "^2.1.1", @@ -3540,7 +3540,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3548,7 +3548,7 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.1.2", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3556,7 +3556,7 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -3565,7 +3565,7 @@ }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.1", @@ -3578,12 +3578,12 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.17", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "3.1.0", @@ -4374,7 +4374,7 @@ "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "devOptional": true + "dev": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4419,7 +4419,7 @@ "version": "18.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", - "devOptional": true, + "dev": true, "dependencies": { "@types/react": "*" } @@ -4753,7 +4753,7 @@ }, "node_modules/acorn": { "version": "8.8.0", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5174,7 +5174,7 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/builtin-modules": { @@ -9781,7 +9781,7 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -9790,7 +9790,7 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10080,7 +10080,7 @@ }, "node_modules/terser": { "version": "5.15.1", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.2", @@ -10097,7 +10097,7 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/text-table": { diff --git a/src/components/Modals/CableDiagnosticsModal/index.tsx b/src/components/Modals/CableDiagnosticsModal/index.tsx new file mode 100644 index 0000000..622909e --- /dev/null +++ b/src/components/Modals/CableDiagnosticsModal/index.tsx @@ -0,0 +1,257 @@ +import React from 'react'; +import { + Modal, + Text, + ModalOverlay, + ModalContent, + ModalBody, + Center, + Spinner, + Checkbox, + Stack, + Table, + Thead, + Tbody, + Tr, + Th, + Td, +} from '@chakra-ui/react'; +import { PlugsConnected } from '@phosphor-icons/react'; +import { useTranslation } from 'react-i18next'; +import { CloseButton } from 'components/Buttons/CloseButton'; +import { ResponsiveButton } from 'components/Buttons/ResponsiveButton'; +import { ModalHeader } from 'components/Containers/Modal/ModalHeader'; +import { useCableDiagnostics } from 'hooks/Network/Devices'; +import { ModalProps } from 'models/Modal'; +import Button from 'theme/components/button'; +import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; +import { DataGrid } from 'components/DataTables/DataGrid'; + +export type CableDiagnosticsModalProps = { + modalProps: ModalProps; + serialNumber: string; + port: string; +}; + +type DiagnosticsRow = { + port: string; + linkStatus: string; + pairA: string; + pairB: string; + pairC: string; + pairD: string; + type: string; +}; + +type OpticalRow = { + port: string; + vendorName: string; + formFactor: string; + partNumber: string; + serialNumber: string; + temperature: string; + txPower: string; + rxPower: string; + revision: string; +}; + +export const CableDiagnosticsModal = ({ + modalProps: { isOpen, onClose }, + serialNumber, + port, +}: CableDiagnosticsModalProps) => { + const { t } = useTranslation(); + const [selectedPorts, setSelectedPorts] = React.useState([]); + const [diagnosticsResult, setDiagnosticsResult] = React.useState(null); + const { mutateAsync: diagnose, isLoading } = useCableDiagnostics({ serialNumber }); + + const handlePortToggle = (port: string) => { + setSelectedPorts((prev) => (prev.includes(port) ? prev.filter((p) => p !== port) : [...prev, port])); + }; + + const handleDiagnose = async () => { + if (port) { + try { + const result = await diagnose([port]); + setDiagnosticsResult(result); + } catch (error) { + console.error('Error diagnosing cable:', error); + } + } + }; + + const tableController = useDataGrid({ + tableSettingsId: 'cable.diagnostics.table', + defaultOrder: ['port', 'linkStatus', 'pairA', 'pairB', 'pairC', 'pairD', 'type'], + showAllRows: true, + }); + + const columns: DataGridColumn[] = React.useMemo(() => { + const data = diagnosticsResult?.results?.status?.text?.[port]; + const isOpticalData = data && 'form-factor' in data; + + return isOpticalData + ? [ + { + id: 'vendorName', + header: 'Vendor Name', + accessorKey: 'vendorName', + }, + { + id: 'formFactor', + header: 'Form Factor', + accessorKey: 'formFactor', + }, + { + id: 'partNumber', + header: 'Part Number', + accessorKey: 'partNumber', + }, + { + id: 'serialNumber', + header: 'Serial Number', + accessorKey: 'serialNumber', + }, + { + id: 'temperature', + header: 'Temperature', + accessorKey: 'temperature', + }, + { + id: 'txPower', + header: 'TX Power', + accessorKey: 'txPower', + }, + { + id: 'rxPower', + header: 'RX Power', + accessorKey: 'rxPower', + }, + { + id: 'revision', + header: 'Revision', + accessorKey: 'revision', + }, + ] + : [ + { + id: 'port', + header: 'Port', + accessorKey: 'port', + }, + { + id: 'linkStatus', + header: 'Link Status', + accessorKey: 'linkStatus', + }, + { + id: 'pairA', + header: 'Pair A', + accessorKey: 'pairA', + }, + { + id: 'pairB', + header: 'Pair B', + accessorKey: 'pairB', + }, + { + id: 'pairC', + header: 'Pair C', + accessorKey: 'pairC', + }, + { + id: 'pairD', + header: 'Pair D', + accessorKey: 'pairD', + }, + { + id: 'type', + header: 'Type', + accessorKey: 'type', + }, + ]; + }, [diagnosticsResult]); + + const formatDiagnosticsData = (result: any): (DiagnosticsRow | OpticalRow)[] => { + if (!result?.results?.status?.text?.[port]) return []; + + const data = result.results.status.text[port]; + + if (data['form-factor']) { + return [ + { + port, + vendorName: data['vendor-name'] || 'N/A', + formFactor: data['form-factor'] || 'N/A', + partNumber: data['part-number'] || 'N/A', + serialNumber: data['serial-number'] || 'N/A', + temperature: data.temperature ? `${data.temperature.toFixed(2)}` : 'N/A', + txPower: data['tx-optical-power'] ? `${data['tx-optical-power']}` : 'N/A', + rxPower: data['rx-optical-power'] ? `${data['rx-optical-power']}` : 'N/A', + revision: data.revision || 'N/A', + }, + ]; + } + + return [ + { + port, + linkStatus: data['link-status'], + pairA: `${data['pair-A'].meters} (${data['pair-A'].status})`, + pairB: `${data['pair-B'].meters} (${data['pair-B'].status})`, + pairC: `${data['pair-C'].meters} (${data['pair-C'].status})`, + pairD: `${data['pair-D'].meters} (${data['pair-D'].status})`, + type: data.type, + }, + ]; + }; + + return ( + + + + } /> + + {isLoading ? ( +
+ + Please wait... + + Please do not close this window. This may take a few seconds. + +
+ ) : ( +
+ } + label={`${ + diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 ? 'Retake' : 'Start' + } Test for Port ${port}`} + onClick={handleDiagnose} + isLoading={isLoading} + isDisabled={!port} + isCompact={false} + /> + {diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 && ( + + controller={tableController} + header={{ + title: '', + objectListed: 'Cable Diagnostics', + }} + columns={columns} + isLoading={isLoading} + data={formatDiagnosticsData(diagnosticsResult)} + options={{ + isHidingControls: true, + }} + /> + )} +
+ )} +
+
+
+ ); +}; diff --git a/src/hooks/Network/Devices.ts b/src/hooks/Network/Devices.ts index 3c22158..3f14be7 100644 --- a/src/hooks/Network/Devices.ts +++ b/src/hooks/Network/Devices.ts @@ -377,6 +377,40 @@ export const useWifiScanDevice = ({ serialNumber }: { serialNumber: string }) => ); }; +export const useCableDiagnostics = ({ serialNumber }: { serialNumber: string }) => { + const toast = useToast(); + const { t } = useTranslation(); + + return useMutation( + (ports: string[]): Promise => + axiosGw + .post(`device/${serialNumber}/cable-diagnostics`, { + serial: serialNumber, + ports, + when: 0, + }) + .then(({ data }) => data), + { + onSuccess: (data) => { + console.log('Success data: ', data); + }, + onError: (e: AxiosError) => { + toast({ + id: uuid(), + title: t('common.error'), + description: t('commands.cablediagnostics_error', { + e: e?.response?.data?.ErrorDescription, + }), + status: 'error', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + }, + }, + ); +}; + export const useGetDeviceRtty = ({ serialNumber, extraId }: { serialNumber: string; extraId: string | number }) => { const { t } = useTranslation(); const toast = useToast(); diff --git a/src/pages/Device/SwitchPortExamination/Actions.tsx b/src/pages/Device/SwitchPortExamination/Actions.tsx index 586561f..c4ec5cb 100644 --- a/src/pages/Device/SwitchPortExamination/Actions.tsx +++ b/src/pages/Device/SwitchPortExamination/Actions.tsx @@ -1,17 +1,19 @@ import * as React from 'react'; import { IconButton, Tooltip, useToast } from '@chakra-ui/react'; -import { Power } from '@phosphor-icons/react'; +import { Power, PlugsConnected } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { usePowerCycle } from 'hooks/Network/Devices'; import { useNotification } from 'hooks/useNotification'; import { DeviceLinkState } from 'hooks/Network/Statistics'; +import { CableDiagnosticsModalProps } from 'components/Modals/CableDiagnosticsModal'; type Props = { state: DeviceLinkState & { name: string }; deviceSerialNumber: string; + onOpenCableDiagnostics: (port: string) => void; }; -const LinkStateTableActions = ({ state, deviceSerialNumber }: Props) => { +const LinkStateTableActions = ({ state, deviceSerialNumber, onOpenCableDiagnostics }: Props) => { const { t } = useTranslation(); const powerCycle = usePowerCycle(); const toast = useToast(); @@ -54,16 +56,27 @@ const LinkStateTableActions = ({ state, deviceSerialNumber }: Props) => { }; return ( - - } - colorScheme="green" - onClick={onPowerCycle} - isLoading={powerCycle.isLoading} - size="xs" - /> - + <> + + } + colorScheme="green" + onClick={onPowerCycle} + isLoading={powerCycle.isLoading} + size="xs" + /> + + + } + colorScheme="blue" + onClick={() => onOpenCableDiagnostics(state.name)} + size="xs" + /> + + ); }; diff --git a/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx b/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx index e332307..0fe1ff6 100644 --- a/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx +++ b/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx @@ -9,8 +9,12 @@ import LinkStateTableActions from './Actions'; type Row = DeviceLinkState & { name: string }; const dataCell = (v: number) => ; -const actionCell = (row: Row, serialNumber: string) => ( - +const actionCell = (row: Row, serialNumber: string, onOpenCableDiagnostics: (port: string) => void) => ( + ); type Props = { @@ -19,9 +23,10 @@ type Props = { isFetching: boolean; type: 'upstream' | 'downstream'; serialNumber: string; + onOpenCableDiagnostics: (port: string) => void; }; -const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }: Props) => { +const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber, onOpenCableDiagnostics }: Props) => { const tableController = useDataGrid({ tableSettingsId: 'switch.link-state.table', defaultOrder: [ @@ -157,10 +162,16 @@ const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }: id: 'actions', header: '', accessorKey: '', - cell: ({ cell }) => actionCell(cell.row.original, serialNumber), + cell: ({ cell }) => ( + + ), }, ], - [], + [onOpenCableDiagnostics], ); if (!statistics || statistics?.length === 0) { diff --git a/src/pages/Device/SwitchPortExamination/index.tsx b/src/pages/Device/SwitchPortExamination/index.tsx index 88c3728..16c5f54 100644 --- a/src/pages/Device/SwitchPortExamination/index.tsx +++ b/src/pages/Device/SwitchPortExamination/index.tsx @@ -5,6 +5,8 @@ import SwitchInterfaceTable from './SwitchInterfaceTable'; import { DeviceLinkState, useGetDeviceLastStats } from 'hooks/Network/Statistics'; import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; +import { CableDiagnosticsModal } from 'components/Modals/CableDiagnosticsModal'; +import { useDisclosure } from '@chakra-ui/react'; type Props = { serialNumber: string; @@ -12,6 +14,8 @@ type Props = { const SwitchPortExamination = ({ serialNumber }: Props) => { const [tabIndex, setTabIndex] = React.useState(0); + const [selectedPort, setSelectedPort] = React.useState(''); + const cableDiagnosticsModalProps = useDisclosure(); const handleTabsChange = React.useCallback((index: number) => { setTabIndex(index); @@ -35,63 +39,73 @@ const SwitchPortExamination = ({ serialNumber }: Props) => { })); }, [getStats.data]); + const handleOpenCableDiagnostics = React.useCallback((port: string) => { + setSelectedPort(port); + cableDiagnosticsModalProps.onOpen(); + }, []); + return ( - - - - - - Interfaces - - - Link-State (Up) - - - Link-State (Down) - - - - - {getStats.data ? ( - - ) : ( - - )} - - - {getStats.data ? ( - - ) : ( - - )} - - - {getStats.data ? ( - - ) : ( - - )} - - - - - + <> + + + + + + Interfaces + + + Link-State (Up) + + + Link-State (Down) + + + + + {getStats.data ? ( + + ) : ( + + )} + + + {getStats.data ? ( + + ) : ( + + )} + + + {getStats.data ? ( + + ) : ( + + )} + + + + + + + ); }; diff --git a/src/pages/Device/Wrapper.tsx b/src/pages/Device/Wrapper.tsx index 66ea61a..6be4097 100644 --- a/src/pages/Device/Wrapper.tsx +++ b/src/pages/Device/Wrapper.tsx @@ -68,6 +68,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 }); const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure(); const scanModalProps = useDisclosure(); + const cableDiagnosticsModalProps = useDisclosure(); const resetModalProps = useDisclosure(); const eventQueueProps = useDisclosure(); const configureModalProps = useDisclosure();