From a3647bca08b72db1652732b10213b4b7e27735f6 Mon Sep 17 00:00:00 2001 From: Sebastian Rubina Date: Mon, 14 Jul 2025 13:16:26 -0400 Subject: [PATCH] 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 --- package-lock.json | 32 ++++---- public/locales/en/translation.json | 2 + .../Buttons/DeviceActionDropdown/index.tsx | 3 + src/hooks/Network/ReEnroll.ts | 78 +++++++++++++++++++ src/pages/Device/Wrapper.tsx | 4 + 5 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 src/hooks/Network/ReEnroll.ts diff --git a/package-lock.json b/package-lock.json index 85d05e5..f459794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "3.2.0", + "version": "4.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "3.2.0", + "version": "4.1.0", "license": "ISC", "dependencies": { "@chakra-ui/anatomy": "^2.1.1", @@ -3540,7 +3540,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3548,7 +3548,7 @@ }, "node_modules/@jridgewell/set-array": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3556,7 +3556,7 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -3565,7 +3565,7 @@ }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { "version": "0.3.2", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.1", @@ -3578,12 +3578,12 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.17", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "3.1.0", @@ -4374,7 +4374,7 @@ "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true + "devOptional": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4419,7 +4419,7 @@ "version": "18.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -4753,7 +4753,7 @@ }, "node_modules/acorn": { "version": "8.8.0", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5174,7 +5174,7 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/builtin-modules": { @@ -9781,7 +9781,7 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -9790,7 +9790,7 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10080,7 +10080,7 @@ }, "node_modules/terser": { "version": "5.15.1", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.2", @@ -10097,7 +10097,7 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/text-table": { diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 49b02d9..ec7faac 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -514,6 +514,8 @@ "started_upgrade": "{{serialNumber}} just shut down to start the upgrade!", "trace": "Trace", "trace_description": "Launch a remote trace of this device for either a specific duration or a number of packets", + "re_enroll": "Re-enroll", + "re_enroll_initiated": "Re-enrollment initiated for device {{serialNumber}}", "update_success": "Device updated!", "updated_blacklist": "Updated Blacklist!" }, diff --git a/src/components/Buttons/DeviceActionDropdown/index.tsx b/src/components/Buttons/DeviceActionDropdown/index.tsx index d34eb40..487281c 100644 --- a/src/components/Buttons/DeviceActionDropdown/index.tsx +++ b/src/components/Buttons/DeviceActionDropdown/index.tsx @@ -32,6 +32,7 @@ interface Props { onOpenTelemetryModal: (serialNumber: string) => void; onOpenScriptModal: (device: GatewayDevice) => void; onOpenRebootModal: (serialNumber: string) => void; + onReEnroll?: () => void; size?: 'sm' | 'md' | 'lg'; isCompact?: boolean; } @@ -49,6 +50,7 @@ const DeviceActionDropdown = ({ onOpenConfigureModal, onOpenScriptModal, onOpenRebootModal, + onReEnroll, size, isCompact, }: Props) => { @@ -234,6 +236,7 @@ const DeviceActionDropdown = ({ + {onReEnroll && {t('controller.devices.re_enroll')}} {t('controller.telemetry.title')} {t('script.one')} {t('controller.devices.trace')} diff --git a/src/hooks/Network/ReEnroll.ts b/src/hooks/Network/ReEnroll.ts new file mode 100644 index 0000000..a7d46a3 --- /dev/null +++ b/src/hooks/Network/ReEnroll.ts @@ -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(`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', + }); + }, + }); +}; diff --git a/src/pages/Device/Wrapper.tsx b/src/pages/Device/Wrapper.tsx index 6be4097..b5e9f68 100644 --- a/src/pages/Device/Wrapper.tsx +++ b/src/pages/Device/Wrapper.tsx @@ -48,6 +48,7 @@ import { TelemetryModal } from 'components/Modals/TelemetryModal'; import { TraceModal } from 'components/Modals/TraceModal'; import { WifiScanModal } from 'components/Modals/WifiScanModal'; import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices'; +import { useReEnroll } from 'hooks/Network/ReEnroll'; import SwitchPortExamination from './SwitchPortExamination'; type Props = { @@ -77,6 +78,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { const traceModalProps = useDisclosure(); const rebootModalProps = useDisclosure(); const scriptModal = useScriptModal(); + const reEnroll = useReEnroll({ serialNumber }); // Sticky-top styles const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md'; const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none'); @@ -216,6 +218,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { onOpenTelemetryModal={telemetryModalProps.onOpen} onOpenScriptModal={scriptModal.openModal} onOpenRebootModal={rebootModalProps.onOpen} + onReEnroll={() => reEnroll.mutate({ serialNumber, when: 0 })} size="md" isCompact /> @@ -268,6 +271,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { onOpenTelemetryModal={telemetryModalProps.onOpen} onOpenRebootModal={rebootModalProps.onOpen} onOpenScriptModal={scriptModal.openModal} + onReEnroll={() => reEnroll.mutate({ serialNumber, when: 0 })} size="md" /> )}