345 lines
10 KiB
TypeScript
345 lines
10 KiB
TypeScript
import {
|
||
Button,
|
||
CircularProgress,
|
||
Dialog,
|
||
DialogActions,
|
||
DialogContent,
|
||
DialogContentText,
|
||
DialogTitle,
|
||
Divider,
|
||
IconButton,
|
||
LinearProgress,
|
||
Link,
|
||
Theme,
|
||
Tooltip,
|
||
Typography
|
||
} from "@mui/material";
|
||
// import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||
import UpdateIcon from "@mui/icons-material/Update";
|
||
import { useDeviceAPI, useFirmwareAPI, usePrevious, useSnackbar } from "hooks";
|
||
import { Device, Firmware, latestFirmwareVersion, Upgrade } from "models";
|
||
import moment from "moment";
|
||
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
|
||
import { useGlobalState } from "providers";
|
||
import React, { useCallback, useEffect, useState } from "react";
|
||
import { or } from "utils/types";
|
||
import { makeStyles } from "@mui/styles";
|
||
|
||
const useStyles = makeStyles((theme: Theme) => {
|
||
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 { device } = props;
|
||
const prevDevice = usePrevious(device);
|
||
const [open, setOpen] = useState<boolean>(false);
|
||
const prevOpen = usePrevious(open);
|
||
const [upgrade, setUpgrade] = useState<Upgrade>(Upgrade.create());
|
||
const [gettingStatus, setGettingStatus] = useState(0);
|
||
const [firmware, setFirmware] = useState<Map<string, Firmware>>(new Map())
|
||
const [firmwareLoading, setFirmwareLoading] = useState(false)
|
||
const prevFirmware = usePrevious(firmware);
|
||
|
||
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
|
||
);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (firmwareLoading) return
|
||
// if (firmware.size > 0) return
|
||
setFirmwareLoading(true)
|
||
firmwareAPI.getAllLatestFirmware().then(resp => {
|
||
let versions: Map<string, Firmware> = new Map();
|
||
if (resp && resp.data && resp.data.firmware) {
|
||
resp.data.firmware.forEach((raw: any) => {
|
||
let firmware = Firmware.any(raw);
|
||
let key = firmware.settings.platform + ":" + firmware.settings.channel;
|
||
versions.set(key, firmware);
|
||
});
|
||
}
|
||
// dispatch({ key: "firmware", value: versions });
|
||
setFirmware(versions)
|
||
}).finally(() => {
|
||
setFirmwareLoading(false)
|
||
})
|
||
}, [])
|
||
|
||
const upgradeVersion = () => {
|
||
return upgrade.settings.version;
|
||
};
|
||
|
||
const available = () => {
|
||
if (firmware.size < 1) return false
|
||
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 = () => {
|
||
let progress = Math.round((upgrade.status.offset / upgrade.status.size) * 100)
|
||
return progress;
|
||
};
|
||
|
||
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 (
|
||
<Tooltip title={tooltip()}>
|
||
<IconButton
|
||
color="default"
|
||
className={failed() ? classes.failed : undefined}
|
||
onClick={() => setOpen(true)}
|
||
aria-label={tooltip()}>
|
||
<UpdateIcon />
|
||
{upgrading() && (
|
||
<CircularProgress
|
||
variant="determinate"
|
||
value={percent()}
|
||
size={"2rem"}
|
||
className={classes.buttonProgress}
|
||
/>
|
||
)}
|
||
</IconButton>
|
||
</Tooltip>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<React.Fragment>
|
||
{(available() || upgrading() || failed()) && button()}
|
||
<Dialog fullWidth open={open} onClose={close} aria-labelledby="form-dialog-title">
|
||
{mode() === "prompt" && (
|
||
<React.Fragment>
|
||
<DialogTitle id="form-dialog-title">
|
||
Upgrade Device
|
||
<Typography variant="body2" color="textSecondary">
|
||
{device.name()}
|
||
</Typography>
|
||
</DialogTitle>
|
||
<Divider />
|
||
<DialogContent className={classes.dialogContent}>
|
||
<DialogContentText id="alert-dialog-slide-description">
|
||
Upgrading {device.platformName()} from {currentFirmware()} to {latestFirmware()}
|
||
<br />
|
||
<br />
|
||
It is recommended that the device be plugged in during the upgrade as it will take
|
||
several minutes to complete
|
||
<br />
|
||
<br />
|
||
While the upgrade is in progress you can return to this dialog to view its status
|
||
</DialogContentText>
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={close} color="primary">
|
||
Close
|
||
</Button>
|
||
<Button onClick={handleUpgrade} color="primary">
|
||
Upgrade
|
||
</Button>
|
||
</DialogActions>
|
||
</React.Fragment>
|
||
)}
|
||
{mode() === "progress" && (
|
||
<React.Fragment>
|
||
<DialogTitle id="form-dialog-title">
|
||
Upgrading Device
|
||
<Typography variant="body2" color="textSecondary">
|
||
{device.name()}
|
||
</Typography>
|
||
</DialogTitle>
|
||
<Divider />
|
||
<DialogContent className={classes.dialogContent}>
|
||
<DialogContentText id="alert-dialog-slide-description">
|
||
Upgrade to {latestFirmware()} {upgradeTimingDesc()}
|
||
</DialogContentText>
|
||
<br />
|
||
<LinearProgress variant="determinate" value={percent()} />
|
||
<DialogContentText>
|
||
{or(upgrade.status.offset, 0)} / {upgrade.status.size} bytes ({percent()}%)
|
||
</DialogContentText>
|
||
{failed() && (
|
||
<React.Fragment>
|
||
<DialogContentText>
|
||
<br />
|
||
Uh oh – something went wrong. You can try to{" "}
|
||
<Link variant="body2" onClick={restart} style={{ cursor: "pointer" }}>
|
||
restart
|
||
</Link>{" "}
|
||
the upgrade, if the problem persists we'll be alerted and follow up as soon as
|
||
possible.
|
||
</DialogContentText>
|
||
</React.Fragment>
|
||
)}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={close} color="primary">
|
||
Close
|
||
</Button>
|
||
{upgrading() && (
|
||
<Button onClick={cancel} style={{ color: "red" }}>
|
||
Cancel
|
||
</Button>
|
||
)}
|
||
{failed() && (
|
||
<Button onClick={restart} color="primary">
|
||
Restart
|
||
</Button>
|
||
)}
|
||
</DialogActions>
|
||
</React.Fragment>
|
||
)}
|
||
</Dialog>
|
||
</React.Fragment>
|
||
);
|
||
}
|