405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
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";
|
|
import { useGlobalState } from "providers";
|
|
|
|
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 [{as}] = useGlobalState();
|
|
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, undefined, undefined, undefined, as)
|
|
.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(), as)
|
|
.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, as)
|
|
.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>
|
|
);
|
|
}
|