From f316ec1c01f4176da9dd423ebf8107953a90be9c Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 8 May 2025 13:45:13 -0600 Subject: [PATCH] added the device upgrade code --- src/device/DeviceActions.tsx | 3 +- src/device/UpgradeDevice.tsx | 321 +++++++++++++++++++++++++++++++++++ src/models/Upgrade.ts | 32 ++++ src/models/index.ts | 2 +- src/pages/Firmware.tsx | 2 +- 5 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 src/device/UpgradeDevice.tsx create mode 100644 src/models/Upgrade.ts diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index 787581c..152ba70 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -70,6 +70,7 @@ import SaveDeviceProfile from "./SaveDeviceProfile"; import Connection from "./Connection"; import ComponentOrder from "component/ComponentOrder"; import ArcGISDeviceData from "./ArcGISDeviceData"; +import UpgradeDevice from "./UpgradeDevice"; const useStyles = makeStyles((_theme: Theme) => { // const isMobile = useMobile() @@ -497,7 +498,7 @@ export default function DeviceActions(props: Props) { const buttons = () => { return ( - {/* {canWrite && user.allowedTo("provision") && } */} + {canWrite && user.allowedTo("provision") && } {/* {!isShareableLink(match.params.deviceID) && ( */} { + return ({ + dialogContent: { + marginTop: theme.spacing(2) + }, + buttonProgress: { + color: theme.palette.primary.main, + position: "absolute", + zIndex: 1 + }, + failed: { + color: theme.palette.error.main + } + }) +}); + +interface Props { + device: Device; +} + +export default function UpgradeDevice(props: Props) { + const classes = useStyles(); + const deviceAPI = useDeviceAPI(); + const { success, error } = useSnackbar(); + const firmwareAPI = useFirmwareAPI(); + const [{ firmware }] = useGlobalState(); + const prevFirmware = usePrevious(firmware); + const { device } = props; + const prevDevice = usePrevious(device); + const [open, setOpen] = useState(false); + const prevOpen = usePrevious(open); + const [upgrade, setUpgrade] = useState(Upgrade.create()); + const [gettingStatus, setGettingStatus] = useState(0); + + const progress = useCallback(() => { + return upgrade.status.progress; + }, [upgrade.status.progress]); + + const currentFirmware = () => { + return device.status.firmwareVersion; + }; + + const latestFirmware = () => { + return latestFirmwareVersion( + firmware, + device.settings.platform, + device.settings.upgradeChannel + ); + }; + + const upgradeVersion = () => { + return upgrade.settings.version; + }; + + const available = () => { + return currentFirmware() !== latestFirmware(); + }; + + const upgrading = useCallback(() => { + return progress() === "upgrading"; + }, [progress]); + + const completed = () => { + return progress() === "completed" && currentFirmware() === latestFirmware(); + }; + const failed = () => { + return progress() === "failed" && upgradeVersion() === latestFirmware(); + }; + + const percent = () => { + return Math.round((upgrade.status.offset / upgrade.status.size) * 100); + }; + + const tooltip = () => { + return upgrading() + ? "Upgrade in progress: " + percent() + "%" + : failed() + ? "Upgrade failed" + : "Upgrade Device"; + }; + + const upgradeTimingDesc = () => { + let desc = "requested " + moment(or(upgrade.status.started, 0)).fromNow(); + if (completed()) { + desc = "completed " + moment(or(upgrade.status.stopped, 0)).fromNow(); + } else if (failed()) { + desc = "failed " + moment(or(upgrade.status.changed, 0)).fromNow(); + } + + return desc; + }; + + const mode = () => { + let mode = "prompt"; + if (upgrading() || failed() || completed()) { + mode = "progress"; + } + return mode; + }; + + const getStatus = useCallback(() => { + if (gettingStatus === device.id() && !upgrading()) { + return; + } + setGettingStatus(device.id()); + deviceAPI + .getUpgradeStatus(device.id(), getContextKeys(), getContextTypes()) + .then((res: any) => { + if (res && res.data && res.data.upgrade) { + setUpgrade(Upgrade.any(res.data.upgrade)); + } + }) + .catch((err: any) => error(err)); + }, [device, deviceAPI, error, gettingStatus, upgrading]); + + useEffect(() => { + const deviceIdUnchanged = !prevDevice || prevDevice.id() === device.id(); + if ( + (open && !prevOpen) || + firmware !== prevFirmware || + (device !== prevDevice && deviceIdUnchanged) + ) { + getStatus(); + } + }, [open, prevOpen, getStatus, device, prevDevice, firmware, prevFirmware]); + + useEffect(() => { + function getStatusAgain() { + if (upgrading()) { + getStatus(); + } + } + const interval = setInterval(() => getStatusAgain(), 5000); + return () => { + clearInterval(interval); + }; + }, [open, getStatus, upgrading]); + + const close = () => { + setOpen(false); + }; + + const handleUpgrade = () => { + firmwareAPI + .upgradeFirmware(device.id()) + .then(() => { + success(device.name() + " will upgrade to " + latestFirmware() + " next time it checks in"); + getStatus(); + }) + .catch((err: any) => { + console.error(err); + error(err); + }); + }; + + const restart = () => { + firmwareAPI + .upgradeFirmware(device.id()) + .then(() => { + success( + "Upgrade successfully restarted, " + + device.name() + + " will upgrade next time it checks in" + ); + getStatus(); + }) + .catch((err: any) => { + console.error(err); + error(err); + }); + }; + + const cancel = () => { + firmwareAPI + .cancelUpgrade(device.id()) + .then(() => { + success("Upgrade cancelled"); + }) + .catch((err: any) => { + console.error(err); + error(err); + }); + }; + + const button = () => { + return ( + + setOpen(true)} + aria-label={tooltip()}> + + {upgrading() && ( + + )} + + + ); + }; + + return ( + + {(available() || upgrading() || failed()) && button()} + + {mode() === "prompt" && ( + + + Upgrade Device + + {device.name()} + + + + + + Upgrading from {currentFirmware()} to {latestFirmware()} +
+
+ It is recommended that the device be plugged in during the upgrade as it will take + several minutes to complete +
+
+ While the upgrade is in progress you can return to this dialog to view its status +
+
+ + + + +
+ )} + {mode() === "progress" && ( + + + Upgrading Device + + {device.name()} + + + + + + Upgrade to {latestFirmware()} {upgradeTimingDesc()} + +
+ + + {or(upgrade.status.offset, 0)} / {upgrade.status.size} bytes ({percent()}%) + + {failed() && ( + + +
+ Uh oh – something went wrong. You can try to{" "} + + restart + {" "} + the upgrade, if the problem persists we'll be alerted and follow up as soon as + possible. +
+
+ )} +
+ + + {upgrading() && ( + + )} + {failed() && ( + + )} + +
+ )} +
+
+ ); +} diff --git a/src/models/Upgrade.ts b/src/models/Upgrade.ts new file mode 100644 index 0000000..0802fd1 --- /dev/null +++ b/src/models/Upgrade.ts @@ -0,0 +1,32 @@ +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; +import { cloneDeep } from "lodash"; + +export class Upgrade { + public settings: pond.UpgradeSettings = pond.UpgradeSettings.create(); + public status: pond.UpgradeStatus = pond.UpgradeStatus.create(); + + public static create(pb?: pond.Upgrade): Upgrade { + let my = new Upgrade(); + if (pb) { + my.settings = pond.UpgradeSettings.fromObject(or(pb.settings, {})); + my.status = pond.UpgradeStatus.fromObject(or(pb.status, {})); + } + return my; + } + + public static any(data: any): Upgrade { + return Upgrade.create(pond.Upgrade.fromObject(cloneDeep(data))); + } + + public static clone(other: Upgrade): Upgrade { + let my = new Upgrade(); + my.settings = pond.UpgradeSettings.fromObject(cloneDeep(other.settings)); + my.status = pond.UpgradeStatus.fromObject(cloneDeep(other.status)); + return my; + } + + public key(): string { + return this.settings.key; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 08f0b54..8568628 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -11,7 +11,7 @@ export * from "./ShareableLink"; export * from "./Site"; export * from "./Tag"; export * from "./Task"; -// export * from "./Upgrade"; +export * from "./Upgrade"; // export * from "./User"; export * from "./user"; export * from "./Field"; diff --git a/src/pages/Firmware.tsx b/src/pages/Firmware.tsx index f6af40f..ecd628a 100644 --- a/src/pages/Firmware.tsx +++ b/src/pages/Firmware.tsx @@ -206,7 +206,7 @@ export default function Firmware() { // }; const load = useCallback(() => { - firmwareAPI.listFirmware(pageSize, pageSize * tablePage, "asc", "version", searchText) + firmwareAPI.listFirmware(pageSize, pageSize * tablePage, "desc", "version", searchText) .then((response) => { setTotal(response.data.total ? response.data.total : 0) let rows: FirmwareModel[] = or(response.data.firmware, []).map(