mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-28 17:02:21 +00:00
[WIFI-12506] Added radius search and radius clients tile
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
@@ -3,3 +3,4 @@ build
|
||||
dist
|
||||
node_modules
|
||||
.github
|
||||
/helm
|
||||
|
||||
72
package-lock.json
generated
72
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.10.0(11)",
|
||||
"version": "2.10.0(14)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.10.0(11)",
|
||||
"version": "2.10.0(14)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@textea/json-viewer": "^2.10.0",
|
||||
"axios": "^1.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
"chakra-react-select": "^4.3.0",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"chart.js": "^3.9.1",
|
||||
"dagre": "^0.8.5",
|
||||
"fast-equals": "^4.0.3",
|
||||
@@ -2839,14 +2839,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT"
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg=="
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.6.tgz",
|
||||
"integrity": "sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.0.1"
|
||||
"@floating-ui/core": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/inter": {
|
||||
@@ -4387,15 +4389,17 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chakra-react-select": {
|
||||
"version": "4.3.0",
|
||||
"license": "MIT",
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chakra-react-select/-/chakra-react-select-4.6.0.tgz",
|
||||
"integrity": "sha512-Ckcs+ofX5LxCc0oOz4SorDIRqF/afd5tAQOa694JVJiIckYorUmZASEUSSDdXaZltsUAtJE11CUmEZgVVsk9Eg==",
|
||||
"dependencies": {
|
||||
"react-select": "^5.5.0"
|
||||
"react-select": "5.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@chakra-ui/form-control": "^2.0.0",
|
||||
"@chakra-ui/icon": "^3.0.0",
|
||||
"@chakra-ui/layout": "^2.0.0",
|
||||
"@chakra-ui/media-query": "^3.0.0",
|
||||
"@chakra-ui/menu": "^2.0.0",
|
||||
"@chakra-ui/spinner": "^2.0.0",
|
||||
"@chakra-ui/system": "^2.0.0",
|
||||
@@ -7979,15 +7983,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-select": {
|
||||
"version": "5.5.2",
|
||||
"license": "MIT",
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.0.tgz",
|
||||
"integrity": "sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"memoize-one": "^5.0.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"use-isomorphic-layout-effect": "^1.1.2"
|
||||
@@ -7997,6 +8002,11 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-select/node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.1",
|
||||
"license": "MIT",
|
||||
@@ -9129,7 +9139,8 @@
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
|
||||
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
@@ -11486,12 +11497,16 @@
|
||||
}
|
||||
},
|
||||
"@floating-ui/core": {
|
||||
"version": "1.0.1"
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz",
|
||||
"integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg=="
|
||||
},
|
||||
"@floating-ui/dom": {
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.6.tgz",
|
||||
"integrity": "sha512-02vxFDuvuVPs22iJICacezYJyf7zwwOCWkPNkWNBr1U0Qt1cKFYzWvxts0AmqcOQGwt/3KJWcWIgtbUU38keyw==",
|
||||
"requires": {
|
||||
"@floating-ui/core": "^1.0.1"
|
||||
"@floating-ui/core": "^1.2.6"
|
||||
}
|
||||
},
|
||||
"@fontsource/inter": {
|
||||
@@ -12379,9 +12394,11 @@
|
||||
"version": "1.0.30001422"
|
||||
},
|
||||
"chakra-react-select": {
|
||||
"version": "4.3.0",
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chakra-react-select/-/chakra-react-select-4.6.0.tgz",
|
||||
"integrity": "sha512-Ckcs+ofX5LxCc0oOz4SorDIRqF/afd5tAQOa694JVJiIckYorUmZASEUSSDdXaZltsUAtJE11CUmEZgVVsk9Eg==",
|
||||
"requires": {
|
||||
"react-select": "^5.5.0"
|
||||
"react-select": "5.7.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
@@ -14532,17 +14549,26 @@
|
||||
}
|
||||
},
|
||||
"react-select": {
|
||||
"version": "5.5.2",
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.0.tgz",
|
||||
"integrity": "sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.0",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.8.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"memoize-one": "^5.0.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"use-isomorphic-layout-effect": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-style-singleton": {
|
||||
@@ -15209,6 +15235,8 @@
|
||||
},
|
||||
"use-isomorphic-layout-effect": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
|
||||
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
|
||||
"requires": {}
|
||||
},
|
||||
"use-sidecar": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.10.0(11)",
|
||||
"version": "2.10.0(14)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
@@ -27,7 +27,7 @@
|
||||
"@react-spring/web": "^9.5.5",
|
||||
"axios": "^1.1.3",
|
||||
"buffer": "^6.0.3",
|
||||
"chakra-react-select": "^4.3.0",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"dagre": "^0.8.5",
|
||||
"formik": "^2.2.9",
|
||||
"fast-equals": "^4.0.3",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,79 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import { Select } from 'chakra-react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useControllerDeviceSearch } from 'contexts/ControllerSocketProvider/hooks/Commands/useDeviceSearch';
|
||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||
|
||||
const DeviceSearchBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { token } = useAuth();
|
||||
const { startWebSocket, isWebSocketOpen } = useControllerStore((state) => ({
|
||||
startWebSocket: state.startWebSocket,
|
||||
isWebSocketOpen: state.isWebSocketOpen,
|
||||
}));
|
||||
const { inputValue, results, onInputChange } = useControllerDeviceSearch({
|
||||
minLength: 2,
|
||||
});
|
||||
|
||||
const NoOptionsMessage = React.useCallback(
|
||||
() => (
|
||||
<Heading size="sm" textAlign="center">
|
||||
{isWebSocketOpen ? t('common.no_devices_found') : `${t('controller.devices.connecting')}...`}
|
||||
</Heading>
|
||||
),
|
||||
[t, isWebSocketOpen],
|
||||
);
|
||||
const onClick = React.useCallback((v: { value: string }) => {
|
||||
navigate(`/devices/${v.value}`);
|
||||
}, []);
|
||||
const onChange = React.useCallback((v: string) => {
|
||||
if ((v.length === 0 || v.match('^[a-fA-F0-9-*]+$')) && v.length <= 13) onInputChange(v);
|
||||
}, []);
|
||||
|
||||
const onFocus = () => {
|
||||
if (!isWebSocketOpen && token && token.length > 0) {
|
||||
startWebSocket(token, 0);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Select
|
||||
chakraStyles={{
|
||||
control: (provided) => ({
|
||||
...provided,
|
||||
borderRadius: '15px',
|
||||
color: 'unset',
|
||||
}),
|
||||
input: (provided) => ({
|
||||
...provided,
|
||||
width: '140px',
|
||||
}),
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
backgroundColor: 'unset',
|
||||
border: 'unset',
|
||||
}),
|
||||
menu: (provided) => ({
|
||||
...provided,
|
||||
color: 'black',
|
||||
}),
|
||||
}}
|
||||
components={{ NoOptionsMessage }}
|
||||
// @ts-ignore
|
||||
options={results.map((v: string) => ({ label: v, value: v }))}
|
||||
filterOption={() => true}
|
||||
inputValue={inputValue}
|
||||
value={inputValue}
|
||||
placeholder={t('common.search')}
|
||||
onInputChange={onChange}
|
||||
onFocus={onFocus}
|
||||
// @ts-ignore
|
||||
onChange={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceSearchBar;
|
||||
155
src/components/GlobalSearchBar/index.tsx
Normal file
155
src/components/GlobalSearchBar/index.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import * as React from 'react';
|
||||
import { Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||
import {
|
||||
AsyncSelect,
|
||||
ChakraStylesConfig,
|
||||
GroupBase,
|
||||
LoadingIndicatorProps,
|
||||
OptionBase,
|
||||
OptionsOrGroups,
|
||||
chakraComponents,
|
||||
} from 'chakra-react-select';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||
import debounce from 'helpers/debounce';
|
||||
import { getUsernameRadiusSessions } from 'hooks/Network/Radius';
|
||||
|
||||
const chakraStyles: ChakraStylesConfig<SearchOption, false, GroupBase<SearchOption>> = {
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
width: '32px',
|
||||
}),
|
||||
placeholder: (provided) => ({
|
||||
...provided,
|
||||
lineHeight: '1',
|
||||
}),
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
width: '320px',
|
||||
}),
|
||||
};
|
||||
|
||||
interface SearchOption extends OptionBase {
|
||||
label: string;
|
||||
value: string;
|
||||
type: 'serial' | 'radius-username' | 'radius-mac';
|
||||
}
|
||||
|
||||
const asyncComponents = {
|
||||
LoadingIndicator: (props: LoadingIndicatorProps<SearchOption, false, GroupBase<SearchOption>>) => {
|
||||
const { color, emptyColor } = useColorModeValue(
|
||||
{
|
||||
color: 'blue.500',
|
||||
emptyColor: 'blue.100',
|
||||
},
|
||||
{
|
||||
color: 'blue.300',
|
||||
emptyColor: 'blue.900',
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<chakraComponents.LoadingIndicator
|
||||
color={color}
|
||||
emptyColor={emptyColor}
|
||||
speed="750ms"
|
||||
spinnerSize="md"
|
||||
thickness="3px"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalSearchBar = () => {
|
||||
const navigate = useNavigate();
|
||||
const store = useControllerStore((state) => ({
|
||||
searchSerialNumber: state.searchSerialNumber,
|
||||
}));
|
||||
|
||||
const onNewSearch = React.useCallback(
|
||||
async (v: string, callback: (options: OptionsOrGroups<SearchOption, GroupBase<SearchOption>>) => void) => {
|
||||
if (v.length < 3) return callback([]);
|
||||
|
||||
if (v.includes('rad:')) {
|
||||
const trimmed = v.replace('rad:', '').trim();
|
||||
if (trimmed.length < 3) return callback([]);
|
||||
const cleaned = trimmed.toLowerCase();
|
||||
return getUsernameRadiusSessions(cleaned)
|
||||
.then((res) =>
|
||||
callback(
|
||||
res
|
||||
.map((r) => ({
|
||||
label: r.serialNumber,
|
||||
value: r.serialNumber,
|
||||
type: 'radius-username',
|
||||
}))
|
||||
.filter(({ value }, i, a) => a.findIndex((t) => t.value === value) === i) as SearchOption[],
|
||||
),
|
||||
)
|
||||
.then(() => callback([]));
|
||||
}
|
||||
if (v.match('^[a-fA-F0-9-*]+$')) {
|
||||
await store
|
||||
.searchSerialNumber(v)
|
||||
.then((res) => {
|
||||
callback(
|
||||
res.map((r) => ({
|
||||
label: r,
|
||||
value: r,
|
||||
type: 'serial',
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => []);
|
||||
}
|
||||
return callback([]);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const debouncedNewSearch = React.useCallback(
|
||||
debounce(
|
||||
// @ts-ignore
|
||||
({
|
||||
v,
|
||||
callback,
|
||||
}: {
|
||||
v: string;
|
||||
callback: (options: OptionsOrGroups<SearchOption, GroupBase<SearchOption>>) => void;
|
||||
}) => {
|
||||
onNewSearch(v as string, callback);
|
||||
},
|
||||
300,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={`Search serial numbers and radius clients. For radius clients you can either use the client's username (rad:client@client.com)
|
||||
or use the client's station ID (rad:11:22:33:44:55:66)`}
|
||||
shouldWrapChildren
|
||||
placement="left"
|
||||
>
|
||||
<AsyncSelect<SearchOption, false, GroupBase<SearchOption>>
|
||||
name="global_search"
|
||||
chakraStyles={chakraStyles}
|
||||
closeMenuOnSelect
|
||||
placeholder="Search MACs or radius clients"
|
||||
components={asyncComponents}
|
||||
loadOptions={(inputValue, callback) => {
|
||||
debouncedNewSearch({ v: inputValue, callback });
|
||||
}}
|
||||
value={null}
|
||||
onChange={(newValue) => {
|
||||
if (newValue) {
|
||||
navigate(`/devices/${newValue.value}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchBar;
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import create from 'zustand';
|
||||
import { ControllerSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { randomIntId } from 'helpers/stringHelper';
|
||||
import { DevicesStats, DeviceWithStatus } from 'hooks/Network/Devices';
|
||||
import { NotificationType } from 'models/Socket';
|
||||
|
||||
@@ -122,6 +123,7 @@ export type ControllerStoreState = {
|
||||
lastSearchResults: string[];
|
||||
setLastSearchResults: (result: string[]) => void;
|
||||
errors: { str: string; timestamp: Date }[];
|
||||
searchSerialNumber: (serialNumber: string, timeout?: number) => Promise<string[]>;
|
||||
};
|
||||
|
||||
export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||
@@ -169,13 +171,23 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||
id: uuid(),
|
||||
};
|
||||
|
||||
const eventsToFire = get().eventListeners.filter(
|
||||
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
||||
);
|
||||
const eventsToFire = get().eventListeners.filter((event) => {
|
||||
if (event.type === 'DEVICE_CONNECTION' || event.type === 'DEVICE_DISCONNECTION') {
|
||||
return event.serialNumber === msg.serialNumber;
|
||||
}
|
||||
if (msg.type === 'DEVICE_SEARCH_RESULTS' && event.type === 'DEVICE_SEARCH_RESULTS') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
for (const event of eventsToFire) {
|
||||
event.callback();
|
||||
if (event.type === 'DEVICE_SEARCH_RESULTS' && msg.type === 'DEVICE_SEARCH_RESULTS') {
|
||||
event.callback(msg.serialNumbers);
|
||||
} else if (event.type === 'DEVICE_CONNECTION' || event.type === 'DEVICE_DISCONNECTION') {
|
||||
event.callback();
|
||||
}
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
@@ -211,6 +223,37 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||
const ws = get().webSocket;
|
||||
if (ws) ws.send(str);
|
||||
},
|
||||
searchSerialNumber: async (serialNumber: string, timeout = 1000 * 5): Promise<string[]> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Promise timed out after ${timeout} ms`));
|
||||
}, timeout);
|
||||
const ws = get().webSocket;
|
||||
if (ws) {
|
||||
const id = randomIntId();
|
||||
get().addEventListeners([
|
||||
{
|
||||
id: uuid(),
|
||||
type: 'DEVICE_SEARCH_RESULTS',
|
||||
searchId: id,
|
||||
callback: (serialNumbers: string[]) => {
|
||||
clearTimeout(timer);
|
||||
resolve(serialNumbers);
|
||||
},
|
||||
},
|
||||
]);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
command: 'serial_number_search',
|
||||
serial_prefix: serialNumber,
|
||||
id: randomIntId(),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
clearTimeout(timer);
|
||||
reject(new Error('No websocket connection'));
|
||||
}
|
||||
}),
|
||||
startWebSocket: (token: string, tries = 0) => {
|
||||
const newTries = tries + 1;
|
||||
if (tries <= 10) {
|
||||
|
||||
@@ -116,9 +116,16 @@ export type SocketWebSocketNotificationData =
|
||||
log?: undefined;
|
||||
message: InitialSocketMessage;
|
||||
};
|
||||
export type SocketEventCallback = {
|
||||
id: string;
|
||||
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
};
|
||||
export type SocketEventCallback =
|
||||
| {
|
||||
id: string;
|
||||
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: 'DEVICE_SEARCH_RESULTS';
|
||||
searchId: number;
|
||||
callback: (serialNumbers: string[]) => void;
|
||||
};
|
||||
|
||||
48
src/hooks/Network/Radius.ts
Normal file
48
src/hooks/Network/Radius.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { QueryFunctionContext, useQuery } from '@tanstack/react-query';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
|
||||
export type RadiusSession = {
|
||||
started: number;
|
||||
lastTransaction: number;
|
||||
inputPackets: number;
|
||||
outputPackets: number;
|
||||
inputOctets: number;
|
||||
outputOctets: number;
|
||||
inputGigaWords: number;
|
||||
outputGigaWords: number;
|
||||
sessionTime: number;
|
||||
serialNumber: string;
|
||||
destination: string;
|
||||
userName: string;
|
||||
accountingSessionId: string;
|
||||
accountingMultiSessionId: string;
|
||||
callingStationId: string;
|
||||
};
|
||||
|
||||
export const getDeviceRadiusSessions = async (mac: string) =>
|
||||
axiosGw.get(`/radiusSessions/${mac}`).then((res) => res.data.sessions as RadiusSession[]);
|
||||
const getDeviceSessions = async (context: QueryFunctionContext<[string, string]>) =>
|
||||
getDeviceRadiusSessions(context.queryKey[1]);
|
||||
export const useGetDeviceRadiusSessions = ({ serialNumber }: { serialNumber: string }) =>
|
||||
useQuery(['radius-sessions', serialNumber], getDeviceSessions, {
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
|
||||
export const getUsernameRadiusSessions = async (username: string) =>
|
||||
axiosGw.get(`/radiusSessions/0?userName=${username}`).then((res) => res.data.sessions as RadiusSession[]);
|
||||
const getUserSessions = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||
getUsernameRadiusSessions(context.queryKey[2]);
|
||||
export const useGetUserRadiusSessions = ({ userName }: { userName: string }) =>
|
||||
useQuery(['radius-sessions', 'username', userName], getUserSessions, {
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
|
||||
export const getStationRadiusSessions = async (station: string) =>
|
||||
axiosGw.get(`/radiusSessions/0?mac=${station}`).then((res) => res.data.sessions as RadiusSession[]);
|
||||
const getStationSessions = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||
getStationRadiusSessions(context.queryKey[2]);
|
||||
export const useGetStationRadiusSessions = ({ station }: { station: string }) =>
|
||||
useQuery(['radius-sessions', 'station', station], getStationSessions, {
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
@@ -11,6 +11,8 @@ export interface GatewayDevice {
|
||||
entity: string;
|
||||
firmware: string;
|
||||
fwUpdatePolicy: string;
|
||||
hasGPS: boolean;
|
||||
hasRADIUSSessions: number;
|
||||
lastConfigurationChange: number;
|
||||
lastConfigurationDownload: number;
|
||||
lastFWUpdate: number;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Note } from './Note';
|
||||
|
||||
|
||||
export type UserRole =
|
||||
| 'root'
|
||||
| 'admin'
|
||||
|
||||
132
src/pages/Device/RadiusClients/Table.tsx
Normal file
132
src/pages/Device/RadiusClients/Table.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
/* eslint-disable react/no-unstable-nested-components */
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
import DataCell from 'components/TableCells/DataCell';
|
||||
import DurationCell from 'components/TableCells/DurationCell';
|
||||
import { RadiusSession } from 'hooks/Network/Radius';
|
||||
|
||||
type Props = {
|
||||
sessions: RadiusSession[];
|
||||
refetch: () => void;
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'gateway.device_radius_sessions.table',
|
||||
defaultSortBy: [
|
||||
{
|
||||
id: 'callingStationId',
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const columns: DataGridColumn<RadiusSession>[] = React.useMemo(
|
||||
(): DataGridColumn<RadiusSession>[] => [
|
||||
{
|
||||
id: 'callingStationId',
|
||||
header: t('controller.radius.calling_station_id'),
|
||||
accessorKey: 'callingStationId',
|
||||
cell: ({ cell }) => {
|
||||
let { callingStationId } = cell.row.original;
|
||||
callingStationId = callingStationId.replace(/-/g, ':').toLowerCase();
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1">{callingStationId}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
customWidth: '150px',
|
||||
isMonospace: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'userName',
|
||||
header: t('controller.radius.username'),
|
||||
accessorKey: 'userName',
|
||||
meta: {
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sessionTime',
|
||||
header: t('controller.radius.session_time'),
|
||||
accessorKey: 'sessionTime',
|
||||
cell: ({ cell }) => {
|
||||
const { sessionTime } = cell.row.original;
|
||||
return <DurationCell seconds={sessionTime} />;
|
||||
},
|
||||
meta: {
|
||||
customWidth: '120px',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inputOctets',
|
||||
header: t('controller.radius.input_octets'),
|
||||
accessorKey: 'inputOctets',
|
||||
cell: ({ cell }) => {
|
||||
const { inputOctets } = cell.row.original;
|
||||
return (
|
||||
<Box textAlign="right">
|
||||
<DataCell bytes={inputOctets} showZerosAs="-" />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'outputOctets',
|
||||
header: t('controller.radius.output_octets'),
|
||||
accessorKey: 'outputOctets',
|
||||
cell: ({ cell }) => {
|
||||
const { outputOctets } = cell.row.original;
|
||||
return (
|
||||
<Box textAlign="right">
|
||||
<DataCell bytes={outputOctets} showZerosAs="-" />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
customWidth: '40px',
|
||||
customMinWidth: '40px',
|
||||
headerStyleProps: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<DataGrid<RadiusSession>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: `${t('controller.radius.radius_clients')} (${sessions.length})`,
|
||||
objectListed: t('controller.radius.radius_clients'),
|
||||
}}
|
||||
columns={columns}
|
||||
data={sessions}
|
||||
isLoading={isFetching}
|
||||
options={{
|
||||
refetch,
|
||||
minimumHeight: '200px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceRadiusClientsTable;
|
||||
32
src/pages/Device/RadiusClients/index.tsx
Normal file
32
src/pages/Device/RadiusClients/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import DeviceRadiusClientsTable from './Table';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { useGetDeviceRadiusSessions } from 'hooks/Network/Radius';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
};
|
||||
|
||||
const RadiusClientsCard = ({ serialNumber }: Props) => {
|
||||
const getRadiusClients = useGetDeviceRadiusSessions({ serialNumber });
|
||||
|
||||
if (!getRadiusClients.data || getRadiusClients.data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card mb={4}>
|
||||
<CardBody>
|
||||
<Box w="100%">
|
||||
<DeviceRadiusClientsTable
|
||||
sessions={getRadiusClients.data}
|
||||
refetch={getRadiusClients.refetch}
|
||||
isFetching={getRadiusClients.isFetching}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadiusClientsCard;
|
||||
@@ -29,6 +29,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import DeviceDetails from './Details';
|
||||
import DeviceLogsCard from './LogsCard';
|
||||
import DeviceNotes from './Notes';
|
||||
import RadiusClientsCard from './RadiusClients';
|
||||
import RestrictionsCard from './RestrictionsCard';
|
||||
import DeviceStatisticsCard from './StatisticsCard';
|
||||
import DeviceSummary from './Summary';
|
||||
@@ -38,7 +39,7 @@ import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import DeviceSearchBar from 'components/DeviceSearchBar';
|
||||
import GlobalSearchBar from 'components/GlobalSearchBar';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { ConfigureModal } from 'components/Modals/ConfigureModal';
|
||||
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
||||
@@ -194,7 +195,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
</HStack>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<DeviceSearchBar />
|
||||
<GlobalSearchBar />
|
||||
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||
{getDevice?.data && (
|
||||
<DeviceActionDropdown
|
||||
@@ -244,7 +245,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
</HStack>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<DeviceSearchBar />
|
||||
<GlobalSearchBar />
|
||||
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||
{getDevice?.data && (
|
||||
<DeviceActionDropdown
|
||||
@@ -319,6 +320,9 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
<DeviceStatisticsCard serialNumber={serialNumber} />
|
||||
<WifiAnalysisCard serialNumber={serialNumber} />
|
||||
<DeviceLogsCard serialNumber={serialNumber} />
|
||||
{getDevice.data && getDevice.data?.hasRADIUSSessions > 0 ? (
|
||||
<RadiusClientsCard serialNumber={serialNumber} />
|
||||
) : null}
|
||||
<RestrictionsCard serialNumber={serialNumber} />
|
||||
<Box />
|
||||
<DeviceNotes serialNumber={serialNumber} />
|
||||
|
||||
@@ -24,7 +24,7 @@ import DeviceUptimeCell from './Uptime';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
import DeviceSearchBar from 'components/DeviceSearchBar';
|
||||
import GlobalSearchBar from 'components/GlobalSearchBar';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { ConfigureModal } from 'components/Modals/ConfigureModal';
|
||||
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
||||
@@ -644,7 +644,7 @@ const DeviceListCard = () => {
|
||||
header={{
|
||||
title: `${getCount.data?.count} ${t('devices.title')}`,
|
||||
objectListed: t('devices.title'),
|
||||
leftContent: <DeviceSearchBar />,
|
||||
leftContent: <GlobalSearchBar />,
|
||||
}}
|
||||
columns={columns}
|
||||
data={data}
|
||||
|
||||
Reference in New Issue
Block a user