can provision devices
This commit is contained in:
parent
0ae37f1370
commit
03f8ed20ca
18 changed files with 1467 additions and 17 deletions
403
src/device/ProvisionDevice.tsx
Normal file
403
src/device/ProvisionDevice.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
src/device/SelectDevicePlatform.tsx
Normal file
31
src/device/SelectDevicePlatform.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue