[WIFI-13282] Add support for OLS

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2024-01-11 12:56:20 -05:00
parent cf977b7612
commit adaebb17e7
17 changed files with 907 additions and 53 deletions

340
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -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'}
/>
</Tooltip>
<Tooltip label={t('controller.configure.title')}>
@@ -205,7 +206,7 @@ const DeviceActionDropdown = ({
isDisabled={isDisabled}
onClick={handleOpenScan}
colorScheme="teal"
hidden={isCompact}
hidden={isCompact || deviceType !== 'AP'}
/>
</Tooltip>
<Menu>
@@ -221,7 +222,7 @@ const DeviceActionDropdown = ({
<Portal>
<MenuList maxH="315px" overflowY="auto">
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact}>
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact || deviceType !== 'AP'}>
{t('controller.configure.title')}
</MenuItem>
<MenuItem onClick={handleConnectClick} hidden={!isCompact}>
@@ -239,7 +240,7 @@ const DeviceActionDropdown = ({
<MenuItem onClick={handleUpdateToLatest} hidden>
{t('premium.toolbox.upgrade_to_latest')}
</MenuItem>
<MenuItem onClick={handleOpenScan} hidden={!isCompact}>
<MenuItem onClick={handleOpenScan} hidden={!isCompact || deviceType !== 'AP'}>
{t('commands.wifiscan')}
</MenuItem>
</MenuList>

View File

@@ -26,7 +26,7 @@ export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...p
return (
<Tooltip label={tooltip ?? label}>
<Tag size="lg" colorScheme="blue" {...props}>
<TagLeftIcon boxSize="18px" as={icon} />
<TagLeftIcon boxSize="18px" as={icon} mt={-0.5} />
<TagLabel>{label}</TagLabel>
</Tag>
</Tooltip>

1
src/custom.d.ts vendored
View File

@@ -8,3 +8,4 @@ declare module '*.png' {
const value: string;
export = value;
}
/// <reference types="vite-plugin-svgr/client" />

View File

@@ -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']);
},
});
};

View File

@@ -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,
});

View File

@@ -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) => <DataCell bytes={v} />;
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<Row>[] = React.useMemo(
(): DataGridColumn<Row>[] => [
{
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 (
<Center>
<Alert status="info">
<AlertIcon />
<AlertDescription>
There are currently no {type} link-states provided in this devices statistics
</AlertDescription>
</Alert>
</Center>
);
}
return (
<DataGrid<Row>
controller={tableController}
header={{
title: '',
objectListed: 'Statistics',
}}
columns={columns}
isLoading={isFetching}
data={statistics ?? []}
options={{
refetch,
isHidingControls: true,
}}
/>
);
};
export default LinkStateTable;

View File

@@ -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) => <DataCell bytes={v} />;
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<DeviceInterfaceStatistics>[] = React.useMemo(
(): DataGridColumn<DeviceInterfaceStatistics>[] => [
{
id: 'name',
header: 'Name',
accessorKey: 'name',
meta: {
customWidth: '35px',
},
},
{
id: 'uptime',
header: 'Uptime',
accessorKey: 'uptime',
cell: ({ cell }) => <DurationCell seconds={cell.row.original.uptime} />,
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 (
<Center>
<Alert status="info">
<AlertIcon />
<AlertDescription>There are currently no interfaces provided in this devices statistics</AlertDescription>
</Alert>
</Center>
);
}
return (
<DataGrid<DeviceInterfaceStatistics>
controller={tableController}
header={{
title: '',
objectListed: 'Statistics',
}}
columns={columns}
isLoading={isFetching}
data={statistics.interfaces ?? []}
options={{
refetch,
isHidingControls: true,
}}
/>
);
};
export default SwitchInterfaceTable;

View File

@@ -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 (
<Card p={0} mb={4}>
<CardBody p={0} display="block">
<Tabs index={tabIndex} onChange={handleTabsChange} variant="enclosed" w="100%">
<TabList>
<Tab fontSize="lg" fontWeight="bold">
Interfaces
</Tab>
<Tab fontSize="lg" fontWeight="bold">
Link-State (Up)
</Tab>
<Tab fontSize="lg" fontWeight="bold">
Link-State (Down)
</Tab>
</TabList>
<TabPanels>
<TabPanel>
{getStats.data ? (
<SwitchInterfaceTable
statistics={getStats.data}
refetch={getStats.refetch}
isFetching={getStats.isFetching}
/>
) : (
<Spinner size="xl" />
)}
</TabPanel>
<TabPanel>
{getStats.data ? (
<LinkStateTable
statistics={upLinkStates}
refetch={getStats.refetch}
isFetching={getStats.isFetching}
type="upstream"
/>
) : (
<Spinner size="xl" />
)}
</TabPanel>
<TabPanel>
{getStats.data ? (
<LinkStateTable
statistics={downLinkStates}
refetch={getStats.refetch}
isFetching={getStats.isFetching}
type="downstream"
/>
) : (
<Spinner size="xl" />
)}
</TabPanel>
</TabPanels>
</Tabs>
</CardBody>
</Card>
);
};
export default SwitchPortExamination;

View File

@@ -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 (
<ResponsiveTag
label={getStatus?.data?.connected ? t('common.connected') : t('common.disconnected')}
colorScheme={getStatus?.data?.connected ? 'green' : 'red'}
icon={getStatus.data.connected ? WifiHigh : WifiSlash}
icon={icon}
/>
);
}, [getStatus.data, getDevice.data]);
@@ -318,7 +325,11 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<DeviceSummary serialNumber={serialNumber} />
<DeviceDetails serialNumber={serialNumber} />
<DeviceStatisticsCard serialNumber={serialNumber} />
<WifiAnalysisCard serialNumber={serialNumber} />
{getDevice.data?.deviceType === 'AP' ? (
<WifiAnalysisCard serialNumber={serialNumber} />
) : (
<SwitchPortExamination serialNumber={serialNumber} />
)}
<DeviceLogsCard serialNumber={serialNumber} />
{getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? (
<RadiusClientsCard serialNumber={serialNumber} />

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#48BB78;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><rect class="cls-1" x="1.5" y="1.5" width="21" height="21" rx="1.91"/><polygon class="cls-1" points="5.32 6.27 5.32 13.91 7.23 13.91 7.23 15.82 10.09 15.82 10.09 17.73 13.91 17.73 13.91 15.82 16.77 15.82 16.77 13.91 18.68 13.91 18.68 6.27 5.32 6.27"/><line class="cls-1" x1="8.18" y1="9.14" x2="8.18" y2="6.27"/><line class="cls-1" x1="12" y1="9.14" x2="12" y2="6.27"/><line class="cls-1" x1="15.82" y1="9.14" x2="15.82" y2="6.27"/></svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#FC8181;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><rect class="cls-1" x="1.5" y="1.5" width="21" height="21" rx="1.91"/><polygon class="cls-1" points="5.32 6.27 5.32 13.91 7.23 13.91 7.23 15.82 10.09 15.82 10.09 17.73 13.91 17.73 13.91 15.82 16.77 15.82 16.77 13.91 18.68 13.91 18.68 6.27 5.32 6.27"/><line class="cls-1" x1="8.18" y1="9.14" x2="8.18" y2="6.27"/><line class="cls-1" x1="12" y1="9.14" x2="12" y2="6.27"/><line class="cls-1" x1="15.82" y1="9.14" x2="15.82" y2="6.27"/></svg>

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -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<string>('');
const [platform, setPlatform] = React.useState<DevicePlatform>('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: <GlobalSearchBar />,
leftContent: (
<>
<GlobalSearchBar />
<Select
value={platform}
onChange={(e) => setPlatform(e.target.value as DevicePlatform)}
w="max-content"
ml={2}
>
<option value="ALL">All</option>
<option value="AP">APs</option>
<option value="SWITCH">Switches</option>
</Select>
</>
),
otherButtons: (
<ExportDevicesTableButton currentPageSerialNumbers={data.map((device) => device.serialNumber)} />
),

View File

@@ -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: {