mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
synced 2025-12-25 23:07:35 +00:00
Compare commits
30 Commits
v3.2.0
...
release/v4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ee5537381 | ||
|
|
06b9bc227d | ||
|
|
624b97d685 | ||
|
|
d07e83d000 | ||
|
|
0a671bbdab | ||
|
|
9ec7a25766 | ||
|
|
757404b682 | ||
|
|
c4aff418ed | ||
|
|
dd5c894b03 | ||
|
|
c3256b93c7 | ||
|
|
932f1f4a12 | ||
|
|
db3cbb0b35 | ||
|
|
c895274ebf | ||
|
|
a3647bca08 | ||
|
|
5fbf421d77 | ||
|
|
e09b3ee5f4 | ||
|
|
855960559d | ||
|
|
4cecfc6fc4 | ||
|
|
e62d1e4a98 | ||
|
|
6dddba0848 | ||
|
|
30fffdfe52 | ||
|
|
c8d6540ca6 | ||
|
|
2b2f08c231 | ||
|
|
0cfed90a7b | ||
|
|
01008dc1aa | ||
|
|
26b90cfdba | ||
|
|
b218051104 | ||
|
|
a2fa93938f | ||
|
|
c220d11dd0 | ||
|
|
40d533ecc5 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -20,7 +20,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_REGISTRY_URL: tip-tip-wlan-cloud-ucentral.jfrog.io
|
||||
DOCKER_REGISTRY_USERNAME: ucentral
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -11,7 +11,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
helm-package:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HELM_REPO_URL: https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral-helm/
|
||||
HELM_REPO_USERNAME: ucentral
|
||||
|
||||
@@ -8,7 +8,7 @@ fullnameOverride: ""
|
||||
images:
|
||||
owgwui:
|
||||
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
||||
tag: main
|
||||
tag: v4.2.0
|
||||
pullPolicy: Always
|
||||
|
||||
services:
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "3.1.0(5)",
|
||||
"version": "4.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ucentral-client",
|
||||
"version": "3.1.0(5)",
|
||||
"version": "4.1.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "3.2.0",
|
||||
"version": "4.1.0",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,8 @@ interface Props {
|
||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||
onOpenRebootModal: (serialNumber: string) => void;
|
||||
onOpenReEnrollModal?: (serialNumber: string) => void;
|
||||
onOpenExportModal?: (serialNumber: string) => void;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isCompact?: boolean;
|
||||
}
|
||||
@@ -49,6 +51,8 @@ const DeviceActionDropdown = ({
|
||||
onOpenConfigureModal,
|
||||
onOpenScriptModal,
|
||||
onOpenRebootModal,
|
||||
onOpenReEnrollModal,
|
||||
onOpenExportModal,
|
||||
size,
|
||||
isCompact,
|
||||
}: Props) => {
|
||||
@@ -234,6 +238,11 @@ const DeviceActionDropdown = ({
|
||||
<MenuItem onClick={handleRebootClick} hidden={!isCompact}>
|
||||
{t('commands.reboot')}
|
||||
</MenuItem>
|
||||
{onOpenReEnrollModal && (
|
||||
<MenuItem onClick={() => onOpenReEnrollModal(device.serialNumber)}>
|
||||
{t('controller.devices.re_enroll')}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
||||
@@ -243,6 +252,11 @@ const DeviceActionDropdown = ({
|
||||
<MenuItem onClick={handleOpenScan} hidden={!isCompact || deviceType !== 'ap'}>
|
||||
{t('commands.wifiscan')}
|
||||
</MenuItem>
|
||||
{onOpenExportModal && (
|
||||
<MenuItem onClick={() => onOpenExportModal(device.serialNumber)}>
|
||||
{t('export.title')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
|
||||
257
src/components/Modals/CableDiagnosticsModal/index.tsx
Normal file
257
src/components/Modals/CableDiagnosticsModal/index.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Text,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
Center,
|
||||
Spinner,
|
||||
Checkbox,
|
||||
Stack,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
} from '@chakra-ui/react';
|
||||
import { PlugsConnected } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
||||
import { ResponsiveButton } from 'components/Buttons/ResponsiveButton';
|
||||
import { ModalHeader } from 'components/Containers/Modal/ModalHeader';
|
||||
import { useCableDiagnostics } from 'hooks/Network/Devices';
|
||||
import { ModalProps } from 'models/Modal';
|
||||
import Button from 'theme/components/button';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
|
||||
export type CableDiagnosticsModalProps = {
|
||||
modalProps: ModalProps;
|
||||
serialNumber: string;
|
||||
port: string;
|
||||
};
|
||||
|
||||
type DiagnosticsRow = {
|
||||
port: string;
|
||||
linkStatus: string;
|
||||
pairA: string;
|
||||
pairB: string;
|
||||
pairC: string;
|
||||
pairD: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type OpticalRow = {
|
||||
port: string;
|
||||
vendorName: string;
|
||||
formFactor: string;
|
||||
partNumber: string;
|
||||
serialNumber: string;
|
||||
temperature: string;
|
||||
txPower: string;
|
||||
rxPower: string;
|
||||
revision: string;
|
||||
};
|
||||
|
||||
export const CableDiagnosticsModal = ({
|
||||
modalProps: { isOpen, onClose },
|
||||
serialNumber,
|
||||
port,
|
||||
}: CableDiagnosticsModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPorts, setSelectedPorts] = React.useState<string[]>([]);
|
||||
const [diagnosticsResult, setDiagnosticsResult] = React.useState<any>(null);
|
||||
const { mutateAsync: diagnose, isLoading } = useCableDiagnostics({ serialNumber });
|
||||
|
||||
const handlePortToggle = (port: string) => {
|
||||
setSelectedPorts((prev) => (prev.includes(port) ? prev.filter((p) => p !== port) : [...prev, port]));
|
||||
};
|
||||
|
||||
const handleDiagnose = async () => {
|
||||
if (port) {
|
||||
try {
|
||||
const result = await diagnose([port]);
|
||||
setDiagnosticsResult(result);
|
||||
} catch (error) {
|
||||
console.error('Error diagnosing cable:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'cable.diagnostics.table',
|
||||
defaultOrder: ['port', 'linkStatus', 'pairA', 'pairB', 'pairC', 'pairD', 'type'],
|
||||
showAllRows: true,
|
||||
});
|
||||
|
||||
const columns: DataGridColumn<DiagnosticsRow | OpticalRow>[] = React.useMemo(() => {
|
||||
const data = diagnosticsResult?.results?.status?.text?.[port];
|
||||
const isOpticalData = data && 'form-factor' in data;
|
||||
|
||||
return isOpticalData
|
||||
? [
|
||||
{
|
||||
id: 'vendorName',
|
||||
header: 'Vendor Name',
|
||||
accessorKey: 'vendorName',
|
||||
},
|
||||
{
|
||||
id: 'formFactor',
|
||||
header: 'Form Factor',
|
||||
accessorKey: 'formFactor',
|
||||
},
|
||||
{
|
||||
id: 'partNumber',
|
||||
header: 'Part Number',
|
||||
accessorKey: 'partNumber',
|
||||
},
|
||||
{
|
||||
id: 'serialNumber',
|
||||
header: 'Serial Number',
|
||||
accessorKey: 'serialNumber',
|
||||
},
|
||||
{
|
||||
id: 'temperature',
|
||||
header: 'Temperature',
|
||||
accessorKey: 'temperature',
|
||||
},
|
||||
{
|
||||
id: 'txPower',
|
||||
header: 'TX Power',
|
||||
accessorKey: 'txPower',
|
||||
},
|
||||
{
|
||||
id: 'rxPower',
|
||||
header: 'RX Power',
|
||||
accessorKey: 'rxPower',
|
||||
},
|
||||
{
|
||||
id: 'revision',
|
||||
header: 'Revision',
|
||||
accessorKey: 'revision',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'port',
|
||||
header: 'Port',
|
||||
accessorKey: 'port',
|
||||
},
|
||||
{
|
||||
id: 'linkStatus',
|
||||
header: 'Link Status',
|
||||
accessorKey: 'linkStatus',
|
||||
},
|
||||
{
|
||||
id: 'pairA',
|
||||
header: 'Pair A',
|
||||
accessorKey: 'pairA',
|
||||
},
|
||||
{
|
||||
id: 'pairB',
|
||||
header: 'Pair B',
|
||||
accessorKey: 'pairB',
|
||||
},
|
||||
{
|
||||
id: 'pairC',
|
||||
header: 'Pair C',
|
||||
accessorKey: 'pairC',
|
||||
},
|
||||
{
|
||||
id: 'pairD',
|
||||
header: 'Pair D',
|
||||
accessorKey: 'pairD',
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
accessorKey: 'type',
|
||||
},
|
||||
];
|
||||
}, [diagnosticsResult]);
|
||||
|
||||
const formatDiagnosticsData = (result: any): (DiagnosticsRow | OpticalRow)[] => {
|
||||
if (!result?.results?.status?.text?.[port]) return [];
|
||||
|
||||
const data = result.results.status.text[port];
|
||||
|
||||
if (data['form-factor']) {
|
||||
return [
|
||||
{
|
||||
port,
|
||||
vendorName: data['vendor-name'] || 'N/A',
|
||||
formFactor: data['form-factor'] || 'N/A',
|
||||
partNumber: data['part-number'] || 'N/A',
|
||||
serialNumber: data['serial-number'] || 'N/A',
|
||||
temperature: data.temperature ? `${data.temperature.toFixed(2)}` : 'N/A',
|
||||
txPower: data['tx-optical-power'] ? `${data['tx-optical-power']}` : 'N/A',
|
||||
rxPower: data['rx-optical-power'] ? `${data['rx-optical-power']}` : 'N/A',
|
||||
revision: data.revision || 'N/A',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
port,
|
||||
linkStatus: data['link-status'],
|
||||
pairA: `${data['pair-A'].meters} (${data['pair-A'].status})`,
|
||||
pairB: `${data['pair-B'].meters} (${data['pair-B'].status})`,
|
||||
pairC: `${data['pair-C'].meters} (${data['pair-C'].status})`,
|
||||
pairD: `${data['pair-D'].meters} (${data['pair-D'].status})`,
|
||||
type: data.type,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW="50vw">
|
||||
<ModalHeader title={t('commands.cable_diagnostics')} right={<CloseButton onClick={onClose} />} />
|
||||
<ModalBody pb={6}>
|
||||
{isLoading ? (
|
||||
<Center my={4} flexDirection="column" gap={4}>
|
||||
<Spinner size="lg" />
|
||||
<Text>Please wait...</Text>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
Please do not close this window. This may take a few seconds.
|
||||
</Text>
|
||||
</Center>
|
||||
) : (
|
||||
<Center flexDirection="column" gap={4}>
|
||||
<ResponsiveButton
|
||||
color="blue"
|
||||
icon={<PlugsConnected size={20} />}
|
||||
label={`${
|
||||
diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 ? 'Retake' : 'Start'
|
||||
} Test for Port ${port}`}
|
||||
onClick={handleDiagnose}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!port}
|
||||
isCompact={false}
|
||||
/>
|
||||
{diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 && (
|
||||
<DataGrid<DiagnosticsRow | OpticalRow>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: '',
|
||||
objectListed: 'Cable Diagnostics',
|
||||
}}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
data={formatDiagnosticsData(diagnosticsResult)}
|
||||
options={{
|
||||
isHidingControls: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Center>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
381
src/components/Modals/ExportStatsModal/index.tsx
Normal file
381
src/components/Modals/ExportStatsModal/index.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Checkbox,
|
||||
FormLabel,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { DownloadSimple } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices';
|
||||
import { useGetDeviceLastStats, useGetDeviceNewestStats } from 'hooks/Network/Statistics';
|
||||
import { useGetTag } from 'hooks/Network/Inventory';
|
||||
import { useGetCommandHistory } from 'hooks/Network/Commands';
|
||||
import { useGetDeviceLogs } from 'hooks/Network/DeviceLogs';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
};
|
||||
|
||||
const ExportStatsModal = ({ serialNumber, modalProps }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [selectedOptions, setSelectedOptions] = React.useState<string[]>([
|
||||
'deviceInfo',
|
||||
'status',
|
||||
'statistics',
|
||||
]);
|
||||
const [isExporting, setIsExporting] = React.useState(false);
|
||||
|
||||
const getDevice = useGetDevice({ serialNumber });
|
||||
const getStatus = useGetDeviceStatus({ serialNumber });
|
||||
const getStats = useGetDeviceLastStats({ serialNumber });
|
||||
const getNewestStats = useGetDeviceNewestStats({ serialNumber, limit: 30 });
|
||||
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 50 });
|
||||
const getTag = useGetTag({ serialNumber });
|
||||
const getCommands = useGetCommandHistory({ serialNumber, limit: 100 });
|
||||
const getLogs = useGetDeviceLogs({ serialNumber, limit: 100, logType: 0 });
|
||||
const getCrashes = useGetDeviceLogs({ serialNumber, limit: 100, logType: 1 });
|
||||
const getReboots = useGetDeviceLogs({ serialNumber, limit: 100, logType: 2 });
|
||||
|
||||
const onToggle = (value: string) => (e: { target: { checked: boolean } }) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedOptions([...selectedOptions, value]);
|
||||
} else {
|
||||
setSelectedOptions(selectedOptions.filter((opt) => opt !== value));
|
||||
}
|
||||
};
|
||||
|
||||
const buildExportData = React.useCallback(() => {
|
||||
const exportData: Record<string, unknown> = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
serialNumber,
|
||||
};
|
||||
|
||||
if (selectedOptions.includes('deviceInfo') && getDevice.data) {
|
||||
exportData.deviceInfo = {
|
||||
serialNumber: getDevice.data.serialNumber,
|
||||
macAddress: getDevice.data.macAddress,
|
||||
manufacturer: getDevice.data.manufacturer,
|
||||
deviceType: getDevice.data.deviceType,
|
||||
compatible: getDevice.data.compatible,
|
||||
firmware: getDevice.data.firmware,
|
||||
locale: getDevice.data.locale,
|
||||
createdTimestamp: getDevice.data.createdTimestamp,
|
||||
modified: getDevice.data.modified,
|
||||
lastConfigurationChange: getDevice.data.lastConfigurationChange,
|
||||
lastConfigurationDownload: getDevice.data.lastConfigurationDownload,
|
||||
lastFWUpdate: getDevice.data.lastFWUpdate,
|
||||
lastRecordedContact: getDevice.data.lastRecordedContact,
|
||||
certificateExpiryDate: getDevice.data.certificateExpiryDate,
|
||||
fwUpdatePolicy: getDevice.data.fwUpdatePolicy,
|
||||
restrictedDevice: getDevice.data.restrictedDevice,
|
||||
restrictionDetails: getDevice.data.restrictionDetails,
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('configuration') && getDevice.data) {
|
||||
exportData.configuration = getDevice.data.configuration;
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('status') && getStatus.data) {
|
||||
exportData.status = {
|
||||
connected: getStatus.data.connected,
|
||||
connectReason: getStatus.data.connectReason,
|
||||
ipAddress: getStatus.data.ipAddress,
|
||||
firmware: getStatus.data.firmware,
|
||||
lastContact: getStatus.data.lastContact,
|
||||
certificateExpiryDate: getStatus.data.certificateExpiryDate,
|
||||
certificateIssuerName: getStatus.data.certificateIssuerName,
|
||||
started: getStatus.data.started,
|
||||
sessionId: getStatus.data.sessionId,
|
||||
totalConnectionTime: getStatus.data.totalConnectionTime,
|
||||
associations_2G: getStatus.data.associations_2G,
|
||||
associations_5G: getStatus.data.associations_5G,
|
||||
rxBytes: getStatus.data.rxBytes,
|
||||
txBytes: getStatus.data.txBytes,
|
||||
messageCount: getStatus.data.messageCount,
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('statistics')) {
|
||||
if (getStats.data) {
|
||||
exportData.lastStatistics = getStats.data;
|
||||
}
|
||||
if (getNewestStats.data?.data) {
|
||||
exportData.statisticsHistory = getNewestStats.data.data.map((stat) => ({
|
||||
recorded: stat.recorded,
|
||||
UUID: stat.UUID,
|
||||
data: stat.data,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('healthChecks') && getHealth.data?.values) {
|
||||
exportData.healthChecks = getHealth.data.values.map((check) => ({
|
||||
recorded: check.recorded,
|
||||
sanity: check.sanity,
|
||||
UUID: check.UUID,
|
||||
values: check.values,
|
||||
}));
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('provisioning') && getTag.data) {
|
||||
exportData.provisioning = {
|
||||
entity: getTag.data.entity,
|
||||
venue: getTag.data.venue,
|
||||
subscriber: getTag.data.subscriber,
|
||||
extendedInfo: getTag.data.extendedInfo,
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('commands') && getCommands.data?.commands) {
|
||||
exportData.commands = getCommands.data.commands;
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('logs') && getLogs.data?.values) {
|
||||
exportData.logs = getLogs.data.values;
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('crashes') && getCrashes.data?.values) {
|
||||
exportData.crashes = getCrashes.data.values;
|
||||
}
|
||||
|
||||
if (selectedOptions.includes('reboots') && getReboots.data?.values) {
|
||||
exportData.reboots = getReboots.data.values;
|
||||
}
|
||||
|
||||
return exportData;
|
||||
}, [selectedOptions, getDevice.data, getStatus.data, getStats.data, getNewestStats.data, getHealth.data, getTag.data, getCommands.data, getLogs.data, getCrashes.data, getReboots.data, serialNumber]);
|
||||
|
||||
const handleExport = () => {
|
||||
if (selectedOptions.length === 0) {
|
||||
toast({
|
||||
id: 'export-no-selection',
|
||||
title: t('common.error'),
|
||||
description: t('export.select_at_least_one'),
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsExporting(true);
|
||||
|
||||
try {
|
||||
const exportData = buildExportData();
|
||||
const jsonString = JSON.stringify(exportData, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${serialNumber}-export-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
id: 'export-success',
|
||||
title: t('common.success'),
|
||||
description: t('export.success'),
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
|
||||
modalProps.onClose();
|
||||
} catch (e) {
|
||||
toast({
|
||||
id: 'export-error',
|
||||
title: t('common.error'),
|
||||
description: t('export.error'),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
getDevice.isFetching ||
|
||||
getStatus.isFetching ||
|
||||
getStats.isFetching ||
|
||||
getNewestStats.isFetching ||
|
||||
getHealth.isFetching ||
|
||||
getTag.isFetching ||
|
||||
getCommands.isFetching ||
|
||||
getLogs.isFetching ||
|
||||
getCrashes.isFetching ||
|
||||
getReboots.isFetching;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalProps.isOpen}
|
||||
onClose={modalProps.onClose}
|
||||
title={t('export.title')}
|
||||
topRightButtons={
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
leftIcon={<DownloadSimple size={20} />}
|
||||
onClick={handleExport}
|
||||
isLoading={isExporting}
|
||||
isDisabled={selectedOptions.length === 0 || isLoading}
|
||||
>
|
||||
{t('common.download')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
{isLoading ? (
|
||||
<Center my={8}>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
) : (
|
||||
<Box>
|
||||
<FormLabel>{t('export.select_data')}</FormLabel>
|
||||
<Stack spacing={3} mt={2}>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="pink"
|
||||
isChecked={selectedOptions.includes('commands')}
|
||||
onChange={onToggle('commands')}
|
||||
>
|
||||
{t('controller.devices.commands')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.commands_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="purple"
|
||||
isChecked={selectedOptions.includes('configuration')}
|
||||
onChange={onToggle('configuration')}
|
||||
>
|
||||
{t('configurations.one')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.configuration_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="red"
|
||||
isChecked={selectedOptions.includes('crashes')}
|
||||
onChange={onToggle('crashes')}
|
||||
>
|
||||
{t('devices.crash_logs')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.crashes_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="blue"
|
||||
isChecked={selectedOptions.includes('deviceInfo')}
|
||||
onChange={onToggle('deviceInfo')}
|
||||
>
|
||||
{t('common.details')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.device_info_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="orange"
|
||||
isChecked={selectedOptions.includes('healthChecks')}
|
||||
onChange={onToggle('healthChecks')}
|
||||
>
|
||||
{t('controller.devices.healthchecks')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.health_checks_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="yellow"
|
||||
isChecked={selectedOptions.includes('logs')}
|
||||
onChange={onToggle('logs')}
|
||||
>
|
||||
{t('controller.devices.logs')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.logs_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="cyan"
|
||||
isChecked={selectedOptions.includes('provisioning')}
|
||||
onChange={onToggle('provisioning')}
|
||||
>
|
||||
{t('controller.provisioning.title')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.provisioning_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="gray"
|
||||
isChecked={selectedOptions.includes('reboots')}
|
||||
onChange={onToggle('reboots')}
|
||||
>
|
||||
{t('devices.reboot_logs')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.reboots_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="teal"
|
||||
isChecked={selectedOptions.includes('statistics')}
|
||||
onChange={onToggle('statistics')}
|
||||
>
|
||||
{t('configurations.statistics')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.statistics_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Checkbox
|
||||
colorScheme="green"
|
||||
isChecked={selectedOptions.includes('status')}
|
||||
onChange={onToggle('status')}
|
||||
>
|
||||
{t('common.status')}
|
||||
</Checkbox>
|
||||
<Text fontSize="xs" color="gray.500" ml={6}>
|
||||
{t('export.status_desc')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ExportStatsModal);
|
||||
50
src/components/Modals/ReEnrollModal/index.tsx
Normal file
50
src/components/Modals/ReEnrollModal/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Center, Spinner, Alert, Button } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '../Modal';
|
||||
import { useReEnroll } from 'hooks/Network/ReEnroll';
|
||||
import { ModalProps } from 'models/Modal';
|
||||
|
||||
interface Props {
|
||||
modalProps: ModalProps;
|
||||
serialNumber: string;
|
||||
}
|
||||
|
||||
const ReEnrollModal = ({ modalProps: { isOpen, onClose }, serialNumber }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate: reEnroll, isLoading } = useReEnroll({ serialNumber });
|
||||
|
||||
const submit = () => {
|
||||
reEnroll(
|
||||
{ serialNumber, when: 0 },
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={t('controller.devices.re_enroll')}>
|
||||
{isLoading ? (
|
||||
<Center>
|
||||
<Spinner size="lg" />
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<Alert colorScheme="blue" mb={6}>
|
||||
{t('controller.devices.re_enroll_warning', { serialNumber })}
|
||||
</Alert>
|
||||
<Center mb={6}>
|
||||
<Button size="lg" colorScheme="blue" onClick={submit} fontWeight="bold">
|
||||
{t('controller.devices.confirm_re_enroll', { serialNumber })}
|
||||
</Button>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReEnrollModal;
|
||||
@@ -166,6 +166,7 @@ export type DeviceStatus = {
|
||||
connected: boolean;
|
||||
connectReason?: string;
|
||||
certificateExpiryDate: number;
|
||||
certificateIssuerName?: string;
|
||||
connectionCompletionTime: number;
|
||||
firmware: string;
|
||||
ipAddress: string;
|
||||
@@ -377,6 +378,40 @@ export const useWifiScanDevice = ({ serialNumber }: { serialNumber: string }) =>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCableDiagnostics = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation(
|
||||
(ports: string[]): Promise<unknown> =>
|
||||
axiosGw
|
||||
.post(`device/${serialNumber}/cable-diagnostics`, {
|
||||
serial: serialNumber,
|
||||
ports,
|
||||
when: 0,
|
||||
})
|
||||
.then(({ data }) => data),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
console.log('Success data: ', data);
|
||||
},
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('commands.cablediagnostics_error', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetDeviceRtty = ({ serialNumber, extraId }: { serialNumber: string; extraId: string | number }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
78
src/hooks/Network/ReEnroll.ts
Normal file
78
src/hooks/Network/ReEnroll.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
|
||||
export type ReEnrollRequest = {
|
||||
serialNumber: string;
|
||||
when?: number;
|
||||
};
|
||||
|
||||
export type ReEnrollResponse = {
|
||||
UUID: string;
|
||||
command: 're-enroll' | 'reenroll';
|
||||
completed: number;
|
||||
custom: number;
|
||||
details: {
|
||||
serial: string;
|
||||
when: number;
|
||||
};
|
||||
errorCode: number;
|
||||
errorText: string;
|
||||
executed: number;
|
||||
executionTime: number;
|
||||
results: {
|
||||
serial: string;
|
||||
status: {
|
||||
error: number;
|
||||
resultCode: number;
|
||||
resultText: string;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
serialNumber: string;
|
||||
status: string;
|
||||
submitted: number;
|
||||
submittedBy: string;
|
||||
when: number;
|
||||
};
|
||||
|
||||
const reEnrollDevice = async ({ serialNumber, when = 0 }: ReEnrollRequest) =>
|
||||
axiosGw.post<ReEnrollResponse>(`device/${serialNumber}/reenroll`, {
|
||||
serial: serialNumber,
|
||||
when,
|
||||
});
|
||||
|
||||
export const useReEnroll = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useMutation(reEnrollDevice, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
queryClient.invalidateQueries(['device', serialNumber]);
|
||||
queryClient.invalidateQueries(['device-status', serialNumber]);
|
||||
toast({
|
||||
id: `re-enroll-success-${serialNumber}`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.devices.re_enroll_initiated', { serialNumber }),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
id: `re-enroll-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: error?.response?.data?.ErrorDescription || t('common.error'),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -171,6 +171,12 @@ const DeviceSummary = ({ serialNumber }: Props) => {
|
||||
'-'
|
||||
)}
|
||||
</GridItem>
|
||||
<GridItem colSpan={1} alignContent="center" alignItems="center">
|
||||
<Heading size="sm">{t('devices.certificate_issuer')}:</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={1}>
|
||||
{getStatus.data?.certificateIssuerName ? getStatus.data.certificateIssuerName.split('CN=')[1] : '-'}
|
||||
</GridItem>
|
||||
<GridItem colSpan={1} alignContent="center" alignItems="center">
|
||||
<Heading size="sm">Connect Reason:</Heading>
|
||||
</GridItem>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { IconButton, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import { Power } from '@phosphor-icons/react';
|
||||
import { Power, PlugsConnected } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePowerCycle } from 'hooks/Network/Devices';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
import { DeviceLinkState } from 'hooks/Network/Statistics';
|
||||
import { CableDiagnosticsModalProps } from 'components/Modals/CableDiagnosticsModal';
|
||||
|
||||
type Props = {
|
||||
state: DeviceLinkState & { name: string };
|
||||
deviceSerialNumber: string;
|
||||
onOpenCableDiagnostics: (port: string) => void;
|
||||
};
|
||||
|
||||
const LinkStateTableActions = ({ state, deviceSerialNumber }: Props) => {
|
||||
const LinkStateTableActions = ({ state, deviceSerialNumber, onOpenCableDiagnostics }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const powerCycle = usePowerCycle();
|
||||
const toast = useToast();
|
||||
@@ -54,16 +56,27 @@ const LinkStateTableActions = ({ state, deviceSerialNumber }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label="Power Cycle" placement="auto-start">
|
||||
<IconButton
|
||||
aria-label="Power Cycle"
|
||||
icon={<Power size={20} />}
|
||||
colorScheme="green"
|
||||
onClick={onPowerCycle}
|
||||
isLoading={powerCycle.isLoading}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
<>
|
||||
<Tooltip label="Power Cycle" placement="auto-start">
|
||||
<IconButton
|
||||
aria-label="Power Cycle"
|
||||
icon={<Power size={20} />}
|
||||
colorScheme="green"
|
||||
onClick={onPowerCycle}
|
||||
isLoading={powerCycle.isLoading}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Cable Diagnostics" placement="auto-start">
|
||||
<IconButton
|
||||
aria-label="Cable Diagnostics"
|
||||
icon={<PlugsConnected size={20} />}
|
||||
colorScheme="blue"
|
||||
onClick={() => onOpenCableDiagnostics(state.name)}
|
||||
size="xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,8 +9,12 @@ import LinkStateTableActions from './Actions';
|
||||
|
||||
type Row = DeviceLinkState & { name: string };
|
||||
const dataCell = (v: number) => <DataCell bytes={v} />;
|
||||
const actionCell = (row: Row, serialNumber: string) => (
|
||||
<LinkStateTableActions state={row} deviceSerialNumber={serialNumber} />
|
||||
const actionCell = (row: Row, serialNumber: string, onOpenCableDiagnostics: (port: string) => void) => (
|
||||
<LinkStateTableActions
|
||||
state={row}
|
||||
deviceSerialNumber={serialNumber}
|
||||
onOpenCableDiagnostics={onOpenCableDiagnostics}
|
||||
/>
|
||||
);
|
||||
|
||||
type Props = {
|
||||
@@ -19,9 +23,10 @@ type Props = {
|
||||
isFetching: boolean;
|
||||
type: 'upstream' | 'downstream';
|
||||
serialNumber: string;
|
||||
onOpenCableDiagnostics: (port: string) => void;
|
||||
};
|
||||
|
||||
const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }: Props) => {
|
||||
const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber, onOpenCableDiagnostics }: Props) => {
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'switch.link-state.table',
|
||||
defaultOrder: [
|
||||
@@ -157,10 +162,16 @@ const LinkStateTable = ({ statistics, refetch, isFetching, type, serialNumber }:
|
||||
id: 'actions',
|
||||
header: '',
|
||||
accessorKey: '',
|
||||
cell: ({ cell }) => actionCell(cell.row.original, serialNumber),
|
||||
cell: ({ cell }) => (
|
||||
<LinkStateTableActions
|
||||
state={cell.row.original}
|
||||
deviceSerialNumber={serialNumber}
|
||||
onOpenCableDiagnostics={onOpenCableDiagnostics}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
[onOpenCableDiagnostics],
|
||||
);
|
||||
|
||||
if (!statistics || statistics?.length === 0) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import SwitchInterfaceTable from './SwitchInterfaceTable';
|
||||
import { DeviceLinkState, useGetDeviceLastStats } from 'hooks/Network/Statistics';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CableDiagnosticsModal } from 'components/Modals/CableDiagnosticsModal';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
@@ -12,6 +14,8 @@ type Props = {
|
||||
|
||||
const SwitchPortExamination = ({ serialNumber }: Props) => {
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
const [selectedPort, setSelectedPort] = React.useState<string>('');
|
||||
const cableDiagnosticsModalProps = useDisclosure();
|
||||
|
||||
const handleTabsChange = React.useCallback((index: number) => {
|
||||
setTabIndex(index);
|
||||
@@ -35,63 +39,73 @@ const SwitchPortExamination = ({ serialNumber }: Props) => {
|
||||
}));
|
||||
}, [getStats.data]);
|
||||
|
||||
const handleOpenCableDiagnostics = React.useCallback((port: string) => {
|
||||
setSelectedPort(port);
|
||||
cableDiagnosticsModalProps.onOpen();
|
||||
}, []);
|
||||
|
||||
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"
|
||||
serialNumber={serialNumber}
|
||||
/>
|
||||
) : (
|
||||
<Spinner size="xl" />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{getStats.data ? (
|
||||
<LinkStateTable
|
||||
statistics={downLinkStates}
|
||||
refetch={getStats.refetch}
|
||||
isFetching={getStats.isFetching}
|
||||
type="downstream"
|
||||
serialNumber={serialNumber}
|
||||
/>
|
||||
) : (
|
||||
<Spinner size="xl" />
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<>
|
||||
<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"
|
||||
serialNumber={serialNumber}
|
||||
onOpenCableDiagnostics={handleOpenCableDiagnostics}
|
||||
/>
|
||||
) : (
|
||||
<Spinner size="xl" />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{getStats.data ? (
|
||||
<LinkStateTable
|
||||
statistics={downLinkStates}
|
||||
refetch={getStats.refetch}
|
||||
isFetching={getStats.isFetching}
|
||||
type="downstream"
|
||||
serialNumber={serialNumber}
|
||||
onOpenCableDiagnostics={handleOpenCableDiagnostics}
|
||||
/>
|
||||
) : (
|
||||
<Spinner size="xl" />
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<CableDiagnosticsModal modalProps={cableDiagnosticsModalProps} serialNumber={serialNumber} port={selectedPort} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { Barcode } from '@phosphor-icons/react';
|
||||
import { Barcode, DownloadSimple } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { useGetDevice } from 'hooks/Network/Devices';
|
||||
@@ -35,6 +35,17 @@ const ViewConfigurationModal = ({ serialNumber }: { serialNumber: string }) => {
|
||||
}
|
||||
}, [getDevice.data?.configuration]);
|
||||
|
||||
const handleDownload = () => {
|
||||
const jsonString = JSON.stringify(getDevice.data?.configuration ?? {}, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${serialNumber}-configuration.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleOpenClick = () => {
|
||||
getDevice.refetch();
|
||||
onOpen();
|
||||
@@ -58,6 +69,14 @@ const ViewConfigurationModal = ({ serialNumber }: { serialNumber: string }) => {
|
||||
<Button onClick={onCopy} size="md" colorScheme="teal">
|
||||
{hasCopied ? `${t('common.copied')}!` : t('common.copy')}
|
||||
</Button>
|
||||
<Tooltip label={t('common.download')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('common.download')}
|
||||
icon={<DownloadSimple size={20} />}
|
||||
onClick={handleDownload}
|
||||
colorScheme="blue"
|
||||
/>
|
||||
</Tooltip>
|
||||
<RefreshButton onClick={getDevice.refetch} isFetching={getDevice.isFetching} />
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
||||
import FactoryResetModal from 'components/Modals/FactoryResetModal';
|
||||
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
|
||||
import { RebootModal } from 'components/Modals/RebootModal';
|
||||
import ReEnrollModal from 'components/Modals/ReEnrollModal';
|
||||
import ExportStatsModal from 'components/Modals/ExportStatsModal';
|
||||
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
||||
import ethernetConnected from './ethernetIconConnected.svg?react';
|
||||
import ethernetDisconnected from './ethernetIconDisconnected.svg?react';
|
||||
@@ -68,6 +70,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 });
|
||||
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
||||
const scanModalProps = useDisclosure();
|
||||
const cableDiagnosticsModalProps = useDisclosure();
|
||||
const resetModalProps = useDisclosure();
|
||||
const eventQueueProps = useDisclosure();
|
||||
const configureModalProps = useDisclosure();
|
||||
@@ -75,6 +78,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
const telemetryModalProps = useDisclosure();
|
||||
const traceModalProps = useDisclosure();
|
||||
const rebootModalProps = useDisclosure();
|
||||
const reEnrollModalProps = useDisclosure();
|
||||
const exportModalProps = useDisclosure();
|
||||
const scriptModal = useScriptModal();
|
||||
// Sticky-top styles
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
@@ -215,6 +220,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
||||
onOpenScriptModal={scriptModal.openModal}
|
||||
onOpenRebootModal={rebootModalProps.onOpen}
|
||||
onOpenReEnrollModal={reEnrollModalProps.onOpen}
|
||||
onOpenExportModal={exportModalProps.onOpen}
|
||||
size="md"
|
||||
isCompact
|
||||
/>
|
||||
@@ -267,6 +274,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
||||
onOpenRebootModal={rebootModalProps.onOpen}
|
||||
onOpenScriptModal={scriptModal.openModal}
|
||||
onOpenReEnrollModal={reEnrollModalProps.onOpen}
|
||||
onOpenExportModal={exportModalProps.onOpen}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
@@ -310,6 +319,8 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||
<ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} />
|
||||
<TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} />
|
||||
<RebootModal serialNumber={serialNumber} modalProps={rebootModalProps} />
|
||||
<ReEnrollModal serialNumber={serialNumber} modalProps={reEnrollModalProps} />
|
||||
<ExportStatsModal serialNumber={serialNumber} modalProps={exportModalProps} />
|
||||
{scriptModal.modal}
|
||||
<Box mt={isCompact ? '0px' : '68px'}>
|
||||
<Masonry
|
||||
|
||||
469
src/pages/Notifications/ExportAll/index.tsx
Normal file
469
src/pages/Notifications/ExportAll/index.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
CircularProgress,
|
||||
CircularProgressLabel,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { Play, Stop, Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||
import { useSecurityStore } from 'contexts/SecuritySocketProvider/useStore';
|
||||
import { useFirmwareStore } from 'contexts/FirmwareSocketProvider/useStore';
|
||||
|
||||
type ExportFormat = 'json' | 'csv';
|
||||
|
||||
type LogEntry = {
|
||||
source: string;
|
||||
timestamp: string;
|
||||
type: string;
|
||||
level?: string;
|
||||
thread?: string;
|
||||
message: string;
|
||||
serialNumber?: string;
|
||||
rawData?: string;
|
||||
};
|
||||
|
||||
const ExportAllLogsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
const [duration, setDuration] = React.useState<number>(1);
|
||||
const [format, setFormat] = React.useState<ExportFormat>('json');
|
||||
const [isCollecting, setIsCollecting] = React.useState(false);
|
||||
const [timeRemaining, setTimeRemaining] = React.useState<number>(0);
|
||||
const [startTime, setStartTime] = React.useState<Date | null>(null);
|
||||
|
||||
const controllerLogs = useControllerStore((state) => state.allMessages);
|
||||
const securityLogs = useSecurityStore((state) => state.allMessages);
|
||||
const firmwareLogs = useFirmwareStore((state) => state.allMessages);
|
||||
|
||||
const controllerConnected = useControllerStore((state) => state.isWebSocketOpen);
|
||||
const securityConnected = useSecurityStore((state) => state.isWebSocketOpen);
|
||||
const firmwareConnected = useFirmwareStore((state) => state.isWebSocketOpen);
|
||||
|
||||
const timerRef = React.useRef<NodeJS.Timeout | null>(null);
|
||||
const collectedLogsRef = React.useRef<{
|
||||
devices: typeof controllerLogs;
|
||||
controller: typeof controllerLogs;
|
||||
security: typeof securityLogs;
|
||||
firmware: typeof firmwareLogs;
|
||||
startTime: Date | null;
|
||||
}>({
|
||||
devices: [],
|
||||
controller: [],
|
||||
security: [],
|
||||
firmware: [],
|
||||
startTime: null,
|
||||
});
|
||||
|
||||
const formatLogEntry = (
|
||||
source: string,
|
||||
msg: (typeof controllerLogs)[0] | (typeof securityLogs)[0] | (typeof firmwareLogs)[0],
|
||||
): LogEntry | null => {
|
||||
if (msg.type !== 'NOTIFICATION') return null;
|
||||
|
||||
const data = msg.data;
|
||||
if (data.type === 'LOG' && data.log) {
|
||||
return {
|
||||
source,
|
||||
timestamp: msg.timestamp.toISOString(),
|
||||
type: 'LOG',
|
||||
level: data.log.level,
|
||||
thread: `${data.log.thread_id}-${data.log.thread_name}`,
|
||||
message: typeof data.log.msg === 'string' ? data.log.msg : JSON.stringify(data.log.msg),
|
||||
};
|
||||
}
|
||||
if (data.type === 'DEVICE_CONNECTION' || data.type === 'DEVICE_DISCONNECTION') {
|
||||
return {
|
||||
source: 'Devices',
|
||||
timestamp: msg.timestamp.toISOString(),
|
||||
type: data.type,
|
||||
serialNumber: data.serialNumber,
|
||||
message: data.type === 'DEVICE_CONNECTION' ? 'Device connected' : 'Device disconnected',
|
||||
};
|
||||
}
|
||||
if (data.type === 'DEVICE_STATISTICS') {
|
||||
return {
|
||||
source: 'Devices',
|
||||
timestamp: msg.timestamp.toISOString(),
|
||||
type: 'DEVICE_STATISTICS',
|
||||
serialNumber: data.serialNumber,
|
||||
message: 'New statistics received',
|
||||
};
|
||||
}
|
||||
if (data.type === 'DEVICE_CONNECTIONS_STATISTICS') {
|
||||
return {
|
||||
source: 'Devices',
|
||||
timestamp: msg.timestamp.toISOString(),
|
||||
type: 'DEVICE_CONNECTIONS_STATISTICS',
|
||||
message: 'Global connection statistics update',
|
||||
rawData: JSON.stringify(data.statistics),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildExportData = React.useCallback(() => {
|
||||
const logs = collectedLogsRef.current;
|
||||
const allLogs: LogEntry[] = [];
|
||||
|
||||
// Filter logs that arrived after startTime
|
||||
const filterByTime = <T extends { timestamp: Date }>(arr: T[], start: Date | null): T[] => {
|
||||
if (!start) return arr;
|
||||
return arr.filter((item) => item.timestamp >= start);
|
||||
};
|
||||
|
||||
// Process Device logs (connections, disconnections, statistics)
|
||||
filterByTime(logs.devices, logs.startTime).forEach((msg) => {
|
||||
const entry = formatLogEntry('Devices', msg);
|
||||
if (entry) allLogs.push(entry);
|
||||
});
|
||||
|
||||
// Process Controller logs
|
||||
filterByTime(logs.controller, logs.startTime).forEach((msg) => {
|
||||
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||
const entry = formatLogEntry('Controller', msg);
|
||||
if (entry) allLogs.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
// Process Security logs
|
||||
filterByTime(logs.security, logs.startTime).forEach((msg) => {
|
||||
const entry = formatLogEntry('Security', msg);
|
||||
if (entry) allLogs.push(entry);
|
||||
});
|
||||
|
||||
// Process Firmware logs
|
||||
filterByTime(logs.firmware, logs.startTime).forEach((msg) => {
|
||||
const entry = formatLogEntry('Firmware', msg);
|
||||
if (entry) allLogs.push(entry);
|
||||
});
|
||||
|
||||
// Sort by timestamp
|
||||
allLogs.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
|
||||
return allLogs;
|
||||
}, []);
|
||||
|
||||
const exportToJson = (data: LogEntry[]) => {
|
||||
const exportObj = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
collectionDuration: `${duration} minute(s)`,
|
||||
totalLogs: data.length,
|
||||
logs: data,
|
||||
};
|
||||
const jsonString = JSON.stringify(exportObj, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `all-logs-export-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const exportToCsv = (data: LogEntry[]) => {
|
||||
const headers = ['Source', 'Timestamp', 'Type', 'Level', 'Thread', 'Serial Number', 'Message', 'Raw Data'];
|
||||
const rows = data.map((log) => [
|
||||
log.source,
|
||||
log.timestamp,
|
||||
log.type,
|
||||
log.level || '',
|
||||
log.thread || '',
|
||||
log.serialNumber || '',
|
||||
`"${(log.message || '').replace(/"/g, '""')}"`,
|
||||
`"${(log.rawData || '').replace(/"/g, '""')}"`,
|
||||
]);
|
||||
|
||||
const csvContent = [headers.join(','), ...rows.map((row) => row.join(','))].join('\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `all-logs-export-${new Date().toISOString().replace(/[:.]/g, '-')}.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleStartCollection = () => {
|
||||
const now = new Date();
|
||||
setStartTime(now);
|
||||
setIsCollecting(true);
|
||||
setTimeRemaining(duration * 60);
|
||||
|
||||
collectedLogsRef.current = {
|
||||
devices: [],
|
||||
controller: [],
|
||||
security: [],
|
||||
firmware: [],
|
||||
startTime: now,
|
||||
};
|
||||
|
||||
toast({
|
||||
id: 'collection-started',
|
||||
title: t('logs.collection_started'),
|
||||
description: t('logs.collection_started_desc', { minutes: duration }),
|
||||
status: 'info',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
};
|
||||
|
||||
const handleStopCollection = React.useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
|
||||
// Capture final state of logs
|
||||
collectedLogsRef.current = {
|
||||
devices: [...controllerLogs],
|
||||
controller: [...controllerLogs],
|
||||
security: [...securityLogs],
|
||||
firmware: [...firmwareLogs],
|
||||
startTime: collectedLogsRef.current.startTime,
|
||||
};
|
||||
|
||||
const data = buildExportData();
|
||||
|
||||
if (data.length === 0) {
|
||||
toast({
|
||||
id: 'no-logs',
|
||||
title: t('common.warning'),
|
||||
description: t('logs.no_logs_collected'),
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
} else {
|
||||
if (format === 'json') {
|
||||
exportToJson(data);
|
||||
} else {
|
||||
exportToCsv(data);
|
||||
}
|
||||
|
||||
toast({
|
||||
id: 'export-success',
|
||||
title: t('common.success'),
|
||||
description: t('logs.export_success', { count: data.length }),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
|
||||
setIsCollecting(false);
|
||||
setTimeRemaining(0);
|
||||
setStartTime(null);
|
||||
}, [controllerLogs, securityLogs, firmwareLogs, format, duration, buildExportData, t, toast]);
|
||||
|
||||
// Timer countdown
|
||||
React.useEffect(() => {
|
||||
if (isCollecting && timeRemaining > 0) {
|
||||
timerRef.current = setInterval(() => {
|
||||
setTimeRemaining((prev) => {
|
||||
if (prev <= 1) {
|
||||
handleStopCollection();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [isCollecting, handleStopCollection]);
|
||||
|
||||
// Cleanup on unmount
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Warn user before leaving page or refreshing during collection
|
||||
React.useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (isCollecting) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
return '';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}, [isCollecting]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const progress = duration * 60 > 0 ? ((duration * 60 - timeRemaining) / (duration * 60)) * 100 : 0;
|
||||
|
||||
const connectedCount = [controllerConnected, securityConnected, firmwareConnected].filter(Boolean).length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Text fontSize="xl" fontWeight="bold">
|
||||
{t('logs.export_all_title')}
|
||||
</Text>
|
||||
<Box flex={1} />
|
||||
{isCollecting ? (
|
||||
<Button colorScheme="red" leftIcon={<Stop size={20} />} onClick={handleStopCollection}>
|
||||
{t('logs.stop_and_export')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
leftIcon={<Play size={20} />}
|
||||
onClick={handleStartCollection}
|
||||
isDisabled={connectedCount === 0}
|
||||
>
|
||||
{t('logs.start_collection')}
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box>
|
||||
{isCollecting ? (
|
||||
<Center flexDirection="column" py={8}>
|
||||
<CircularProgress value={progress} size="150px" thickness="8px" color="blue.400">
|
||||
<CircularProgressLabel>
|
||||
<Text fontSize="2xl" fontWeight="bold">
|
||||
{formatTime(timeRemaining)}
|
||||
</Text>
|
||||
</CircularProgressLabel>
|
||||
</CircularProgress>
|
||||
<Text mt={4} fontSize="lg">
|
||||
{t('logs.collecting_logs')}
|
||||
</Text>
|
||||
<Text mt={2} color="gray.500">
|
||||
{t('logs.logs_will_export', { format: format.toUpperCase() })}
|
||||
</Text>
|
||||
<Box mt={4}>
|
||||
<Text fontSize="sm" color="gray.500" mb={2}>{t('logs.logs_collected')}:</Text>
|
||||
<HStack spacing={4}>
|
||||
<Text fontSize="sm">
|
||||
{t('devices.title')}: {startTime ? controllerLogs.filter((m) => m.timestamp >= startTime).length : 0}
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{t('simulation.controller')}: {startTime ? controllerLogs.filter((m) => m.timestamp >= startTime && m.data?.type === 'LOG').length : 0}
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{t('logs.security')}: {startTime ? securityLogs.filter((m) => m.timestamp >= startTime).length : 0}
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{t('logs.firmware')}: {startTime ? firmwareLogs.filter((m) => m.timestamp >= startTime).length : 0}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
<Alert status="warning" mt={6} maxW="400px" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm">{t('logs.stay_on_page_warning')}</Text>
|
||||
</Alert>
|
||||
</Center>
|
||||
) : (
|
||||
<Stack spacing={6} maxW="400px">
|
||||
<Box>
|
||||
<FormLabel>{t('logs.websocket_status')}</FormLabel>
|
||||
<Stack spacing={2}>
|
||||
<HStack>
|
||||
<Box w={3} h={3} borderRadius="full" bg={controllerConnected ? 'green.500' : 'red.500'} />
|
||||
<Text fontSize="sm" fontWeight="medium">{t('simulation.controller')}</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
({t('devices.title')}, {t('simulation.controller')})
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<Box w={3} h={3} borderRadius="full" bg={securityConnected ? 'green.500' : 'red.500'} />
|
||||
<Text fontSize="sm" fontWeight="medium">{t('logs.security')}</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
({t('logs.security')})
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<Box w={3} h={3} borderRadius="full" bg={firmwareConnected ? 'green.500' : 'red.500'} />
|
||||
<Text fontSize="sm" fontWeight="medium">{t('logs.firmware')}</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
({t('logs.firmware')})
|
||||
</Text>
|
||||
</HStack>
|
||||
</Stack>
|
||||
<Text fontSize="xs" color="gray.500" mt={2}>
|
||||
{t('logs.connected_count', { count: connectedCount })}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<FormLabel>{t('logs.collection_duration')}</FormLabel>
|
||||
<Select value={duration} onChange={(e) => setDuration(Number(e.target.value))} w="200px">
|
||||
<option value={1}>1 {t('common.minute')}</option>
|
||||
<option value={2}>2 {t('common.minutes')}</option>
|
||||
<option value={5}>5 {t('common.minutes')}</option>
|
||||
<option value={10}>10 {t('common.minutes')}</option>
|
||||
<option value={15}>15 {t('common.minutes')}</option>
|
||||
<option value={30}>30 {t('common.minutes')}</option>
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<FormLabel>{t('logs.export_format')}</FormLabel>
|
||||
<RadioGroup value={format} onChange={(val) => setFormat(val as ExportFormat)}>
|
||||
<Stack direction="row" spacing={4}>
|
||||
<Radio value="json">JSON</Radio>
|
||||
<Radio value="csv">CSV</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
|
||||
<Alert status="info" borderRadius="md">
|
||||
<AlertIcon />
|
||||
<Text fontSize="sm">{t('logs.stay_on_page_info')}</Text>
|
||||
</Alert>
|
||||
|
||||
{connectedCount === 0 && (
|
||||
<Text color="red.500" fontSize="sm">
|
||||
{t('logs.no_websockets_connected')}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportAllLogsPage;
|
||||
@@ -11,6 +11,7 @@ const AllDevicesPage = React.lazy(() => import('pages/Devices/ListCard'));
|
||||
const BlacklistPage = React.lazy(() => import('pages/Devices/Blacklist'));
|
||||
const ControllerLogsPage = React.lazy(() => import('pages/Notifications/GeneralLogs'));
|
||||
const DeviceLogsPage = React.lazy(() => import('pages/Notifications/DeviceLogs'));
|
||||
const ExportAllLogsPage = React.lazy(() => import('pages/Notifications/ExportAll'));
|
||||
const FmsLogsPage = React.lazy(() => import('pages/Notifications/FmsLogs'));
|
||||
const SecLogsPage = React.lazy(() => import('pages/Notifications/SecLogs'));
|
||||
const FirmwarePage = React.lazy(() => import('pages/Firmware/List'));
|
||||
@@ -144,6 +145,13 @@ const routes: Route[] = [
|
||||
navName: (t) => `${t('logs.firmware')} ${t('controller.devices.logs')}`,
|
||||
component: FmsLogsPage,
|
||||
},
|
||||
{
|
||||
id: 'logs-export',
|
||||
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
|
||||
path: '/logs/export',
|
||||
name: 'logs.export_all',
|
||||
component: ExportAllLogsPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user