mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 01:12:19 +00:00
[WIFI-13136] Display WiFi scan new station count and channel utilization values
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.11.0(13)",
|
||||
"version": "2.11.0(15)",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "2.11.0(13)",
|
||||
"version": "2.11.0(15)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.11.0(13)",
|
||||
"version": "2.11.0(15)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
|
||||
8
src/@tanstack.react-table.d.ts
vendored
8
src/@tanstack.react-table.d.ts
vendored
@@ -4,18 +4,22 @@ import '@tanstack/react-table';
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
ref?: React.MutableRefObject<HTMLTableCellElement | null>;
|
||||
customMinWidth?: string;
|
||||
anchored?: boolean;
|
||||
stopPropagation?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
anchored?: boolean;
|
||||
hasPopover?: boolean;
|
||||
customMaxWidth?: string;
|
||||
customMinWidth?: string;
|
||||
customWidth?: string;
|
||||
isMonospace?: boolean;
|
||||
isCentered?: boolean;
|
||||
columnSelectorOptions?: {
|
||||
label?: string;
|
||||
};
|
||||
rowContentOptions?: {
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
headerOptions?: {
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
@@ -24,7 +24,6 @@ export const DataGridCellRow = <TValue extends object>({
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
borderRight="1px solid gray"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Td
|
||||
@@ -55,6 +54,7 @@ export const DataGridCellRow = <TValue extends object>({
|
||||
: undefined
|
||||
}
|
||||
border="0.5px solid gray"
|
||||
style={cell.column.columnDef.meta?.rowContentOptions?.style}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
|
||||
@@ -8,7 +8,7 @@ export type DataGridHeaderRowProps<TValue extends object> = {
|
||||
};
|
||||
|
||||
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
|
||||
<Tr p={0} borderRight="1px solid gray">
|
||||
<Tr p={0}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Th
|
||||
color="gray.400"
|
||||
|
||||
@@ -40,13 +40,16 @@ export type DataGridOptions<TValue extends object> = {
|
||||
onRowClick?: (row: TValue) => (() => void) | undefined;
|
||||
refetch?: () => void;
|
||||
showAsCard?: boolean;
|
||||
hideTablePreferences?: boolean;
|
||||
hideTableTitleRow?: boolean;
|
||||
};
|
||||
|
||||
export type DataGridProps<TValue extends object> = {
|
||||
innerTableKey?: string | number;
|
||||
controller: UseDataGridReturn;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
header: {
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
objectListed: string;
|
||||
leftContent?: React.ReactNode;
|
||||
addButton?: React.ReactNode;
|
||||
@@ -58,6 +61,7 @@ export type DataGridProps<TValue extends object> = {
|
||||
};
|
||||
|
||||
export const DataGrid = <TValue extends object>({
|
||||
innerTableKey,
|
||||
controller,
|
||||
columns,
|
||||
header,
|
||||
@@ -149,6 +153,20 @@ export const DataGrid = <TValue extends object>({
|
||||
...tableOptions,
|
||||
});
|
||||
|
||||
// If this is a manual DataTable, with a page index that is higher than 0 and higher than the max possible page, we send to index 0
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
options.isManual &&
|
||||
!isLoading &&
|
||||
data &&
|
||||
pagination.pageIndex > 0 &&
|
||||
options.count !== undefined &&
|
||||
Math.ceil(options.count / pagination.pageSize) - 1 < pagination.pageIndex
|
||||
) {
|
||||
controller.onPaginationChange({ pageIndex: 0, pageSize: pagination.pageSize });
|
||||
}
|
||||
}, [options.count, isLoading, pagination, data]);
|
||||
|
||||
if (isLoading && !options.showAsCard && data.length === 0) {
|
||||
return (
|
||||
<Center>
|
||||
@@ -160,25 +178,29 @@ export const DataGrid = <TValue extends object>({
|
||||
return options.showAsCard ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
{typeof header.title === 'string' ? (
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
) : (
|
||||
header.title
|
||||
)}
|
||||
{header.leftContent}
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
{options.hideTablePreferences ? null : (
|
||||
// @ts-ignore
|
||||
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||
}
|
||||
)}
|
||||
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody display="flex" flexDirection="column">
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
@@ -206,7 +228,7 @@ export const DataGrid = <TValue extends object>({
|
||||
</Card>
|
||||
) : (
|
||||
<Box w="100%">
|
||||
<Flex mb={2}>
|
||||
<Flex mb={2} hidden={options.hideTableTitleRow}>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
@@ -215,16 +237,16 @@ export const DataGrid = <TValue extends object>({
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
{options.hideTablePreferences ? null : (
|
||||
// @ts-ignore
|
||||
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||
}
|
||||
)}
|
||||
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||
</HStack>
|
||||
</Flex>
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
|
||||
@@ -104,18 +104,43 @@ const GlobalSearchBar = () => {
|
||||
.then(() => callback([]));
|
||||
}
|
||||
if (v.match('^[a-fA-F0-9-*]+$')) {
|
||||
let result: { label: string; value: string; type: 'serial' }[] = [];
|
||||
let tryAgain = true;
|
||||
|
||||
await store
|
||||
.searchSerialNumber(v)
|
||||
.then((res) => {
|
||||
callback(
|
||||
res.map((r) => ({
|
||||
result = res.map((r) => ({
|
||||
label: r,
|
||||
value: r,
|
||||
type: 'serial',
|
||||
}));
|
||||
tryAgain = false;
|
||||
})
|
||||
.catch(() => {
|
||||
result = [];
|
||||
});
|
||||
|
||||
if (tryAgain) {
|
||||
// Wait 1 second and try again
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
await store
|
||||
.searchSerialNumber(v)
|
||||
.then((res) => {
|
||||
result = res.map((r) => ({
|
||||
label: r,
|
||||
value: r,
|
||||
type: 'serial',
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => []);
|
||||
}));
|
||||
tryAgain = false;
|
||||
})
|
||||
.catch(() => {
|
||||
result = [];
|
||||
});
|
||||
}
|
||||
|
||||
callback(result);
|
||||
}
|
||||
return callback([]);
|
||||
},
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Heading,
|
||||
IconButton,
|
||||
Spacer,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Button, Center, Heading, IconButton, Spacer, useColorMode } from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,21 +7,124 @@ import { v4 as uuid } from 'uuid';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { ScanChannel } from 'models/Device';
|
||||
import { DeviceScanResult, ScanChannel } from 'models/Device';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
|
||||
interface Props {
|
||||
channelInfo: ScanChannel;
|
||||
}
|
||||
const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
|
||||
|
||||
const ueCell = (ies: DeviceScanResult['ies'], setIes: (ies: DeviceScanResult['ies']) => void) => (
|
||||
<Button size="sm" colorScheme="blue" onClick={() => setIes(ies)} w="100%">
|
||||
{ies.length}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const centerIfUndefinedCell = (v?: string | number, suffix?: string) =>
|
||||
v !== undefined ? `${v}${suffix ? `${suffix}` : ''}` : <Center>-</Center>;
|
||||
|
||||
const ResultCard = ({ channelInfo: { channel, devices } }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const [ies, setIes] = React.useState<{ content: unknown; name: string; type: number }[] | undefined>();
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'wifiscan.devices.table',
|
||||
defaultOrder: ['ssid', 'signal', 'actions'],
|
||||
defaultSortBy: [
|
||||
{
|
||||
desc: false,
|
||||
id: 'ssid',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const columns: DataGridColumn<DeviceScanResult>[] = React.useMemo(
|
||||
(): DataGridColumn<DeviceScanResult>[] => [
|
||||
{
|
||||
id: 'ssid',
|
||||
header: 'SSID',
|
||||
footer: '',
|
||||
accessorKey: 'ssid',
|
||||
meta: {
|
||||
anchored: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'signal',
|
||||
header: 'Signal',
|
||||
footer: '',
|
||||
accessorKey: 'signal',
|
||||
cell: (v) => `${v.cell.row.original.signal} db`,
|
||||
meta: {
|
||||
anchored: true,
|
||||
customWidth: '80px',
|
||||
alwaysShow: true,
|
||||
rowContentOptions: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'station',
|
||||
header: 'UEs',
|
||||
accessorKey: 'sta_count',
|
||||
cell: (v) => centerIfUndefinedCell(v.cell.row.original.sta_count),
|
||||
meta: {
|
||||
anchored: true,
|
||||
customWidth: '40px',
|
||||
alwaysShow: true,
|
||||
rowContentOptions: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'utilization',
|
||||
header: 'Ch. Util.',
|
||||
accessorKey: 'ch_util',
|
||||
cell: (v) => centerIfUndefinedCell(v.cell.row.original.ch_util, '%'),
|
||||
meta: {
|
||||
anchored: true,
|
||||
customWidth: '60px',
|
||||
alwaysShow: true,
|
||||
headerOptions: {
|
||||
tooltip: 'Channel Utilization (%)',
|
||||
},
|
||||
rowContentOptions: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ies',
|
||||
header: 'Ies',
|
||||
footer: '',
|
||||
accessorKey: 'actions',
|
||||
cell: (v) => ueCell(v.cell.row.original.ies ?? [], setIes),
|
||||
meta: {
|
||||
customWidth: '50px',
|
||||
isCentered: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card variant="widget">
|
||||
<Card>
|
||||
<CardHeader display="flex">
|
||||
<Heading size="md" my="auto">
|
||||
{t('commands.channel')} #{channel} ({devices.length} {t('devices.title')})
|
||||
{t('commands.channel')} #{channel} ({devices.length}{' '}
|
||||
{devices.length === 1 ? t('devices.one') : t('devices.title')})
|
||||
</Heading>
|
||||
<Spacer />
|
||||
{ies && (
|
||||
@@ -49,52 +139,43 @@ const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="400px" w="100%" overflowY="auto" overflowX="auto" px={0}>
|
||||
{ies ? (
|
||||
<Box w="800px">
|
||||
{ies.map(({ content, name, type }) => (
|
||||
<Box key={uuid()} my={2}>
|
||||
<Heading size="sm" mb={2} textDecor="underline">
|
||||
{name} ({type})
|
||||
</Heading>
|
||||
<JsonViewer
|
||||
rootName={false}
|
||||
displayDataTypes={false}
|
||||
enableClipboard
|
||||
theme={colorMode === 'light' ? undefined : 'dark'}
|
||||
value={content as object}
|
||||
style={{ background: 'unset', display: 'unset' }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Table variant="simple" px={0}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>SSID</Th>
|
||||
<Th width="110px" isNumeric>
|
||||
{t('commands.signal')}
|
||||
</Th>
|
||||
<Th w="10px">IEs</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{devices.map((dev) => (
|
||||
<Tr key={uuid()}>
|
||||
<Td>{dev.ssid}</Td>
|
||||
<Td width="110px">{dev.signal} db</Td>
|
||||
<Td w="10px">
|
||||
<Button size="sm" colorScheme="blue" onClick={() => setIes(dev.ies ?? [])}>
|
||||
{dev.ies?.length ?? 0}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Box>
|
||||
{ies ? (
|
||||
<Box w="800px">
|
||||
{ies.map(({ content, name, type }) => (
|
||||
<Box key={uuid()} my={2}>
|
||||
<Heading size="sm" mb={2} textDecor="underline">
|
||||
{name} ({type})
|
||||
</Heading>
|
||||
<JsonViewer
|
||||
rootName={false}
|
||||
displayDataTypes={false}
|
||||
enableClipboard
|
||||
theme={colorMode === 'light' ? undefined : 'dark'}
|
||||
value={content as object}
|
||||
style={{ background: 'unset', display: 'unset' }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<DataGrid<DeviceScanResult>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: '',
|
||||
objectListed: t('devices.title'),
|
||||
}}
|
||||
columns={columns}
|
||||
data={devices}
|
||||
options={{
|
||||
count: devices.length,
|
||||
onRowClick: (device) => () => setIes(device.ies ?? []),
|
||||
hideTablePreferences: true,
|
||||
isHidingControls: true,
|
||||
minimumHeight: '0px',
|
||||
hideTableTitleRow: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Alert, Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import { Alert, Heading, VStack } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ResultCard from './ResultCard';
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
setCsvData: (data: DeviceScanResult[]) => void;
|
||||
}
|
||||
|
||||
const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
|
||||
const WifiScanResultDisplay = ({ results, setCsvData }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const scanResults = useMemo(() => {
|
||||
@@ -54,18 +54,18 @@ const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
|
||||
return (
|
||||
<>
|
||||
{results.errorCode === 1 && (
|
||||
<Heading size="sm">
|
||||
<Heading size="md">
|
||||
<Alert colorScheme="red">{t('commands.wifiscan_error_1')}</Alert>
|
||||
</Heading>
|
||||
)}
|
||||
<Heading size="sm">
|
||||
<Heading size="md" mb={2}>
|
||||
{t('commands.execution_time')}: {Math.floor(results.executionTime / 1000)}s
|
||||
</Heading>
|
||||
<SimpleGrid minChildWidth="360px" spacing={2}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{scanResults?.scanList.map((channel) => (
|
||||
<ResultCard key={uuid()} channelInfo={channel} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -123,6 +123,7 @@ export const Navbar = ({
|
||||
top="15px"
|
||||
border={scrolled ? '0.5px solid' : undefined}
|
||||
w={isCompact ? '100%' : 'calc(100% - 254px)'}
|
||||
zIndex={1}
|
||||
>
|
||||
<Flex
|
||||
w="100%"
|
||||
|
||||
@@ -114,12 +114,16 @@ interface BssidResult {
|
||||
bssid: string;
|
||||
capability: number;
|
||||
channel: number;
|
||||
/** Channel Utilization percentage (ex.: 28 -> 28% channel utilization) */
|
||||
ch_util?: number;
|
||||
frequency: number;
|
||||
ht_oper: string;
|
||||
ies: { content: unknown; name: string; type: number }[];
|
||||
last_seen: number;
|
||||
ssid: string;
|
||||
signal: number;
|
||||
/** Station count */
|
||||
sta_count?: number;
|
||||
tsf: number;
|
||||
meshid?: string;
|
||||
vht_oper: string;
|
||||
@@ -144,20 +148,8 @@ export interface WifiScanResult {
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeviceScanResult {
|
||||
bssid: string;
|
||||
capability: number;
|
||||
channel: number;
|
||||
frequency: number;
|
||||
ht_oper: string;
|
||||
ies: { content: unknown; name: string; type: number }[];
|
||||
last_seen: number;
|
||||
ssid: string;
|
||||
signal: number | string;
|
||||
tsf: number;
|
||||
meshid?: string;
|
||||
vht_oper: string;
|
||||
}
|
||||
export type DeviceScanResult = BssidResult;
|
||||
|
||||
export interface ScanChannel {
|
||||
channel: number;
|
||||
devices: DeviceScanResult[];
|
||||
|
||||
@@ -162,7 +162,7 @@ const WifiAnalysisCard = ({ serialNumber }: Props) => {
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
<SliderThumb zIndex={0} />
|
||||
</Slider>
|
||||
)}
|
||||
<Box />
|
||||
|
||||
Reference in New Issue
Block a user