[WIFI-12506] Added radius search and radius clients tile

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-04-12 10:43:35 +02:00
parent df1686a2ae
commit f70992e9a1
19 changed files with 6291 additions and 5824 deletions

View File

@@ -3,3 +3,4 @@ build
dist
node_modules
.github
/helm

72
package-lock.json generated
View File

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

View File

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

View File

@@ -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;

View 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;

View File

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

View File

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

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

View File

@@ -11,6 +11,8 @@ export interface GatewayDevice {
entity: string;
firmware: string;
fwUpdatePolicy: string;
hasGPS: boolean;
hasRADIUSSessions: number;
lastConfigurationChange: number;
lastConfigurationDownload: number;
lastFWUpdate: number;

View File

@@ -1,6 +1,5 @@
import { Note } from './Note';
export type UserRole =
| 'root'
| 'admin'

View 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;

View 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;

View File

@@ -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} />

View File

@@ -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}