From adaebb17e7a9f5feaf9886cabdae64b5560f4a50 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 11 Jan 2024 12:56:20 -0500 Subject: [PATCH 1/2] [WIFI-13282] Add support for OLS Signed-off-by: Charles --- package-lock.json | 340 +++++++++++++++++- package.json | 3 +- public/devices/edgecore_ecs4125.png | Bin 0 -> 33631 bytes .../Buttons/DeviceActionDropdown/index.tsx | 9 +- .../Containers/ResponsiveTag/index.tsx | 2 +- src/custom.d.ts | 1 + src/hooks/Network/Devices.ts | 53 ++- src/hooks/Network/Statistics.ts | 32 +- .../SwitchPortExamination/LinkStateTable.tsx | 182 ++++++++++ .../SwitchInterfaceTable.tsx | 170 +++++++++ .../Device/SwitchPortExamination/index.tsx | 96 +++++ src/pages/Device/Wrapper.tsx | 15 +- src/pages/Device/ethernetIconConnected.svg | 2 + src/pages/Device/ethernetIconDisconnected.svg | 2 + src/pages/Devices/ListCard/icons/SWITCH.png | Bin 2709 -> 3216 bytes src/pages/Devices/ListCard/index.tsx | 42 ++- vite.config.ts | 11 +- 17 files changed, 907 insertions(+), 53 deletions(-) create mode 100644 public/devices/edgecore_ecs4125.png create mode 100644 src/pages/Device/SwitchPortExamination/LinkStateTable.tsx create mode 100644 src/pages/Device/SwitchPortExamination/SwitchInterfaceTable.tsx create mode 100644 src/pages/Device/SwitchPortExamination/index.tsx create mode 100644 src/pages/Device/ethernetIconConnected.svg create mode 100644 src/pages/Device/ethernetIconDisconnected.svg diff --git a/package-lock.json b/package-lock.json index 5a5665d9..bd545151 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "3.0.0(6)", + "version": "3.0.1(2)", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "3.0.0(6)", + "version": "3.0.1(2)", "license": "ISC", "dependencies": { "@chakra-ui/anatomy": "^2.1.1", @@ -88,6 +88,7 @@ "lint-staged": "^13.2.1", "prettier": "^2.8.7", "vite-plugin-pwa": "^0.14.7", + "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.2.0" } }, @@ -3955,9 +3956,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -3968,7 +3969,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -3997,6 +3998,245 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@tanstack/query-core": { "version": "4.29.1", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.1.tgz", @@ -4969,6 +5209,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001480", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001480.tgz", @@ -5399,6 +5651,16 @@ "csstype": "^3.0.2" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/duplexer": { "version": "0.1.2", "license": "MIT" @@ -5431,6 +5693,18 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6408,14 +6682,15 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -7945,6 +8220,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "dev": true, @@ -8084,6 +8368,16 @@ "dev": true, "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "license": "MIT", @@ -9368,6 +9662,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -9695,6 +9999,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, "node_modules/temp": { "version": "0.9.4", "license": "MIT", @@ -10208,6 +10518,20 @@ "workbox-window": "^6.5.4" } }, + "node_modules/vite-plugin-svgr": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", + "integrity": "sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.5", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4 || 5" + } + }, "node_modules/vite-tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.0.tgz", diff --git a/package.json b/package.json index 1af535c6..e587daf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "3.0.0(6)", + "version": "3.0.1(2)", "description": "", "private": true, "main": "index.tsx", @@ -94,6 +94,7 @@ "lint-staged": "^13.2.1", "prettier": "^2.8.7", "vite-plugin-pwa": "^0.14.7", + "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.2.0" }, "browserslist": { diff --git a/public/devices/edgecore_ecs4125.png b/public/devices/edgecore_ecs4125.png new file mode 100644 index 0000000000000000000000000000000000000000..1a241b33de1ef6121b28220b0150e1d77bfc50ed GIT binary patch literal 33631 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 { const { t } = useTranslation(); const toast = useToast(); + const deviceType = device?.deviceType ?? 'AP'; const connectColor = useColorModeValue('blackAlpha', 'gray'); const addEventListeners = useControllerStore((state) => state.addEventListeners); const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({ @@ -172,7 +173,7 @@ const DeviceActionDropdown = ({ isLoading={isRtty} onClick={handleConnectClick} colorScheme={connectColor} - hidden={isCompact} + hidden={isCompact || deviceType !== 'AP'} /> @@ -205,7 +206,7 @@ const DeviceActionDropdown = ({ isDisabled={isDisabled} onClick={handleOpenScan} colorScheme="teal" - hidden={isCompact} + hidden={isCompact || deviceType !== 'AP'} /> @@ -221,7 +222,7 @@ const DeviceActionDropdown = ({ {t('commands.blink')} - diff --git a/src/components/Containers/ResponsiveTag/index.tsx b/src/components/Containers/ResponsiveTag/index.tsx index 7508cbbc..890afbce 100644 --- a/src/components/Containers/ResponsiveTag/index.tsx +++ b/src/components/Containers/ResponsiveTag/index.tsx @@ -26,7 +26,7 @@ export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...p return ( - + {label} diff --git a/src/custom.d.ts b/src/custom.d.ts index 9c766252..6aa6b4b5 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -8,3 +8,4 @@ declare module '*.png' { const value: string; export = value; } +/// diff --git a/src/hooks/Network/Devices.ts b/src/hooks/Network/Devices.ts index 4c5d65b1..b91c3f40 100644 --- a/src/hooks/Network/Devices.ts +++ b/src/hooks/Network/Devices.ts @@ -10,14 +10,19 @@ import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } import { Note } from 'models/Note'; import { PageInfo } from 'models/Table'; -const getDeviceCount = () => - axiosGw.get('devices?countOnly=true').then((response) => response.data) as Promise<{ count: number }>; +export const DEVICE_PLATFORMS = ['ALL', 'AP', 'SWITCH'] as const; +export type DevicePlatform = (typeof DEVICE_PLATFORMS)[number]; -export const useGetDeviceCount = ({ enabled }: { enabled: boolean }) => { +const getDeviceCount = (platform: DevicePlatform) => + axiosGw.get(`devices?countOnly=true&platform=${platform}`).then((response) => response.data) as Promise<{ + count: number; + }>; + +export const useGetDeviceCount = ({ enabled, platform = 'ALL' }: { enabled: boolean; platform?: DevicePlatform }) => { const { t } = useTranslation(); const toast = useToast(); - return useQuery(['devices', 'count'], getDeviceCount, { + return useQuery(['devices', 'count', { platform }], () => getDeviceCount(platform), { enabled, onError: (e: AxiosError) => { if (!toast.isActive('inventory-fetching-error')) @@ -96,25 +101,27 @@ export const getSingleDeviceWithStatus = (serialNumber: string) => }) .catch(() => undefined); -const getDevices = (limit: number, offset: number) => +const getDevices = (limit: number, offset: number, platform: DevicePlatform) => axiosGw - .get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`) + .get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}&platform=${platform}`) .then((response) => response.data) as Promise<{ devicesWithStatus: DeviceWithStatus[] }>; export const useGetDevices = ({ pageInfo, enabled, onError, + platform = 'ALL', }: { pageInfo?: PageInfo; enabled: boolean; onError?: (e: AxiosError) => void; + platform?: DevicePlatform; }) => { const offset = pageInfo?.limit !== undefined ? pageInfo.limit * pageInfo.index : 0; return useQuery( - ['devices', 'all', { limit: pageInfo?.limit, offset }], - () => getDevices(pageInfo?.limit || 0, offset), + ['devices', 'all', { limit: pageInfo?.limit, offset, platform }], + () => getDevices(pageInfo?.limit || 0, offset, platform), { keepPreviousData: true, enabled: enabled && pageInfo !== undefined, @@ -124,22 +131,28 @@ export const useGetDevices = ({ ); }; -const getAllDevices = async () => { +const getAllDevices = async (platform: DevicePlatform) => { let offset = 0; let devices: DeviceWithStatus[] = []; let devicesResponse: { devicesWithStatus: DeviceWithStatus[] }; do { // eslint-disable-next-line no-await-in-loop - devicesResponse = await getDevices(500, offset); + devicesResponse = await getDevices(500, offset, platform); devices = devices.concat(devicesResponse.devicesWithStatus); offset += 500; } while (devicesResponse.devicesWithStatus.length === 500); return devices; }; -export const useGetAllDevicesWithStatus = ({ onError }: { onError?: (e: AxiosError) => void }) => { +export const useGetAllDevicesWithStatus = ({ + onError, + platform = 'ALL', +}: { + onError?: (e: AxiosError) => void; + platform?: DevicePlatform; +}) => { const { isReady } = useEndpointStatus('owgw'); - return useQuery(['devices', 'all', 'full'], getAllDevices, { + return useQuery(['devices', 'all', 'full', { platform }], () => getAllDevices(platform), { enabled: isReady && false, onError, }); @@ -432,3 +445,19 @@ export const useUpdateDevice = ({ serialNumber }: { serialNumber: string }) => { }, }); }; + +const deleteDeviceBatch = async (pattern: string) => { + if (pattern.length < 6) throw new Error('Pattern must be at least 6 characters long'); + + axiosGw.delete(`devices?macPattern=${pattern}`); +}; + +export const useDeleteDeviceBatch = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteDeviceBatch, { + onSuccess: () => { + queryClient.invalidateQueries(['devices']); + }, + }); +}; diff --git a/src/hooks/Network/Statistics.ts b/src/hooks/Network/Statistics.ts index 6323addc..4292fbbb 100644 --- a/src/hooks/Network/Statistics.ts +++ b/src/hooks/Network/Statistics.ts @@ -2,7 +2,24 @@ import { useQuery } from '@tanstack/react-query'; import { axiosGw } from 'constants/axiosInstances'; import { AxiosError } from 'models/Axios'; -type DeviceInterfaceStatistics = { +export type DeviceLinkState = { + carrier?: number; + counters?: { + collisions: number; + multicast: number; + rx_bytes: number; + rx_dropped: number; + rx_errors: number; + rx_packets: number; + tx_bytes: number; + tx_dropped: number; + tx_errors: number; + tx_packets: number; + }; + duplex?: string; + speed?: number; +}; +export type DeviceInterfaceStatistics = { clients: { ipv4_addresses?: string[]; ipv6_addresses?: string[]; @@ -138,18 +155,10 @@ export type DeviceStatistics = { }; 'link-state'?: { downstream: { - eth1?: { - carrier?: number; - duplex?: string; - speed?: number; - }; + [key: string]: DeviceLinkState; }; upstream: { - eth0?: { - carrier?: number; - duplex?: string; - speed?: number; - }; + [key: string]: DeviceLinkState; }; }; 'lldp-peers'?: { @@ -190,6 +199,7 @@ export const useGetDeviceLastStats = ({ useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), { enabled: serialNumber !== undefined && serialNumber !== '', staleTime: 1000 * 60, + refetchInterval: 1000 * 60, onError, }); diff --git a/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx b/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx new file mode 100644 index 00000000..02d80228 --- /dev/null +++ b/src/pages/Device/SwitchPortExamination/LinkStateTable.tsx @@ -0,0 +1,182 @@ +import * as React from 'react'; +import { Alert, AlertDescription, AlertIcon, Center } from '@chakra-ui/react'; +import { DeviceLinkState } from 'hooks/Network/Statistics'; +import DataCell from 'components/TableCells/DataCell'; +import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; +import { DataGrid } from 'components/DataTables/DataGrid'; +import { uppercaseFirstLetter } from 'helpers/stringHelper'; + +type Row = DeviceLinkState & { name: string }; +const dataCell = (v: number) => ; + +type Props = { + statistics?: Row[]; + refetch: () => void; + isFetching: boolean; + type: 'upstream' | 'downstream'; +}; + +const LinkStateTable = ({ statistics, refetch, isFetching, type }: Props) => { + const tableController = useDataGrid({ + tableSettingsId: 'switch.link-state.table', + defaultOrder: [ + 'carrier', + 'name', + 'duplex', + 'speed', + 'rx_bytes', + 'rx_dropped', + 'rx_error', + 'rx_packets', + 'tx_bytes', + 'tx_dropped', + 'tx_error', + ], + defaultSortBy: [{ id: 'name', desc: false }], + }); + + const columns: DataGridColumn[] = React.useMemo( + (): DataGridColumn[] => [ + { + id: 'carrier', + header: '', + accessorKey: '', + cell: ({ cell }) => (cell.row.original.carrier ? '🟢' : '🔴'), + meta: { + customWidth: '35px', + }, + }, + { + id: 'name', + header: 'Name', + accessorKey: 'name', + meta: { + customWidth: '35px', + }, + }, + { + id: 'duplex', + header: 'Duplex', + accessorKey: 'duplex', + cell: ({ cell }) => (cell.row.original.duplex ? uppercaseFirstLetter(cell.row.original.duplex) : '-'), + meta: { + customWidth: '35px', + }, + }, + { + id: 'speed', + header: 'Speed', + accessorKey: 'speed', + cell: ({ cell }) => `${(cell.row.original.speed ?? 0) / 1000} Gbps`, + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_bytes', + header: 'Rx', + accessorKey: 'rx_bytes', + cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_bytes ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_dropped', + header: 'Rx Dropped', + accessorKey: 'rx_dropped', + cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_dropped ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_error', + header: 'Rx Errors', + accessorKey: 'rx_error', + cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_errors ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_packets', + header: 'Rx Packets', + accessorKey: 'counters.rx_packets', + cell: ({ cell }) => (cell.row.original.counters?.rx_packets ?? 0).toLocaleString(), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_bytes', + header: 'Tx', + accessorKey: 'tx_bytes', + cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_bytes ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_dropped', + header: 'Tx Dropped', + accessorKey: 'tx_dropped', + cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_dropped ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_error', + header: 'Tx Errors', + accessorKey: 'tx_error', + cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_errors ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_packets', + header: 'Tx Packets', + accessorKey: 'counters.tx_packets', + cell: ({ cell }) => (cell.row.original.counters?.tx_packets ?? 0).toLocaleString(), + meta: { + customWidth: '35px', + }, + }, + ], + [], + ); + + if (!statistics || statistics?.length === 0) { + return ( +
+ + + + There are currently no {type} link-states provided in this devices statistics + + +
+ ); + } + + return ( + + controller={tableController} + header={{ + title: '', + objectListed: 'Statistics', + }} + columns={columns} + isLoading={isFetching} + data={statistics ?? []} + options={{ + refetch, + isHidingControls: true, + }} + /> + ); +}; + +export default LinkStateTable; diff --git a/src/pages/Device/SwitchPortExamination/SwitchInterfaceTable.tsx b/src/pages/Device/SwitchPortExamination/SwitchInterfaceTable.tsx new file mode 100644 index 00000000..d3e29e78 --- /dev/null +++ b/src/pages/Device/SwitchPortExamination/SwitchInterfaceTable.tsx @@ -0,0 +1,170 @@ +import * as React from 'react'; +import { Alert, AlertDescription, AlertIcon, Center } from '@chakra-ui/react'; +import { DeviceInterfaceStatistics, DeviceStatistics } from 'hooks/Network/Statistics'; +import DataCell from 'components/TableCells/DataCell'; +import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; +import DurationCell from 'components/TableCells/DurationCell'; +import { DataGrid } from 'components/DataTables/DataGrid'; + +const dataCell = (v: number) => ; + +type Props = { + statistics: DeviceStatistics; + refetch: () => void; + isFetching: boolean; +}; + +const SwitchInterfaceTable = ({ statistics, refetch, isFetching }: Props) => { + const tableController = useDataGrid({ + tableSettingsId: 'switch.interfaces.table', + defaultOrder: [ + 'name', + 'uptime', + 'clients', + 'rx_bytes', + 'rx_dropped', + 'rx_error', + 'rx_packets', + 'tx_bytes', + 'tx_dropped', + 'tx_error', + ], + defaultSortBy: [{ id: 'name', desc: false }], + }); + + const columns: DataGridColumn[] = React.useMemo( + (): DataGridColumn[] => [ + { + id: 'name', + header: 'Name', + accessorKey: 'name', + meta: { + customWidth: '35px', + }, + }, + { + id: 'uptime', + header: 'Uptime', + + accessorKey: 'uptime', + cell: ({ cell }) => , + meta: { + customWidth: '35px', + }, + }, + { + id: 'clients', + header: 'Clients', + + accessorKey: 'clients', + cell: ({ cell }) => cell.row.original.clients?.length ?? 0, + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_bytes', + header: 'Rx', + accessorKey: 'rx_bytes', + cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_bytes ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_dropped', + header: 'Rx Dropped', + accessorKey: 'rx_dropped', + cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_dropped ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_error', + header: 'Rx Errors', + accessorKey: 'rx_error', + cell: ({ cell }) => dataCell(cell.row.original.counters?.rx_errors ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'rx_packets', + header: 'Rx Packets', + accessorKey: 'counters.rx_packets', + cell: ({ cell }) => (cell.row.original.counters?.rx_packets ?? 0).toLocaleString(), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_bytes', + header: 'Tx', + accessorKey: 'tx_bytes', + cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_bytes ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_dropped', + header: 'Tx Dropped', + accessorKey: 'tx_dropped', + cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_dropped ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_error', + header: 'Tx Errors', + accessorKey: 'tx_error', + cell: ({ cell }) => dataCell(cell.row.original.counters?.tx_errors ?? 0), + meta: { + customWidth: '35px', + }, + }, + { + id: 'tx_packets', + header: 'Tx Packets', + accessorKey: 'counters.tx_packets', + cell: ({ cell }) => (cell.row.original.counters?.tx_packets ?? 0).toLocaleString(), + meta: { + customWidth: '35px', + }, + }, + ], + [], + ); + + if (!statistics.interfaces) { + return ( +
+ + + There are currently no interfaces provided in this devices statistics + +
+ ); + } + + return ( + + controller={tableController} + header={{ + title: '', + objectListed: 'Statistics', + }} + columns={columns} + isLoading={isFetching} + data={statistics.interfaces ?? []} + options={{ + refetch, + isHidingControls: true, + }} + /> + ); +}; + +export default SwitchInterfaceTable; diff --git a/src/pages/Device/SwitchPortExamination/index.tsx b/src/pages/Device/SwitchPortExamination/index.tsx new file mode 100644 index 00000000..884749fb --- /dev/null +++ b/src/pages/Device/SwitchPortExamination/index.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; +import LinkStateTable from './LinkStateTable'; +import SwitchInterfaceTable from './SwitchInterfaceTable'; +import { DeviceLinkState, useGetDeviceLastStats } from 'hooks/Network/Statistics'; +import { Card } from 'components/Containers/Card'; +import { CardBody } from 'components/Containers/Card/CardBody'; + +type Props = { + serialNumber: string; +}; + +const SwitchPortExamination = ({ serialNumber }: Props) => { + const [tabIndex, setTabIndex] = React.useState(0); + + const handleTabsChange = React.useCallback((index: number) => { + setTabIndex(index); + }, []); + const getStats = useGetDeviceLastStats({ serialNumber }); + + const upLinkStates: (DeviceLinkState & { name: string })[] = React.useMemo(() => { + if (!getStats.data || !getStats.data['link-state']?.upstream) return []; + + return Object.entries(getStats.data['link-state']?.upstream).map(([name, value]) => ({ + ...value, + name, + })); + }, [getStats.data]); + const downLinkStates: (DeviceLinkState & { name: string })[] = React.useMemo(() => { + if (!getStats.data || !getStats.data['link-state']?.downstream) return []; + + return Object.entries(getStats.data['link-state']?.downstream).map(([name, value]) => ({ + ...value, + name, + })); + }, [getStats.data]); + + return ( + + + + + + Interfaces + + + Link-State (Up) + + + Link-State (Down) + + + + + {getStats.data ? ( + + ) : ( + + )} + + + {getStats.data ? ( + + ) : ( + + )} + + + {getStats.data ? ( + + ) : ( + + )} + + + + + + ); +}; + +export default SwitchPortExamination; diff --git a/src/pages/Device/Wrapper.tsx b/src/pages/Device/Wrapper.tsx index 4ca3de07..91d5dc75 100644 --- a/src/pages/Device/Wrapper.tsx +++ b/src/pages/Device/Wrapper.tsx @@ -42,10 +42,13 @@ import FactoryResetModal from 'components/Modals/FactoryResetModal'; import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal'; import { RebootModal } from 'components/Modals/RebootModal'; import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal'; +import ethernetConnected from './ethernetIconConnected.svg?react'; +import ethernetDisconnected from './ethernetIconDisconnected.svg?react'; import { TelemetryModal } from 'components/Modals/TelemetryModal'; import { TraceModal } from 'components/Modals/TraceModal'; import { WifiScanModal } from 'components/Modals/WifiScanModal'; import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; +import SwitchPortExamination from './SwitchPortExamination'; type Props = { serialNumber: string; @@ -119,11 +122,15 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { ); } + let icon = getStatus.data.connected ? WifiHigh : WifiSlash; + if (getDevice.data?.deviceType === 'SWITCH') + icon = getStatus.data.connected ? ethernetConnected : ethernetDisconnected; + return ( ); }, [getStatus.data, getDevice.data]); @@ -318,7 +325,11 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { - + {getDevice.data?.deviceType === 'AP' ? ( + + ) : ( + + )} {getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? ( diff --git a/src/pages/Device/ethernetIconConnected.svg b/src/pages/Device/ethernetIconConnected.svg new file mode 100644 index 00000000..b7286971 --- /dev/null +++ b/src/pages/Device/ethernetIconConnected.svg @@ -0,0 +1,2 @@ + + diff --git a/src/pages/Device/ethernetIconDisconnected.svg b/src/pages/Device/ethernetIconDisconnected.svg new file mode 100644 index 00000000..cfd2e8ef --- /dev/null +++ b/src/pages/Device/ethernetIconDisconnected.svg @@ -0,0 +1,2 @@ + + diff --git a/src/pages/Devices/ListCard/icons/SWITCH.png b/src/pages/Devices/ListCard/icons/SWITCH.png index 81c4c6d64aa73df1b8bc0d99f9760b79cbb81e28..2e391f56e11f298aaadf941402ab2d97dbb01c68 100644 GIT binary patch delta 3213 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 IM6N<$g3&rX8~^|S diff --git a/src/pages/Devices/ListCard/index.tsx b/src/pages/Devices/ListCard/index.tsx index 8691fd62..b674ec7d 100644 --- a/src/pages/Devices/ListCard/index.tsx +++ b/src/pages/Devices/ListCard/index.tsx @@ -1,5 +1,16 @@ import * as React from 'react'; -import { Box, Center, Image, Link, Tag, TagLabel, TagRightIcon, Tooltip, useDisclosure } from '@chakra-ui/react'; +import { + Box, + Center, + Image, + Link, + Select, + Tag, + TagLabel, + TagRightIcon, + Tooltip, + useDisclosure, +} from '@chakra-ui/react'; import { CheckCircle, Heart, @@ -38,7 +49,7 @@ import { TraceModal } from 'components/Modals/TraceModal'; import { WifiScanModal } from 'components/Modals/WifiScanModal'; import DataCell from 'components/TableCells/DataCell'; import NumberCell from 'components/TableCells/NumberCell'; -import { DeviceWithStatus, useGetDeviceCount, useGetDevices } from 'hooks/Network/Devices'; +import { DevicePlatform, DeviceWithStatus, useGetDeviceCount, useGetDevices } from 'hooks/Network/Devices'; import { FirmwareAgeResponse, useGetFirmwareAges } from 'hooks/Network/Firmware'; const fourDigitNumber = (v?: number) => { @@ -72,6 +83,7 @@ const DeviceListCard = () => { const { t } = useTranslation(); const navigate = useNavigate(); const [serialNumber, setSerialNumber] = React.useState(''); + const [platform, setPlatform] = React.useState('ALL'); const scanModalProps = useDisclosure(); const resetModalProps = useDisclosure(); const upgradeModalProps = useDisclosure(); @@ -110,13 +122,14 @@ const DeviceListCard = () => { 'actions', ], }); - const getCount = useGetDeviceCount({ enabled: true }); + const getCount = useGetDeviceCount({ enabled: true, platform }); const getDevices = useGetDevices({ pageInfo: { limit: tableController.pageInfo.pageSize, index: tableController.pageInfo.pageIndex, }, enabled: true, + platform, }); const getAges = useGetFirmwareAges({ serialNumbers: getDevices.data?.devicesWithStatus.map((device) => device.serialNumber), @@ -556,12 +569,7 @@ const DeviceListCard = () => { header: t('analytics.last_connected'), footer: '', accessorKey: 'lastRecordedContact', - cell: (v) => - dateCell( - v.cell.row.original.lastContact !== 0 - ? v.cell.row.original.lastContact - : v.cell.row.original.lastRecordedContact, - ), + cell: (v) => dateCell(v.cell.row.original.lastRecordedContact), enableSorting: false, meta: { headerOptions: { @@ -719,7 +727,21 @@ const DeviceListCard = () => { header={{ title: `${getCount.data?.count ?? 0} ${t('devices.title')}`, objectListed: t('devices.title'), - leftContent: , + leftContent: ( + <> + + + + ), otherButtons: ( device.serialNumber)} /> ), diff --git a/vite.config.ts b/vite.config.ts index 59431ddb..df2d0bea 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; import react from '@vitejs/plugin-react'; import { VitePWA } from 'vite-plugin-pwa'; +import svgr from 'vite-plugin-svgr'; export default defineConfig({ plugins: [ @@ -15,10 +16,11 @@ export default defineConfig({ /* other options */ }, manifest: { - name: 'OpenWiFi Controller App', - short_name: 'OpenWiFiController', - description: 'OpenWiFi Controller App', + name: 'Arilia Controller App', + short_name: 'AriController', + description: 'Arilia Controller Work App', theme_color: '#000000', + icons: [ { src: 'android-chrome-192x192.png', @@ -44,13 +46,14 @@ export default defineConfig({ ], }, }), + svgr(), ], build: { outDir: './build', chunkSizeWarningLimit: 1000, }, server: { - port: 3000, + port: 3001, open: true, }, esbuild: { From deb7715ea1a38f9c10e14a0abb81fa75b44568cb Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 11 Jan 2024 12:57:27 -0500 Subject: [PATCH 2/2] [WIFI-13282] Add support for OLS Signed-off-by: Charles --- vite.config.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index df2d0bea..cc9d7fca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,11 +16,10 @@ export default defineConfig({ /* other options */ }, manifest: { - name: 'Arilia Controller App', - short_name: 'AriController', - description: 'Arilia Controller Work App', + name: 'OpenWiFi Controller App', + short_name: 'OpenWiFiController', + description: 'OpenWiFi Controller App', theme_color: '#000000', - icons: [ { src: 'android-chrome-192x192.png', @@ -53,7 +52,7 @@ export default defineConfig({ chunkSizeWarningLimit: 1000, }, server: { - port: 3001, + port: 3000, open: true, }, esbuild: {