diff --git a/package-lock.json b/package-lock.json index 5a5665d..bd54515 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 1af535c..e587daf 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 0000000..1a241b3 Binary files /dev/null and b/public/devices/edgecore_ecs4125.png differ diff --git a/src/components/Buttons/DeviceActionDropdown/index.tsx b/src/components/Buttons/DeviceActionDropdown/index.tsx index 31b2b39..47d59b7 100644 --- a/src/components/Buttons/DeviceActionDropdown/index.tsx +++ b/src/components/Buttons/DeviceActionDropdown/index.tsx @@ -54,6 +54,7 @@ const DeviceActionDropdown = ({ }: Props) => { 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 7508cbb..890afbc 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 9c76625..6aa6b4b 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 4c5d65b..b91c3f4 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 6323add..4292fbb 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 0000000..02d8022 --- /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 0000000..d3e29e7 --- /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 0000000..884749f --- /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 4ca3de0..91d5dc7 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 0000000..b728697 --- /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 0000000..cfd2e8e --- /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 81c4c6d..2e391f5 100644 Binary files a/src/pages/Devices/ListCard/icons/SWITCH.png and b/src/pages/Devices/ListCard/icons/SWITCH.png differ diff --git a/src/pages/Devices/ListCard/index.tsx b/src/pages/Devices/ListCard/index.tsx index 8691fd6..b674ec7 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 59431dd..df2d0be 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: {