mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 09:22:21 +00:00
426 lines
14 KiB
TypeScript
426 lines
14 KiB
TypeScript
import * as React from 'react';
|
|
import { Box, Button, Heading, Image, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
|
import { LockSimple } from 'phosphor-react';
|
|
import ReactCountryFlag from 'react-country-flag';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import Actions from './Actions';
|
|
import DeviceSearchBar from './DeviceSearchBar';
|
|
import DeviceListFirmwareButton from './FirmwareButton';
|
|
import AP from './icons/AP.png';
|
|
import IOT from './icons/IOT.png';
|
|
import MESH from './icons/MESH.png';
|
|
import SWITCH from './icons/SWITCH.png';
|
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
|
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
|
|
import { DataTable } from 'components/DataTables/DataTable';
|
|
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
|
import { ConfigureModal } from 'components/Modals/ConfigureModal';
|
|
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
|
import FactoryResetModal from 'components/Modals/FactoryResetModal';
|
|
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
|
|
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
|
import { TelemetryModal } from 'components/Modals/TelemetryModal';
|
|
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 { FirmwareAgeResponse, useGetFirmwareAges } from 'hooks/Network/Firmware';
|
|
import { Column, PageInfo } from 'models/Table';
|
|
|
|
const ICON_STYLE = { width: '24px', height: '24px', borderRadius: '20px' };
|
|
|
|
const ICONS = {
|
|
AP: <Image borderRadius="full" boxSize="25px" src={AP} left="auto" right="auto" />,
|
|
SWITCH: <Image borderRadius="full" boxSize="25px" src={SWITCH} left="auto" right="auto" />,
|
|
IOT: <Image borderRadius="full" boxSize="25px" src={IOT} left="auto" right="auto" />,
|
|
MESH: <Image borderRadius="full" boxSize="25px" src={MESH} left="auto" right="auto" />,
|
|
};
|
|
|
|
const BADGE_COLORS: Record<string, string> = {
|
|
VALID_CERTIFICATE: 'red',
|
|
NO_CERTIFICATE: 'red',
|
|
MISMATCH_SERIAL: 'yellow',
|
|
VERIFIED: 'green',
|
|
};
|
|
|
|
const DeviceListCard = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const [serialNumber, setSerialNumber] = React.useState<string>('');
|
|
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
|
const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined);
|
|
const scanModalProps = useDisclosure();
|
|
const resetModalProps = useDisclosure();
|
|
const upgradeModalProps = useDisclosure();
|
|
const traceModalProps = useDisclosure();
|
|
const eventQueueProps = useDisclosure();
|
|
const telemetryModalProps = useDisclosure();
|
|
const configureModalProps = useDisclosure();
|
|
const scriptModal = useScriptModal();
|
|
const getCount = useGetDeviceCount({ enabled: true });
|
|
const getDevices = useGetDevices({
|
|
pageInfo,
|
|
enabled: true,
|
|
});
|
|
const getAges = useGetFirmwareAges({
|
|
serialNumbers: getDevices.data?.devicesWithStatus.map((device) => device.serialNumber),
|
|
});
|
|
|
|
const onOpenScan = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
scanModalProps.onOpen();
|
|
};
|
|
const onOpenFactoryReset = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
resetModalProps.onOpen();
|
|
};
|
|
const onOpenUpgradeModal = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
upgradeModalProps.onOpen();
|
|
};
|
|
const onOpenTrace = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
traceModalProps.onOpen();
|
|
};
|
|
const onOpenEventQueue = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
eventQueueProps.onOpen();
|
|
};
|
|
const onOpenTelemetry = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
telemetryModalProps.onOpen();
|
|
};
|
|
const onOpenConfigure = (serial: string) => {
|
|
setSerialNumber(serial);
|
|
configureModalProps.onOpen();
|
|
};
|
|
|
|
const goToSerial = (serial: string) => () => {
|
|
navigate(`/devices/${serial}`);
|
|
};
|
|
|
|
const badgeCell = React.useCallback(
|
|
(device: DeviceWithStatus) => (
|
|
<Box
|
|
h="35px"
|
|
w="35px"
|
|
borderRadius="50em"
|
|
bgColor={BADGE_COLORS[device.verifiedCertificate] ?? 'red'}
|
|
alignItems="center"
|
|
display="inline-flex"
|
|
justifyContent="center"
|
|
position="relative"
|
|
>
|
|
<Tooltip
|
|
label={`${device.verifiedCertificate} - ${
|
|
device.connected ? t('common.connected') : t('common.disconnected')
|
|
} ${device.restrictedDevice ? `- ${t('devices.restricted')}` : ''}`}
|
|
>
|
|
{ICONS[device.deviceType] ?? ICONS.AP}
|
|
</Tooltip>
|
|
<Box
|
|
w="0.65em"
|
|
h="0.65em"
|
|
borderRadius="full"
|
|
position="absolute"
|
|
bg={device.connected ? 'green' : 'red'}
|
|
right={0.5}
|
|
bottom={0}
|
|
borderColor="gray.200"
|
|
borderWidth={1}
|
|
/>
|
|
{device.restrictedDevice && (
|
|
<Box
|
|
w="0.95em"
|
|
h="0.95em"
|
|
borderRadius="full"
|
|
position="absolute"
|
|
bg="blue.100"
|
|
left={-1}
|
|
top={0}
|
|
borderColor="gray.200"
|
|
borderWidth={1}
|
|
>
|
|
<LockSimple
|
|
size={12}
|
|
style={{
|
|
color: 'black',
|
|
}}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
),
|
|
[],
|
|
);
|
|
|
|
const serialCell = React.useCallback(
|
|
(device: DeviceWithStatus) => (
|
|
<Button variant="link" onClick={goToSerial(device.serialNumber)} fontSize="sm">
|
|
<pre>{device.serialNumber}</pre>
|
|
</Button>
|
|
),
|
|
[],
|
|
);
|
|
const dataCell = React.useCallback(
|
|
(v: number) => (
|
|
<Box textAlign="right">
|
|
<DataCell bytes={v} />
|
|
</Box>
|
|
),
|
|
[],
|
|
);
|
|
const dateCell = React.useCallback(
|
|
(v?: number | string, hidePrefix?: boolean) =>
|
|
v !== undefined && typeof v === 'number' && v !== 0 ? (
|
|
<FormattedDate date={v as number} hidePrefix={hidePrefix} />
|
|
) : (
|
|
'-'
|
|
),
|
|
[],
|
|
);
|
|
const firmwareCell = React.useCallback(
|
|
(device: DeviceWithStatus & { age?: FirmwareAgeResponse }) => (
|
|
<DeviceListFirmwareButton device={device} age={device.age} onOpenUpgrade={onOpenUpgradeModal} />
|
|
),
|
|
[getAges],
|
|
);
|
|
const localeCell = React.useCallback(
|
|
(device: DeviceWithStatus) => (
|
|
<Tooltip label={`${device.locale !== '' ? `${device.locale} - ` : ''}${device.ipAddress}`} placement="top">
|
|
<Box w="100%" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
|
{device.locale !== '' && device.ipAddress !== '' && (
|
|
<ReactCountryFlag style={ICON_STYLE} countryCode={device.locale} svg />
|
|
)}
|
|
{` ${device.ipAddress}`}
|
|
</Box>
|
|
</Tooltip>
|
|
),
|
|
[],
|
|
);
|
|
const numberCell = React.useCallback((v?: number) => <NumberCell value={v !== undefined ? v : 0} />, []);
|
|
const actionCell = React.useCallback(
|
|
(device: DeviceWithStatus) => (
|
|
<Actions
|
|
device={device}
|
|
refreshTable={getDevices.refetch}
|
|
onOpenScan={onOpenScan}
|
|
onOpenFactoryReset={onOpenFactoryReset}
|
|
onOpenUpgradeModal={onOpenUpgradeModal}
|
|
onOpenTrace={onOpenTrace}
|
|
onOpenEventQueue={onOpenEventQueue}
|
|
onOpenConfigureModal={onOpenConfigure}
|
|
onOpenTelemetryModal={onOpenTelemetry}
|
|
onOpenScriptModal={scriptModal.openModal}
|
|
/>
|
|
),
|
|
[],
|
|
);
|
|
|
|
const columns: Column<DeviceWithStatus>[] = React.useMemo(
|
|
(): Column<DeviceWithStatus>[] => [
|
|
{
|
|
id: 'badge',
|
|
Header: '',
|
|
Footer: '',
|
|
accessor: 'badge',
|
|
Cell: (v) => badgeCell(v.cell.row.original),
|
|
customWidth: '35px',
|
|
alwaysShow: true,
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'serialNumber',
|
|
Header: t('inventory.serial_number'),
|
|
Footer: '',
|
|
accessor: 'serialNumber',
|
|
Cell: (v) => serialCell(v.cell.row.original),
|
|
alwaysShow: true,
|
|
customMaxWidth: '200px',
|
|
customWidth: '130px',
|
|
customMinWidth: '130px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'firmware',
|
|
Header: t('commands.revision'),
|
|
Footer: '',
|
|
accessor: 'firmware',
|
|
Cell: (v) => firmwareCell(v.cell.row.original),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'compatible',
|
|
Header: t('common.type'),
|
|
Footer: '',
|
|
accessor: 'compatible',
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'IP',
|
|
Header: 'IP',
|
|
Footer: '',
|
|
accessor: 'IP',
|
|
Cell: (v) => localeCell(v.cell.row.original),
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'lastContact',
|
|
Header: t('analytics.last_contact'),
|
|
Footer: '',
|
|
accessor: 'lastContact',
|
|
Cell: (v) => dateCell(v.cell.row.original.lastContact),
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'lastFWUpdate',
|
|
Header: t('controller.devices.last_upgrade'),
|
|
Footer: '',
|
|
accessor: 'lastFWUpdate',
|
|
Cell: (v) => dateCell(v.cell.row.original.lastFWUpdate),
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'rxBytes',
|
|
Header: 'Rx',
|
|
Footer: '',
|
|
accessor: 'rxBytes',
|
|
Cell: (v) => dataCell(v.cell.row.original.rxBytes),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'txBytes',
|
|
Header: 'Tx',
|
|
Footer: '',
|
|
accessor: 'txBytes',
|
|
Cell: (v) => dataCell(v.cell.row.original.txBytes),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: '2G',
|
|
Header: '2G',
|
|
Footer: '',
|
|
accessor: 'associations_2G',
|
|
Cell: (v) => numberCell(v.cell.row.original.associations_2G),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: '5G',
|
|
Header: '5G',
|
|
Footer: '',
|
|
accessor: 'associations_5G',
|
|
Cell: (v) => numberCell(v.cell.row.original.associations_5G),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: '6G',
|
|
Header: '6G',
|
|
Footer: '',
|
|
accessor: 'associations_6G',
|
|
Cell: (v) => numberCell(v.cell.row.original.associations_6G),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'certificateExpiryDate',
|
|
Header: t('devices.certificate_expiry'),
|
|
Footer: '',
|
|
accessor: 'certificateExpiryDate',
|
|
Cell: (v) => dateCell(v.cell.row.original.certificateExpiryDate, true),
|
|
customWidth: '50px',
|
|
disableSortBy: true,
|
|
},
|
|
{
|
|
id: 'actions',
|
|
Header: t('common.actions'),
|
|
Footer: '',
|
|
accessor: 'actions',
|
|
Cell: (v) => actionCell(v.cell.row.original),
|
|
customWidth: '50px',
|
|
alwaysShow: true,
|
|
disableSortBy: true,
|
|
},
|
|
],
|
|
[t, firmwareCell],
|
|
);
|
|
|
|
const data = React.useMemo(() => {
|
|
if (!getDevices.data) return [];
|
|
return getDevices.data.devicesWithStatus.map((device) => ({
|
|
...device,
|
|
age: getAges?.data?.ages.find(({ serialNumber: devSerial }) => devSerial === device.serialNumber),
|
|
}));
|
|
}, [getAges, getDevices]);
|
|
|
|
return (
|
|
<>
|
|
<CardHeader px={4} pt={4}>
|
|
<Heading size="md" my="auto" mr={2}>
|
|
{getCount.data?.count} {t('devices.title')}
|
|
</Heading>
|
|
<DeviceSearchBar />
|
|
<Spacer />
|
|
<ColumnPicker
|
|
columns={columns as Column<unknown>[]}
|
|
hiddenColumns={hiddenColumns}
|
|
setHiddenColumns={setHiddenColumns}
|
|
preference="gateway.devices.table.hiddenColumns"
|
|
/>
|
|
<RefreshButton
|
|
onClick={() => {
|
|
getDevices.refetch();
|
|
getCount.refetch();
|
|
}}
|
|
isCompact
|
|
ml={2}
|
|
isFetching={getCount.isFetching || getDevices.isFetching}
|
|
/>
|
|
</CardHeader>
|
|
<CardBody p={4}>
|
|
<Box overflowX="auto" w="100%">
|
|
<DataTable
|
|
columns={
|
|
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
|
|
id: string;
|
|
Header: string;
|
|
Footer: string;
|
|
accessor: string;
|
|
}[]
|
|
}
|
|
data={data ?? []}
|
|
isLoading={getCount.isFetching || getDevices.isFetching}
|
|
isManual
|
|
hiddenColumns={hiddenColumns}
|
|
obj={t('devices.title')}
|
|
count={getCount.data?.count || 0}
|
|
// @ts-ignore
|
|
setPageInfo={setPageInfo}
|
|
saveSettingsId="gateway.devices.table"
|
|
minHeight="600px"
|
|
/>
|
|
</Box>
|
|
</CardBody>
|
|
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
|
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
|
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
|
<TraceModal modalProps={traceModalProps} serialNumber={serialNumber} />
|
|
<EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} />
|
|
<ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} />
|
|
<TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} />
|
|
{scriptModal.modal}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default React.memo(DeviceListCard);
|