Compare commits

...

35 Commits

Author SHA1 Message Date
Sebastian Rubina
c4aff418ed Merge pull request #233 from Telecominfraproject/WIFI-14521-set-correct-tag-for-main
Set correct tag for helm version
2025-08-05 13:06:11 -04:00
Carsten Schafer
dd5c894b03 Set correct tag for helm version
Signed-off-by: Carsten Schafer <Carsten.Schafer@kinarasystems.com>
2025-08-05 11:51:43 -04:00
Sebastian Rubina
c3256b93c7 Merge pull request #232 from Telecominfraproject/re-enroll-modal
Add device re-enrollment with confirmation modal
2025-07-14 15:50:59 -04:00
Sebastian Rubina
932f1f4a12 Change wording of translation.json.
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-14 15:48:41 -04:00
Sebastian Rubina
db3cbb0b35 Add device re-enrollment with confirmation modal
- Add ReEnrollModal component for user confirmation before re-enrollment
  - Update DeviceActionDropdown to open modal instead of direct action
  - Add modal state management in Device Wrapper component
  - Add translation keys for re-enrollment UI with certificate renewal
  messaging
  - Remove direct useReEnroll hook usage in favor of modal pattern

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-14 15:38:30 -04:00
Sebastian Rubina
c895274ebf Merge pull request #231 from Telecominfraproject/re-enroll-devices
Add device re-enrollment functionality
2025-07-14 13:31:58 -04:00
Sebastian Rubina
a3647bca08 Add device re-enrollment functionality
- Add re-enrollment API hook with mutation handling
  - Add re-enroll option to device action dropdown menu
  - Add translation keys for re-enrollment UI messages

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-14 13:16:26 -04:00
Carsten Schafer
5fbf421d77 Merge pull request #230 from Telecominfraproject/display-certificate-issuer
Display certificate issuer
2025-07-02 13:49:24 -04:00
Carsten Schafer
e09b3ee5f4 Merge branch 'main' into display-certificate-issuer 2025-07-02 11:45:47 -04:00
Sebastian Rubina
855960559d Update package.json version 2025-07-02 11:33:03 -04:00
Sebastian Rubina
4cecfc6fc4 Display Certificate Issuer
- Add label on translation.json
- Add new key on DeviceStatus
- Add column label and data on Summary.txt

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-02 09:49:49 -04:00
Sebastian Rubina
e62d1e4a98 Display Certificate Issuer
- Add label on translation.json
- Add new key on DeviceStatus
- Add column label and data on Summary.txt

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-02 09:49:49 -04:00
Ivan Chvets
6dddba0848 fix: Version update - release 4.0.0
Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2025-07-02 09:49:49 -04:00
i-chvets
30fffdfe52 Merge pull request #228 from Telecominfraproject/version_update
WIFI-14521: fix: Version update - release 4.0.0
2025-04-24 16:39:17 -04:00
Ivan Chvets
c8d6540ca6 fix: Version update - release 4.0.0
Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
2025-04-24 16:36:43 -04:00
i-chvets
2b2f08c231 Merge pull request #229 from Telecominfraproject/WIFI-14521-ci-changes
WIFI-14521: Update to ubuntu-latest for GH runner
2025-04-24 16:18:46 -04:00
Carsten Schafer
0cfed90a7b WIFI-14521: Update to ubuntu-latest for GH runner
Signed-off-by: Carsten Schafer <Carsten.Schafer@kinarasystems.com>
2025-04-24 15:54:46 -04:00
Carsten Schafer
01008dc1aa Merge pull request #226 from Telecominfraproject/version_update
fix: release 3.2.1 version update
2024-12-10 15:36:59 -05:00
Ivan Chvets
26b90cfdba fix: release 3.2.1 version update
https://telecominfraproject.atlassian.net/browse/WIFI-14165

Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
2024-12-10 15:34:09 -05:00
i-chvets
b218051104 Merge pull request #224 from Telecominfraproject/OLS-516-feat-cable-diagnostics-ui
Ols 516 feat cable diagnostics UI
2024-12-05 09:12:28 -05:00
Sebastian Rubina
a2fa93938f feat: cable diagnostics ui
https://telecominfraproject.atlassian.net/browse/OLS-516
Signed-off-by: Sebastian Rubina
<sebastian.rubina@kinarasystems.com>

Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2024-12-04 16:41:06 -05:00
TIP Automation User
c220d11dd0 Chg: update image tag in helm values to v3.2.0
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2024-12-04 16:41:01 -05:00
TIP Automation User
40d533ecc5 Chg: update image tag in helm values to v3.2.0-RC1
Signed-off-by: Sebastian Rubina <sebastian.rubina@icloud.com>
2024-12-04 16:40:40 -05:00
jaspreetsachdev
d1a1c96e74 Merge pull request #223 from Telecominfraproject/version_update
WIFI-14165: release 3.2 version update
2024-09-30 21:02:13 -04:00
Ivan Chvets
1a18985c0d fix: release 3.2 version update
https://telecominfraproject.atlassian.net/browse/WIFI-14165

Signed-off-by: Ivan Chvets <ivan.chvets@kinarasystems.com>
2024-09-30 20:56:34 -04:00
Charles Bourque
8eede7b559 Merge pull request #222 from stephb9959/main
[OLS-106] Add new asterfusion images
2024-06-06 12:01:57 -04:00
Charles
caab40b08e [OLS-106] Add new asterfusion images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-06 12:00:29 -04:00
Charles Bourque
18fa320b19 Merge pull request #221 from stephb9959/main
[OLS-106] Add new asterfusion images
2024-06-06 11:56:32 -04:00
Charles
6f9f6638d6 [OLS-106] Add new asterfusion images
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-06 11:55:56 -04:00
Charles Bourque
5688e2f7bc Merge pull request #220 from stephb9959/main
[OLS-51] Added RTTY for OLS switches
2024-06-04 09:14:01 -04:00
Charles
4738097178 [OLS-51] Added RTTY for OLS switches
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-04 09:13:05 -04:00
Charles Bourque
591ecc3664 Merge pull request #219 from stephb9959/main
[OLS-42] Telemetry duration display fix
2024-06-04 09:06:45 -04:00
Charles
b9089a39ac [OLS-42] Telemetry duration display fix
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-04 09:06:16 -04:00
Charles Bourque
b7bdf89d37 Merge pull request #218 from stephb9959/main
[WIFI-13803] Added fingerprint column to wifi analysis
2024-06-04 08:56:08 -04:00
Charles
849ea9f7b2 [WIFI-13803] Added fingerprint column to wifi analysis
Signed-off-by: Charles <charles.bourque96@gmail.com>
2024-06-04 08:55:35 -04:00
22 changed files with 1822 additions and 1320 deletions

1
.env
View File

@@ -1 +0,0 @@
VITE_UCENTRALSEC_URL="https://ucentral.dpaas.arilia.com:16001"

View File

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

View File

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

21
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "3.0.2(9)",
"version": "4.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "3.0.2(9)",
"version": "4.1.0",
"license": "ISC",
"dependencies": {
"@chakra-ui/anatomy": "^2.1.1",
@@ -5671,8 +5671,9 @@
"license": "MIT"
},
"node_modules/ejs": {
"version": "3.1.8",
"license": "Apache-2.0",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dependencies": {
"jake": "^10.8.5"
},
@@ -6682,9 +6683,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -10442,9 +10443,9 @@
}
},
"node_modules/vite": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz",
"integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
"integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==",
"dependencies": {
"esbuild": "^0.18.10",
"postcss": "^8.4.27",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "3.0.2(9)",
"version": "4.1.0",
"description": "",
"private": true,
"main": "index.tsx",

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@ interface Props {
onOpenTelemetryModal: (serialNumber: string) => void;
onOpenScriptModal: (device: GatewayDevice) => void;
onOpenRebootModal: (serialNumber: string) => void;
onOpenReEnrollModal?: (serialNumber: string) => void;
size?: 'sm' | 'md' | 'lg';
isCompact?: boolean;
}
@@ -49,6 +50,7 @@ const DeviceActionDropdown = ({
onOpenConfigureModal,
onOpenScriptModal,
onOpenRebootModal,
onOpenReEnrollModal,
size,
isCompact,
}: Props) => {
@@ -173,7 +175,7 @@ const DeviceActionDropdown = ({
isLoading={isRtty}
onClick={handleConnectClick}
colorScheme={connectColor}
hidden={isCompact || deviceType !== 'ap'}
hidden={isCompact}
/>
</Tooltip>
<Tooltip label={t('controller.configure.title')}>
@@ -234,6 +236,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>

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

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

View File

@@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next';
import { Modal } from '../Modal';
import { lowercaseFirstLetter } from 'helpers/stringHelper';
import { useTelemetry } from 'hooks/Network/Telemetry';
import { secondsDuration } from 'helpers/dateFormatting';
export type TelemetryModalProps = {
serialNumber: string;
@@ -146,8 +147,7 @@ const _TelemetryModal = ({ serialNumber, modalProps }: TelemetryModalProps) => {
{t('controller.telemetry.interval')}: {form.interval} {lowercaseFirstLetter(t('common.seconds'))}
</p>
<p>
{t('controller.telemetry.duration')}: {form.interval}{' '}
{lowercaseFirstLetter(t('controller.telemetry.minutes'))}
{t('controller.telemetry.duration')}: {secondsDuration(form.lifetime, t)}
</p>
<p>
{t('controller.telemetry.types')}: {form.types.join(', ')}

View File

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

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

View File

@@ -59,6 +59,7 @@ export type DeviceInterfaceStatistics = {
dynamic_vlan?: number;
inactive: number;
ipaddr_v4: string;
fingerprint?: object;
rssi: number;
rx_bytes: number;
rx_duration: number;

View File

@@ -65,6 +65,8 @@ const DeviceSummary = ({ serialNumber }: Props) => {
const getDeviceCompatible = () => {
if (!getDevice.data?.compatible) return undefined;
if (getDevice.data.compatible.includes(' ')) return getDevice.data.compatible.replaceAll(' ', '_');
return getDevice.data?.compatible;
};
@@ -169,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>

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ export type ParsedAssociation = {
txNss: number | string;
recorded: number;
dynamicVlan?: number;
fingerprint?: object;
};
type Props = {
@@ -77,6 +78,14 @@ const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
isMonospace: true,
alwaysShow: true,
},
{
id: 'ssid',
Header: 'SSID',
Footer: '',
accessor: 'ssid',
customWidth: '35px',
alwaysShow: true,
},
{
id: 'ips',
Header: 'IPs',
@@ -84,6 +93,13 @@ const WifiAnalysisAssociationsTable = ({ data, ouis, isSingle }: Props) => {
Cell: (v) => ipCell(v.cell.row.original),
disableSortBy: true,
},
{
id: 'fingerprint',
Header: 'Fingerprint',
Footer: '',
Cell: (v) => Object.values(v.cell.row.original.fingerprint ?? {}).join(', '),
disableSortBy: true,
},
{
id: 'vendor',
Header: t('controller.wifi.vendor'),

View File

@@ -105,6 +105,7 @@ const parseAssociations = (data: { data: DeviceStatistics; recorded: number }, r
txNss: association.tx_rate.nss ?? '-',
recorded: data.recorded,
dynamicVlan: association.dynamic_vlan,
fingerprint: association.fingerprint,
});
}
}

View File

@@ -41,6 +41,7 @@ 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 { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
import ethernetConnected from './ethernetIconConnected.svg?react';
import ethernetDisconnected from './ethernetIconDisconnected.svg?react';
@@ -68,6 +69,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 +77,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
const telemetryModalProps = useDisclosure();
const traceModalProps = useDisclosure();
const rebootModalProps = useDisclosure();
const reEnrollModalProps = useDisclosure();
const scriptModal = useScriptModal();
// Sticky-top styles
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
@@ -215,6 +218,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
onOpenTelemetryModal={telemetryModalProps.onOpen}
onOpenScriptModal={scriptModal.openModal}
onOpenRebootModal={rebootModalProps.onOpen}
onOpenReEnrollModal={reEnrollModalProps.onOpen}
size="md"
isCompact
/>
@@ -267,6 +271,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
onOpenTelemetryModal={telemetryModalProps.onOpen}
onOpenRebootModal={rebootModalProps.onOpen}
onOpenScriptModal={scriptModal.openModal}
onOpenReEnrollModal={reEnrollModalProps.onOpen}
size="md"
/>
)}
@@ -310,6 +315,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
<ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} />
<TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} />
<RebootModal serialNumber={serialNumber} modalProps={rebootModalProps} />
<ReEnrollModal serialNumber={serialNumber} modalProps={reEnrollModalProps} />
{scriptModal.modal}
<Box mt={isCompact ? '0px' : '68px'}>
<Masonry