can provision devices

This commit is contained in:
Carter 2024-12-09 17:26:12 -06:00
parent 0ae37f1370
commit 03f8ed20ca
18 changed files with 1467 additions and 17 deletions

View file

@ -0,0 +1,403 @@
import {
Button,
CircularProgress,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Grid2 as Grid,
IconButton,
List,
ListItem,
TextField,
Tooltip,
Typography
} from "@mui/material";
import { FileCopy } from "@mui/icons-material";
import ResponsiveDialog from "common/ResponsiveDialog";
import SearchSelect, { Option } from "common/SearchSelect";
import SelectDevicePlatform from "device/SelectDevicePlatform";
import { useBackpackAPI, useDeviceAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { Backpack, Device } from "models";
import { backpackOptions, genericBackpacks } from "pbHelpers/Backpack";
import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
interface Props {
isOpen: boolean;
refreshCallback: Function;
closeDialogCallback: Function;
}
export default function ProvisionDevice(props: Props) {
const deviceAPI = useDeviceAPI();
const backpackAPI = useBackpackAPI();
const { error, success, warning, info } = useSnackbar();
const { isOpen, closeDialogCallback, refreshCallback } = props;
const [provisioned, setProvisioned] = useState<boolean>(false);
const [idHex, setIdHex] = useState<string>("");
const [keyHex, setKeyHex] = useState<string>("");
const [backpack, setBackpack] = useState<Backpack | undefined>(undefined);
const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [backpacks, setBackpacks] = useState<Backpack[]>([]);
const [command, setCommand] = useState<string>("");
const [deviceLoading, setDeviceLoading] = useState<boolean>(false);
const [platform, setPlatform] = useState<pond.DevicePlatform>(
pond.DevicePlatform.DEVICE_PLATFORM_PHOTON
);
const [device, setDevice] = useState<Device | undefined>(undefined);
const prevIsOpen = usePrevious(isOpen);
const deviceID: number = parseInt(idHex, 16);
const setDefaultState = () => {
setProvisioned(false);
setIdHex("");
setKeyHex("");
setBackpack(undefined);
setName("");
setDescription("");
setLoading(false);
setBackpacks([]);
setCommand("");
setDeviceLoading(false);
setDevice(undefined);
};
useEffect(() => {
if (isOpen && !prevIsOpen) {
setLoading(true);
backpackAPI
.listBackpacks()
.then((response: any) => {
const rawBackpacks = response.data.backpacks;
let updatedBackpacks: Backpack[] = genericBackpacks();
if (rawBackpacks && rawBackpacks.length > 0) {
rawBackpacks.forEach((backpack: any) => {
updatedBackpacks.push(Backpack.any(backpack));
});
}
setLoading(false);
setBackpacks(updatedBackpacks.sort((a, b) => (a.name() > b.name() ? 1 : -1)));
setBackpack(undefined);
})
.catch((err: any) => {
setDefaultState();
error(err ? err : "Error occured while loading device profiles");
})
.finally(() => setLoading(false));
}
}, [backpackAPI, isOpen, prevIsOpen, error]);
useEffect(() => {
if (isOpen && provisioned && !device && !deviceLoading) {
setDeviceLoading(true);
const deviceID = parseInt(idHex, 16);
deviceAPI
.get(deviceID)
.then(response => {
let rDevice = Device.any(response.data);
setDevice(rDevice);
setName(rDevice.name());
setDescription(rDevice.settings.description);
})
.catch(error => {
console.error(error);
setDevice(
Device.create(
pond.Device.create({ settings: pond.DeviceSettings.create({ deviceId: deviceID }) })
)
);
})
.finally(() => setDeviceLoading(false));
}
}, [device, deviceAPI, deviceLoading, idHex, isOpen, provisioned]);
const close = () => {
if (provisioned) {
refreshCallback();
}
closeDialogCallback();
setDefaultState();
};
const generateCommand = (idHex: any, keyHex: any): string => {
if (!idHex || !keyHex) {
return "";
}
return "set creds " + idHex + ":" + keyHex;
};
const assembleBackpack = (): pond.BackpackSettings => {
let assembledBackpack = backpack ? cloneDeep(backpack) : Backpack.create();
let agnosticDevice = assembledBackpack.settings.device
? cloneDeep(assembledBackpack.settings.device)
: ({} as pond.DeviceSettings);
agnosticDevice.deviceId = 0;
agnosticDevice.platform = platform;
agnosticDevice.product = assembledBackpack.settings.product;
assembledBackpack.settings.device = agnosticDevice;
return assembledBackpack.settings;
};
const submitProvision = () => {
setProvisioned(false);
deviceAPI
.add("", "", assembleBackpack())
.then((response: any) => {
let updatedIdHex = response.data.idHex;
let updatedKeyHex = response.data.keyHex;
setIdHex(updatedIdHex);
setKeyHex(updatedKeyHex);
success("Successfully provisioned a new device (id: " + parseInt(updatedIdHex, 16) + ")");
setProvisioned(true);
setCommand(generateCommand(updatedIdHex, updatedKeyHex));
})
.catch((error: any) => {
error.response.data.error
? warning(error.response.data.error)
: error("Error occured while provisioning a new device.");
close();
});
};
const submitDeviceUpdate = () => {
const deviceID = parseInt(idHex, 16);
let settings = pond.DeviceSettings.create(device ? device.settings : { deviceId: deviceID });
settings.name = name;
settings.description = description;
deviceAPI
.update(deviceID, settings)
.then(() => {
success("Device " + deviceID.toString() + " was successfully updated");
})
.catch((error: any) => {
error.response.data.error
? warning(error.response.data.error)
: error("Error occured while updating device " + deviceID.toString() + ".");
close();
})
.finally(() => {
setDevice(undefined);
});
};
const copyID = () => {
navigator.clipboard.writeText(idHex);
info("ID copied to clipboard");
};
const copyKey = () => {
navigator.clipboard.writeText(keyHex);
info("Key copied to clipboard");
};
const copyCommand = () => {
navigator.clipboard.writeText(command + "\n");
info("Command copied to clipboard");
};
const changeBackpack = (option: Option | null) => {
let selectedBackpack = undefined;
if (option) {
const value = Number(option.value);
selectedBackpack = backpacks.find(backpack => {
let idMatch: boolean = backpack.id() === value;
return idMatch && (value === 0 ? backpack.name() === option.label : true);
});
}
setBackpack(selectedBackpack);
};
const content = () => {
if (provisioned) {
return (
<Grid container spacing={1}>
<Grid size={{xs:12}} container justifyContent="center" alignItems="center">
<Grid size={{xs: 11}}>
<TextField
id="id"
name="ID"
label="ID Hex"
disabled
value={idHex}
margin="normal"
variant="outlined"
type="text"
fullWidth
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid size={{xs:1}}>
<Tooltip title="Copy ID">
<IconButton onClick={copyID}>
<FileCopy />
</IconButton>
</Tooltip>
</Grid>
</Grid>
<Grid size={{xs:12}} container justifyContent="center" alignItems="center">
<Grid size={{xs:11}}>
<TextField
id="key"
name="Key"
label="Key Hex"
disabled
value={keyHex}
margin="normal"
variant="outlined"
type="text"
fullWidth
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid size={{xs:1}}>
<Tooltip title="Copy Key">
<IconButton onClick={copyKey}>
<FileCopy />
</IconButton>
</Tooltip>
</Grid>
</Grid>
<Grid size={{xs:12}} container justifyContent="center" alignItems="center">
<Grid size={{xs:11}}>
<TextField
id="command"
name="command"
label="Command"
variant="outlined"
value={command}
disabled
fullWidth
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid size={{xs:1}}>
<Tooltip title="Copy Command">
<IconButton onClick={copyCommand}>
<FileCopy />
</IconButton>
</Tooltip>
</Grid>
</Grid>
{/* {window.NDEFReader && (
<Button
variant="contained"
color="primary"
onClick={() => {
const ndef = new NDEFReader();
ndef
.write({
records: [{ recordType: "text", data: command }]
})
.then(resp => {
success("Comand sent");
})
.catch(error => {
error("Failed to send command");
});
}}>
Write to Device
</Button>
)} */}
<Grid size={{xs:12}}>
<Divider />
</Grid>
{deviceLoading ? (
<CircularProgress />
) : (
<Grid size={{xs:12}}>
<Typography color="textPrimary" variant="body1" gutterBottom>
Update device name/description
</Typography>
<TextField
margin="normal"
id="deviceName"
label="Name"
type="text"
fullWidth
variant="outlined"
value={name}
onChange={(event: any) => setName(event.target.value)}
/>
<TextField
margin="normal"
id="deviceDescription"
label="Description"
type="text"
multiline
rows="4"
// rowsMax="4"
fullWidth
variant="outlined"
value={description}
onChange={(event: any) => setDescription(event.target.value)}
/>
</Grid>
)}
</Grid>
);
}
const selectedBackpack = backpack
? ({ label: backpack.name(), value: backpack.id() } as Option)
: undefined;
return (
<List>
<ListItem>
<SelectDevicePlatform value={platform} onChange={platform => setPlatform(platform)} />
</ListItem>
<ListItem>
<SearchSelect
label="Select a profile to provision with"
selected={selectedBackpack}
options={backpackOptions(backpacks)}
changeSelection={changeBackpack}
loading={loading}
/>
</ListItem>
</List>
);
};
const actions = () => {
return (
<React.Fragment>
<Button onClick={close} color="primary">
Close
</Button>
{!provisioned ? (
<Button onClick={submitProvision} color="primary" disabled={!backpack}>
Provision
</Button>
) : (
<Button onClick={submitDeviceUpdate} color="primary" disabled={!backpack}>
Update
</Button>
)}
</React.Fragment>
);
};
return (
<ResponsiveDialog open={isOpen} onClose={close} aria-labelledby="form-dialog-title" fullWidth>
<DialogTitle id="form-dialog-title">
Provision Device
{provisioned && (
<Typography variant="body2" color="textSecondary">
Device ID: {deviceID}
</Typography>
)}
</DialogTitle>
<DialogContent>{content()}</DialogContent>
<DialogActions>{actions()}</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -0,0 +1,31 @@
import { MenuItem, TextField } from "@mui/material";
import { GetAllDevicePlatformDescribers } from "pbHelpers/DevicePlatform";
import { pond } from "protobuf-ts/pond";
interface Props {
value: pond.DevicePlatform;
onChange: (platform: pond.DevicePlatform) => void;
}
export default function SelectDevicePlatform(props: Props) {
const { value, onChange } = props;
return (
<TextField
id="select-device-platform"
select
label="Device Platform"
value={pond.DevicePlatform[value]}
fullWidth
variant="outlined"
onChange={event =>
onChange(pond.DevicePlatform[event.target.value as keyof typeof pond.DevicePlatform])
}>
{GetAllDevicePlatformDescribers().map(p => (
<MenuItem key={pond.DevicePlatform[p.platform]} value={pond.DevicePlatform[p.platform]}>
{p.label}
</MenuItem>
))}
</TextField>
);
}