tag stuff
This commit is contained in:
parent
67ea4cbfdc
commit
3ce00facd1
23 changed files with 1621 additions and 18 deletions
48
src/app/FirmwareLoader.tsx
Normal file
48
src/app/FirmwareLoader.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import './App.css'
|
||||
import LoadingScreen from './LoadingScreen'
|
||||
import { useFirmwareAPI, useGlobalState } from 'providers'
|
||||
import { Firmware } from 'models'
|
||||
|
||||
interface Props {
|
||||
children: any;
|
||||
}
|
||||
|
||||
export default function FirmwareLoader(props: Props) {
|
||||
const { children } = props;
|
||||
const [loading, setLoading] = useState(false)
|
||||
const firmwareAPI = useFirmwareAPI();
|
||||
const [global, dispatch] = useGlobalState();
|
||||
|
||||
const loadFirmware = () => {
|
||||
setLoading(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 });
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
loadFirmware()
|
||||
}, [loading])
|
||||
|
||||
if (loading || !global) return (
|
||||
<LoadingScreen message='Loading firmwares' />
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import { User } from '../models/user'
|
|||
import { Team } from '../models/team'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import { Theme } from '@mui/material'
|
||||
import FirmwareLoader from './FirmwareLoader'
|
||||
|
||||
const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => {
|
||||
return {
|
||||
|
|
@ -45,7 +46,8 @@ const globalDefault = {
|
|||
as: "",
|
||||
showErrors: false,
|
||||
userTeamPermissions: [],
|
||||
backgroundTasksComplete: false
|
||||
backgroundTasksComplete: false,
|
||||
firmware: new Map()
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -68,15 +70,14 @@ export default function UserWrapper(props: Props) {
|
|||
if (hasFetched.current) return;
|
||||
setLoading(true)
|
||||
userAPI.getUserWithTeam(user_id).then(resp => {
|
||||
// console.log(resp.data.team)
|
||||
// console.log(resp.data.user)
|
||||
setGlobal({
|
||||
user: resp.data.user ? User.create(resp.data.user) : User.create(),
|
||||
team: resp.data.team ? Team.create(resp.data.team) : Team.create(),
|
||||
as: "",
|
||||
showErrors: false,
|
||||
userTeamPermissions: [],
|
||||
backgroundTasksComplete: false
|
||||
backgroundTasksComplete: false,
|
||||
firmware: new Map()
|
||||
})
|
||||
}).catch(() => {
|
||||
setGlobal(globalDefault)
|
||||
|
|
@ -98,9 +99,11 @@ export default function UserWrapper(props: Props) {
|
|||
|
||||
return (
|
||||
<StateProvider state={global} reducer={reducer}>
|
||||
<main className={classes.appContent}>
|
||||
<NavigationContainer toggleTheme={toggleTheme} />
|
||||
</main>
|
||||
<FirmwareLoader>
|
||||
<main className={classes.appContent}>
|
||||
<NavigationContainer toggleTheme={toggleTheme} />
|
||||
</main>
|
||||
</FirmwareLoader>
|
||||
</StateProvider>
|
||||
)
|
||||
}
|
||||
42
src/common/ColourPicker.tsx
Normal file
42
src/common/ColourPicker.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { TwitterPicker } from "react-color";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
|
||||
interface Props {
|
||||
colour?: string;
|
||||
onChange: (colour: string) => void;
|
||||
}
|
||||
|
||||
export default function ColourPicker(props: Props) {
|
||||
const { colour, onChange } = props;
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }: any) => (
|
||||
<TwitterPicker
|
||||
color={colour}
|
||||
onChangeComplete={colour => onChange(colour.hex)}
|
||||
width={width}
|
||||
triangle="hide"
|
||||
colors={[
|
||||
"#f44336",
|
||||
"#e91e63",
|
||||
"#9c27b0",
|
||||
"#673ab7",
|
||||
"#3f51b5",
|
||||
"#2196f3",
|
||||
"#03a9f4",
|
||||
"#00bcd4",
|
||||
"#009688",
|
||||
"#4caf50",
|
||||
"#8bc34a",
|
||||
"#cddc39",
|
||||
"#ffeb3b",
|
||||
"#ffc107",
|
||||
"#ff9800",
|
||||
"#ff5722"
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
20
src/common/StatusChip.tsx
Normal file
20
src/common/StatusChip.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Chip, Tooltip } from "@mui/material";
|
||||
import { getStatusHelper } from "pbHelpers/Status";
|
||||
|
||||
interface Props {
|
||||
status: string;
|
||||
size?: "small" | "medium";
|
||||
}
|
||||
|
||||
export default function StatusChip(props: Props) {
|
||||
const { status, size } = props;
|
||||
const statusHelper = getStatusHelper(status);
|
||||
const icon = statusHelper.icon;
|
||||
const description = statusHelper.description;
|
||||
|
||||
return icon !== null ? (
|
||||
<Tooltip title="Your most recent changes will be synced to the device as soon as possible">
|
||||
<Chip variant="outlined" label={description} icon={icon} size={size} />
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
62
src/common/Tag.tsx
Normal file
62
src/common/Tag.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Chip, Theme } from "@mui/material";
|
||||
import { createStyles, makeStyles } from "@mui/styles";
|
||||
import DeleteIcon from "@mui/icons-material/Cancel";
|
||||
import { Tag as TagModel } from "models";
|
||||
|
||||
const useStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
deleteIconLightBG: {
|
||||
color: "rgba(0, 0, 0, 0.26)",
|
||||
"&:hover": {
|
||||
color: "rgba(0, 0, 0, 0.4)"
|
||||
}
|
||||
},
|
||||
deleteIconDarkBG: {
|
||||
color: "rgba(255, 255, 255, 0.26)",
|
||||
"&:hover": {
|
||||
color: "rgba(255, 255, 255, 0.4)"
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
interface Props {
|
||||
tag?: TagModel;
|
||||
onClick?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export function Tag(props: Props) {
|
||||
const classes = useStyles();
|
||||
const { tag, onClick, onDelete } = props;
|
||||
const colour = tag ? tag.settings.colour : undefined;
|
||||
|
||||
const getBrightnessLevel = () => {
|
||||
if (!colour) return null;
|
||||
const hex = colour.replace("#", "");
|
||||
const c_r = parseInt(hex.substr(0, 2), 16);
|
||||
const c_g = parseInt(hex.substr(2, 2), 16);
|
||||
const c_b = parseInt(hex.substr(4, 2), 16);
|
||||
const brightness = (c_r * 299 + c_g * 587 + c_b * 114) / 1000;
|
||||
return brightness;
|
||||
};
|
||||
|
||||
const brightness = getBrightnessLevel();
|
||||
const isLightBG = brightness !== null && brightness > 169;
|
||||
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={tag ? tag.name() : "Unknown"}
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
style={{
|
||||
backgroundColor: colour,
|
||||
color: isLightBG ? "#000" : "#FFF"
|
||||
}}
|
||||
deleteIcon={
|
||||
<DeleteIcon className={isLightBG ? classes.deleteIconLightBG : classes.deleteIconDarkBG} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
233
src/common/TagSettings.tsx
Normal file
233
src/common/TagSettings.tsx
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Grid2 as Grid,
|
||||
TextField,
|
||||
Theme
|
||||
} from "@mui/material";
|
||||
// import { red } from "@mui/icons-materia";
|
||||
// import { createStyles, makeStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { Delete as DeleteIcon } from "@mui/icons-material";
|
||||
import ColourPicker from "common/ColourPicker";
|
||||
import { Tag as TagModel } from "models";
|
||||
import { useGlobalState, useTagAPI, useSnackbar } from "providers";
|
||||
import { useState } from "react";
|
||||
import { Tag as TagUI } from "./Tag";
|
||||
import { red } from "@mui/material/colors";
|
||||
import { makeStyles, withStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
gutter: {
|
||||
marginBottom: theme.spacing(1),
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
marginBottom: theme.spacing(2)
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
interface ConfirmDeleteTagProps {
|
||||
open: boolean;
|
||||
onClose: (deleted?: boolean) => void;
|
||||
tag: TagModel;
|
||||
}
|
||||
|
||||
function ConfirmDeleteTag(props: ConfirmDeleteTagProps) {
|
||||
const { tag, open, onClose } = props;
|
||||
const { error, success } = useSnackbar();
|
||||
// const [{ tags }, dispatch] = useGlobalState();
|
||||
const tagAPI = useTagAPI();
|
||||
const tagName = tag.name();
|
||||
|
||||
const onSubmit = () => {
|
||||
if (tag && tag.settings.key) {
|
||||
tagAPI
|
||||
.removeTag(tag.settings.key)
|
||||
.then(() => {
|
||||
// dispatch({ key: "tags", value: tags.filter(t => t.settings.key !== tag.settings.key) });
|
||||
success("Tag was successfully deleted");
|
||||
onClose(true);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
error(err && err.message ? err.message : "Error occured while removing the tag");
|
||||
onClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => onClose()}
|
||||
aria-labelledby="confirm-delete-tag-title"
|
||||
aria-describedby="confirm-delete-tag-description">
|
||||
<DialogTitle id="confirm-delete-tag-title">Delete the tag {tagName}?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="confirm-delete-tag-description">
|
||||
By clicking 'Submit', the tag {tagName} will be deleted; anything associated with it will
|
||||
longer be able to access it.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => onClose()} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onSubmit()} color="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
tag?: TagModel;
|
||||
mode: "add" | "update";
|
||||
}
|
||||
|
||||
const DeleteButton = withStyles((theme: Theme) => ({
|
||||
root: {
|
||||
color: "#FFF",
|
||||
backgroundColor: red[500],
|
||||
"&:hover": {
|
||||
backgroundColor: red[700]
|
||||
}
|
||||
}
|
||||
}))(Button);
|
||||
|
||||
export default function TagSettings(props: Props) {
|
||||
const classes = useStyles();
|
||||
const { open, mode, onClose } = props;
|
||||
const { error, success } = useSnackbar();
|
||||
const tagAPI = useTagAPI();
|
||||
const [tag, setTag] = useState<TagModel>(TagModel.clone(props.tag));
|
||||
const [confirmDeleteTagOpen, setConfirmDeleteTagOpen] = useState(false);
|
||||
// const [{ tags }, dispatch] = useGlobalState();
|
||||
|
||||
const submit = () => {
|
||||
switch (mode) {
|
||||
case "add":
|
||||
tagAPI
|
||||
.addTag(tag.settings)
|
||||
.then((response: any) => {
|
||||
let newKey = response && response.data && response.data.key ? response.data.key : null;
|
||||
if (newKey) {
|
||||
tag.settings.key = newKey;
|
||||
}
|
||||
// dispatch({ key: "tags", value: [...tags, tag] });
|
||||
success("Successfully created a new tag");
|
||||
handleClose(true);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
error(err && err.message ? err.message : "Error occured while create a new tag");
|
||||
handleClose();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
tagAPI
|
||||
.updateTag(tag.settings.key, tag.settings)
|
||||
.then((response: any) => {
|
||||
// dispatch({
|
||||
// key: "tags",
|
||||
// value: tags.map(t => {
|
||||
// if (t.settings.key !== tag.settings.key) {
|
||||
// return t;
|
||||
// }
|
||||
// return tag;
|
||||
// })
|
||||
// });
|
||||
success("Successfully updated " + tag.name());
|
||||
handleClose(true);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
error(err && err.message ? err.message : "Error occured while updating the tag");
|
||||
handleClose();
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const onConfirmDeleteTag = (tagDeleted?: boolean) => {
|
||||
if (tagDeleted) {
|
||||
onClose();
|
||||
} else {
|
||||
setConfirmDeleteTagOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (refresh?: boolean) => {
|
||||
if (mode === "add") {
|
||||
setTag(new TagModel());
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => handleClose()}>
|
||||
<DialogTitle>
|
||||
<TagUI tag={tag} />
|
||||
</DialogTitle>
|
||||
<DialogContent className={classes.gutter}>
|
||||
<TextField
|
||||
label="Label"
|
||||
value={tag.settings.name}
|
||||
onChange={event => {
|
||||
let newTag = TagModel.clone(tag);
|
||||
newTag.settings.name = event.target.value;
|
||||
setTag(newTag);
|
||||
}}
|
||||
placeholder="Enter a label for your tag"
|
||||
variant="outlined"
|
||||
className={classes.gutter}
|
||||
/>
|
||||
|
||||
<ColourPicker
|
||||
colour={tag.settings.colour}
|
||||
onChange={colour => {
|
||||
tag.settings.colour = colour;
|
||||
let newTag = TagModel.clone(tag);
|
||||
newTag.settings.colour = colour;
|
||||
setTag(newTag);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container justifyContent="space-between" alignItems="center">
|
||||
<Grid>
|
||||
{mode !== "add" && (
|
||||
<DeleteButton
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setConfirmDeleteTagOpen(true)}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</DeleteButton>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Button onClick={() => handleClose()} color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={() => submit()} color="primary">
|
||||
{mode === "add" ? "Create" : "Update"}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
{mode !== "add" && (
|
||||
<ConfirmDeleteTag
|
||||
open={confirmDeleteTagOpen}
|
||||
onClose={tagDeleted => onConfirmDeleteTag(tagDeleted)}
|
||||
tag={tag}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
238
src/device/DeviceOverview.tsx
Normal file
238
src/device/DeviceOverview.tsx
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/material";
|
||||
import { DataUsage, SimCard, Launch } from "@mui/icons-material";
|
||||
import StatusChip from "common/StatusChip";
|
||||
import DeviceTags from "device/DeviceTags";
|
||||
import VersionChip from "device/VersionChip";
|
||||
import { useSnackbar, useWidth, usePrevious } from "hooks";
|
||||
import { Device, latestFirmwareVersion, Component } from "models";
|
||||
import moment from "moment";
|
||||
import { describeConnectivity } from "pbHelpers/Connectivity";
|
||||
import { describePower } from "pbHelpers/Power";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import { useGlobalState } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// import { useHistory, useRouteMatch } from "react-router";
|
||||
import { notNull, or } from "utils/types";
|
||||
// import { MatchParams } from "navigation/Routes";
|
||||
// import DeviceHologram from "./DeviceHologram";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const useStyles = makeStyles((_theme: Theme) => {
|
||||
return ({
|
||||
chipContainer: {
|
||||
position: "relative",
|
||||
maxWidth: "100%",
|
||||
overflowX: "auto"
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
interface Usage {
|
||||
status: string;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
device: Device;
|
||||
components: Component[];
|
||||
usage?: Usage;
|
||||
loading?: boolean;
|
||||
disableAddTag?: boolean;
|
||||
}
|
||||
|
||||
export default function DeviceOverview(props: Props) {
|
||||
const [{ user, firmware }] = useGlobalState();
|
||||
const { device, components, usage, loading, disableAddTag } = props;
|
||||
const prevComponents = usePrevious(components);
|
||||
const { info } = useSnackbar();
|
||||
const classes = useStyles();
|
||||
const width = useWidth();
|
||||
const now = moment();
|
||||
const navigate = useNavigate();
|
||||
// const match = useRouteMatch<MatchParams>();
|
||||
const [powerComponent, setPowerComponent] = useState<Component | undefined | null>();
|
||||
const [modemComponent, setModemComponent] = useState<Component | undefined | null>();
|
||||
// const [hologramDialog, setHologramDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (components !== prevComponents) {
|
||||
let updatedPower = null;
|
||||
let updatedModem = null;
|
||||
components.forEach(c => {
|
||||
switch (c.settings.type) {
|
||||
case quack.ComponentType.COMPONENT_TYPE_POWER:
|
||||
updatedPower = c;
|
||||
break;
|
||||
case quack.ComponentType.COMPONENT_TYPE_MODEM:
|
||||
updatedModem = c;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
setPowerComponent(updatedPower);
|
||||
setModemComponent(updatedModem);
|
||||
}
|
||||
}, [components, prevComponents]);
|
||||
|
||||
const pathToDevice = () => {
|
||||
const groupID = parseInt(useParams().groupID ?? "", 10);
|
||||
// const groupID: number = parseInt(match.params.groupID, 10);
|
||||
const groupPath: string = groupID > 0 ? "/groups/" + groupID.toString() : "";
|
||||
const devicePath: string = "/devices/" + device.settings.deviceId.toString();
|
||||
return groupPath + devicePath;
|
||||
};
|
||||
|
||||
const copySim = (sim: string) => {
|
||||
navigator.clipboard.writeText(sim);
|
||||
info("SIM copied to clipboard");
|
||||
};
|
||||
|
||||
const chips = () => {
|
||||
const currentVersion = device.status.firmwareVersion;
|
||||
const hasVersion: boolean = currentVersion && currentVersion !== "" ? true : false;
|
||||
const connectivity = describeConnectivity(
|
||||
device.settings.platform,
|
||||
device.status.lastActive,
|
||||
device.settings.pondCheckPeriodS,
|
||||
now
|
||||
);
|
||||
const latestFirmware = latestFirmwareVersion(
|
||||
firmware,
|
||||
device.settings.platform,
|
||||
device.settings.upgradeChannel
|
||||
);
|
||||
const power = describePower(
|
||||
pond.DevicePower.create(device.status.power ? device.status.power : undefined)
|
||||
);
|
||||
const iccid = device.status.sim;
|
||||
let usageDesc = "";
|
||||
if (usage && isNaN(usage.bytes)) {
|
||||
usageDesc = "0 B";
|
||||
} else if (usage && usage.bytes < 1000) {
|
||||
usageDesc = usage.bytes + " B";
|
||||
} else if (usage && usage.bytes < 1000000) {
|
||||
usageDesc = (usage.bytes / 1000.0).toFixed(1) + " KB";
|
||||
} else if (usage && usage.bytes < 1000000000) {
|
||||
usageDesc = (usage.bytes / 1000000.0).toFixed(1) + " MB";
|
||||
} else if (usage) {
|
||||
usageDesc = (usage.bytes / 1000000000.0).toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
let isPaused = false;
|
||||
if (
|
||||
usage &&
|
||||
or(usage.status, "")
|
||||
.toLowerCase()
|
||||
.includes("pause")
|
||||
) {
|
||||
isPaused = true;
|
||||
}
|
||||
let isCellular = true;
|
||||
let canHologram = user.hasFeature("billing");
|
||||
// device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON ||
|
||||
// device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR;
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
wrap={width === "xs" ? "nowrap" : "wrap"}
|
||||
className={width === "xs" ? classes.chipContainer : undefined}>
|
||||
{device.settings.platform > 0 && (
|
||||
<Grid>
|
||||
<Tooltip title={connectivity.tooltip}>
|
||||
<Chip
|
||||
variant={modemComponent ? "filled" : "outlined"}
|
||||
clickable={modemComponent !== null}
|
||||
onClick={() => {
|
||||
if (modemComponent && modemComponent.key()) {
|
||||
navigate(pathToDevice() + "/components/" + modemComponent.key());
|
||||
}
|
||||
}}
|
||||
label={connectivity.description}
|
||||
icon={connectivity.icon}
|
||||
onDelete={() => {
|
||||
if (modemComponent && modemComponent.key()) {
|
||||
navigate(pathToDevice() + "/components/" + modemComponent.key());
|
||||
}
|
||||
}}
|
||||
deleteIcon={modemComponent ? <Launch /> : <React.Fragment />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
{notNull(device.status.power) && (
|
||||
<Grid>
|
||||
<Tooltip title={power.description}>
|
||||
<Chip
|
||||
variant={powerComponent ? "filled" : "outlined"}
|
||||
clickable={powerComponent !== null}
|
||||
onClick={() => {
|
||||
if (powerComponent && powerComponent.key()) {
|
||||
navigate(pathToDevice() + "/components/" + powerComponent.key());
|
||||
}
|
||||
}}
|
||||
label={power.description}
|
||||
icon={power.icon}
|
||||
onDelete={() => {
|
||||
if (powerComponent && powerComponent.key()) {
|
||||
navigate(pathToDevice() + "/components/" + powerComponent.key());
|
||||
}
|
||||
}}
|
||||
deleteIcon={powerComponent ? <Launch /> : <React.Fragment />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{hasVersion && (
|
||||
<Grid>
|
||||
<VersionChip version={currentVersion} available={latestFirmware} />
|
||||
</Grid>
|
||||
)}
|
||||
{isCellular && iccid && (
|
||||
<Grid>
|
||||
<Tooltip title={isPaused ? "Data paused" : "Data enabled"}>
|
||||
<Chip
|
||||
onClick={() => copySim(iccid)}
|
||||
label={"SIM " + iccid.substring(iccid.length - 5)}
|
||||
icon={<SimCard />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
{isCellular && usage && usage.bytes > 0 && (
|
||||
<Grid>
|
||||
<Tooltip title="Estimated usage in the past 24 hours">
|
||||
<Chip
|
||||
variant="outlined"
|
||||
label={usageDesc}
|
||||
icon={<DataUsage />}
|
||||
clickable={canHologram}
|
||||
onClick={() => {
|
||||
// if (canHologram) setHologramDialog(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
)}
|
||||
{!device.status.synced && (
|
||||
<Grid>
|
||||
<StatusChip status="pending" />
|
||||
</Grid>
|
||||
)}
|
||||
{user.allowedTo("provision") && <DeviceTags device={device} disableAdd={disableAddTag} />}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{loading ? <Skeleton variant="text" width={width === "xs" ? 250 : 350} /> : chips()}
|
||||
{/* <DeviceHologram device={device} open={hologramDialog} setOpen={setHologramDialog} /> */}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
179
src/device/DeviceTags.tsx
Normal file
179
src/device/DeviceTags.tsx
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Theme,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { AddCircleOutline as AddIconCircle } from "@mui/icons-material";
|
||||
import SearchBar from "common/SearchBar";
|
||||
import { Tag as TagUI } from "common/Tag";
|
||||
import TagSettings from "common/TagSettings";
|
||||
import { Device, Tag } from "models";
|
||||
import { filterByTag } from "pbHelpers/Tag";
|
||||
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
addIcon: {
|
||||
color: "var(--status-ok)"
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
interface AddDeviceTagProps {
|
||||
device: Device;
|
||||
deviceTags: string[];
|
||||
addTagToDevice: (tag: Tag) => void;
|
||||
}
|
||||
|
||||
function AddDeviceTag(props: AddDeviceTagProps) {
|
||||
const classes = useStyles();
|
||||
const { device, deviceTags, addTagToDevice } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [createNewOpen, setCreateNewOpen] = useState(false);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
// const [{ tags }] = useGlobalState();
|
||||
const [tags, setTags] = useState<Tag[]>([])
|
||||
|
||||
const tagItems = tags
|
||||
.filter(tag => !deviceTags.some(key => key === tag.settings.key))
|
||||
.filter(tag => filterByTag(searchValue, tag))
|
||||
.sort((a, b) => (a.name().toLowerCase() > b.name().toLowerCase() ? 1 : -1))
|
||||
.map((tag, index, array) => (
|
||||
<ListItem key={tag.settings.key} divider={index === array.length - 1}>
|
||||
<TagUI tag={tag} onClick={() => addTagToDevice(tag)} />
|
||||
</ListItem>
|
||||
));
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton aria-label="add tag" onClick={() => setOpen(true)} size="small">
|
||||
<AddIconCircle className={classes.addIcon} />
|
||||
</IconButton>
|
||||
<Dialog open={open} onClose={() => setOpen(false)} scroll="paper">
|
||||
<DialogTitle>
|
||||
Select tags to add
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{device.name()}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<SearchBar
|
||||
value={searchValue}
|
||||
onChange={(newSearchValue: string) => setSearchValue(newSearchValue)}
|
||||
/>
|
||||
<List>
|
||||
{tagItems.length > 0 ? (
|
||||
<React.Fragment>{tagItems}</React.Fragment>
|
||||
) : (
|
||||
<ListItem divider>
|
||||
<ListItemText secondary={"No tags found"} />
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem onClick={() => setCreateNewOpen(true)}>
|
||||
<ListItemIcon>
|
||||
<AddIconCircle className={classes.addIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Create tag</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<TagSettings open={createNewOpen} mode="add" onClose={() => setCreateNewOpen(false)} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeviceTagProps {
|
||||
tag: Tag;
|
||||
removeTagFromDevice: (tag: Tag) => void;
|
||||
}
|
||||
|
||||
function DeviceTag(props: DeviceTagProps) {
|
||||
const { tag, removeTagFromDevice } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TagUI tag={tag} onClick={() => setOpen(true)} onDelete={() => removeTagFromDevice(tag)} />
|
||||
<TagSettings open={open} onClose={() => setOpen(false)} tag={tag} mode="update" />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface DeviceTagsProps {
|
||||
device: Device;
|
||||
disableAdd?: boolean;
|
||||
}
|
||||
|
||||
export default function DeviceTags(props: DeviceTagsProps) {
|
||||
const { device, disableAdd } = props;
|
||||
// const [{ tags }] = useGlobalState();
|
||||
const [tags, setTags] = useState<Tag[]>([])
|
||||
const deviceAPI = useDeviceAPI();
|
||||
const { error } = useSnackbar();
|
||||
const [deviceTags, setDeviceTags] = useState<string[]>(device.status.tagKeys);
|
||||
const previousDeviceRef = useRef(device);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousDeviceRef.current !== device) {
|
||||
setDeviceTags(device.status.tagKeys);
|
||||
previousDeviceRef.current = device;
|
||||
}
|
||||
}, [device]);
|
||||
|
||||
const addTag = (tag: Tag) => {
|
||||
if (!deviceTags.some(dt => dt === tag.key())) {
|
||||
deviceAPI
|
||||
.tag(device.id(), tag.key())
|
||||
.then(() => {
|
||||
setDeviceTags([...deviceTags, tag.key()]);
|
||||
})
|
||||
.catch(() => error("Failed to tag device as " + tag.name()));
|
||||
}
|
||||
};
|
||||
|
||||
const removeTagFromDevice = (tag: Tag) => {
|
||||
if (deviceTags.some(dt => dt === tag.key())) {
|
||||
deviceAPI
|
||||
.untag(device.id(), tag.key())
|
||||
.then(() => {
|
||||
setDeviceTags(deviceTags.filter(t => tag.key() !== t));
|
||||
})
|
||||
.catch(() => error("Failed to remove tag " + tag.name() + " from device"));
|
||||
}
|
||||
};
|
||||
|
||||
if (!device || !tags) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{deviceTags.map(key => {
|
||||
let tag = tags.find(t => t.settings.key === key);
|
||||
if (!tag) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Grid key={tag.settings.key}>
|
||||
<DeviceTag tag={tag} removeTagFromDevice={removeTagFromDevice} />
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
{disableAdd === undefined || disableAdd === false ? (
|
||||
<Grid>
|
||||
<AddDeviceTag device={device} deviceTags={deviceTags} addTagToDevice={addTag} />
|
||||
</Grid>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
33
src/device/VersionChip.tsx
Normal file
33
src/device/VersionChip.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Chip, Theme, Tooltip } from "@mui/material";
|
||||
// import { Theme } from "@material-ui/core/styles/createMuiTheme";
|
||||
// import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
|
||||
import React from "react";
|
||||
import { getFirmwareVersionHelper } from "pbHelpers/FirmwareVersion";
|
||||
import { withStyles, WithStyles } from "@mui/styles";
|
||||
|
||||
const styles = (_theme: Theme) => ({});
|
||||
|
||||
interface Props extends WithStyles<typeof styles> {
|
||||
version: string;
|
||||
available: string;
|
||||
}
|
||||
|
||||
interface State {}
|
||||
|
||||
class VersionChip extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { version, available } = this.props;
|
||||
const firmwareVersionHelper = getFirmwareVersionHelper(version, available);
|
||||
return (
|
||||
<Tooltip title={firmwareVersionHelper.tooltip}>
|
||||
<Chip
|
||||
variant="outlined"
|
||||
label={firmwareVersionHelper.description}
|
||||
icon={firmwareVersionHelper.icon}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(VersionChip);
|
||||
107
src/models/Component.ts
Normal file
107
src/models/Component.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { quack } from "protobuf-ts/pond";
|
||||
import { or } from "utils/types";
|
||||
import { cloneDeep } from "lodash";
|
||||
// import { getSubtypes } from "pbHelpers/ComponentType";
|
||||
|
||||
export class Component {
|
||||
public settings: pond.ComponentSettings = pond.ComponentSettings.create();
|
||||
public status: pond.ComponentStatus = pond.ComponentStatus.create();
|
||||
public lastMeasurement: pond.UnitMeasurementsForComponent[] = [];
|
||||
public lastGoodMeasurement: pond.UnitMeasurementsForComponent[] = [];
|
||||
public permissions: pond.Permission[] = [];
|
||||
|
||||
public static create(pb?: pond.Component): Component {
|
||||
let my = new Component();
|
||||
if (pb) {
|
||||
my.settings = pond.ComponentSettings.fromObject(
|
||||
or(pb.settings, pond.ComponentSettings.create())
|
||||
);
|
||||
my.status = pond.ComponentStatus.fromObject(or(pb.status, pond.ComponentStatus.create()));
|
||||
my.lastMeasurement = pb.lastMeasurement ?? pond.UnitMeasurementsForComponent.create();
|
||||
//my.lastGoodMeasurement = pb.status?.lastGoodMeasurement ?? pond.UnitMeasurementsForComponent.create();
|
||||
my.permissions = pb.permissions ?? [];
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public static clone(other?: Component): Component {
|
||||
if (other) {
|
||||
return Component.create(
|
||||
pond.Component.fromObject(cloneDeep({ settings: other.settings, status: other.status }))
|
||||
);
|
||||
}
|
||||
return Component.create();
|
||||
}
|
||||
|
||||
public static any(data: any): Component {
|
||||
let my = Component.create(pond.Component.fromObject(cloneDeep(data)));
|
||||
if (data && data.status && data.status.lastMeasurement) {
|
||||
my.status.lastMeasurement = data.status.lastMeasurement;
|
||||
}
|
||||
if (data && data.status && data.status.lastGoodMeasurement) {
|
||||
my.status.lastGoodMeasurement = [];
|
||||
data.status.lastGoodMeasurement.forEach((m: any) => {
|
||||
my.status.lastGoodMeasurement.push(m);
|
||||
});
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public update(other: Component) {
|
||||
this.settings = other.settings;
|
||||
this.status = other.status;
|
||||
}
|
||||
|
||||
public name(): string {
|
||||
return this.settings.name !== "" ? this.settings.name : "Component " + this.key();
|
||||
}
|
||||
|
||||
public key(): string {
|
||||
return this.settings.key;
|
||||
}
|
||||
|
||||
public location(): quack.ComponentID {
|
||||
return quack.ComponentID.fromObject({
|
||||
type: this.settings.type,
|
||||
addressType: this.settings.addressType,
|
||||
address: this.settings.address
|
||||
});
|
||||
}
|
||||
|
||||
public locationString(): string {
|
||||
return (
|
||||
or(this.settings.type, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.addressType, 0).toString() +
|
||||
"-" +
|
||||
or(this.settings.address, 0).toString()
|
||||
);
|
||||
}
|
||||
|
||||
public type(): quack.ComponentType {
|
||||
return this.settings.type;
|
||||
}
|
||||
|
||||
public subType(): number {
|
||||
return this.settings.subtype;
|
||||
}
|
||||
|
||||
// public subTypeName(): string {
|
||||
// let subtypes = getSubtypes(this.settings.type);
|
||||
// let subName = "Default";
|
||||
// subtypes.forEach(sub => {
|
||||
// if (this.settings.subtype === sub.key) {
|
||||
// subName = sub.friendlyName;
|
||||
// }
|
||||
// });
|
||||
// return subName;
|
||||
// }
|
||||
|
||||
public numNodes(): number {
|
||||
if (this.lastMeasurement[0] && this.lastMeasurement[0].values[0]) {
|
||||
return this.lastMeasurement[0].values[0].values.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
42
src/models/Firmware.ts
Normal file
42
src/models/Firmware.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { or } from "utils/types";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
export function latestFirmwareVersion(
|
||||
versions: Map<string, Firmware>,
|
||||
platform: pond.DevicePlatform,
|
||||
channel: pond.UpgradeChannel
|
||||
): string {
|
||||
const firmware = versions.get(platform + ":" + channel);
|
||||
if (firmware) {
|
||||
return firmware.settings.version;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export class Firmware {
|
||||
public settings: pond.FirmwareSettings = pond.FirmwareSettings.create();
|
||||
public status: pond.FirmwareStatus = pond.FirmwareStatus.create();
|
||||
|
||||
public static create(pb?: pond.Firmware): Firmware {
|
||||
let my = new Firmware();
|
||||
if (pb) {
|
||||
my.settings = pond.FirmwareSettings.fromObject(cloneDeep(or(pb.settings, {})));
|
||||
my.status = pond.FirmwareStatus.fromObject(cloneDeep(or(pb.status, {})));
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public static clone(other?: Firmware): Firmware {
|
||||
let my = new Firmware();
|
||||
if (other) {
|
||||
my.settings = pond.FirmwareSettings.fromObject(cloneDeep(other.settings));
|
||||
my.status = pond.FirmwareStatus.fromObject(cloneDeep(other.status));
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public static any(data: any): Firmware {
|
||||
return Firmware.create(pond.Firmware.fromObject(cloneDeep(data)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
export * from "./Backpack";
|
||||
// export * from "./Bin";
|
||||
// export * from "./BinYard";
|
||||
// export * from "./Component";
|
||||
export * from "./Component";
|
||||
export * from "./Device";
|
||||
// export * from "./Firmware";
|
||||
export * from "./Firmware";
|
||||
export * from "./Group";
|
||||
// export * from "./Interaction";
|
||||
export * from "./Scope";
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import SmartBreadcrumb from "common/SmartBreadcrumb";
|
|||
import DeviceActions from "common/DeviceActions";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { cloneDeep } from "lodash";
|
||||
import DeviceOverview from "device/DeviceOverview";
|
||||
|
||||
// const useStyles = makeStyles((theme: Theme) => {
|
||||
// // const isMobile = useMobile()
|
||||
|
|
@ -31,6 +32,9 @@ export default function DevicePage() {
|
|||
const [permissions, setPermissions] = useState<pond.Permission[]>([])
|
||||
const [preferences, setPreferences] = useState<pond.DevicePreferences>(pond.DevicePreferences.create())
|
||||
|
||||
const [cellularUsage, setCellularUsage] = useState<number>(0);
|
||||
const [cellularStatus, setCellularStatus] = useState<string>("");
|
||||
|
||||
const loadDevice = () => {
|
||||
if (loading) return
|
||||
// if (state?.device && parseInt(state.device.settings.deviceId) === device.id()) return
|
||||
|
|
@ -96,6 +100,14 @@ export default function DevicePage() {
|
|||
<LoadingScreen message="Loading device"/>
|
||||
)
|
||||
|
||||
const getUsage = () => {
|
||||
let usage = undefined;
|
||||
if (cellularStatus && cellularStatus !== "") {
|
||||
usage = { status: cellularStatus, bytes: cellularUsage };
|
||||
}
|
||||
return usage;
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer padding={isMobile ? 0 : 2}>
|
||||
{/* <Typography>
|
||||
|
|
@ -117,6 +129,13 @@ export default function DevicePage() {
|
|||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<DeviceOverview
|
||||
device={device}
|
||||
// components={[...components.values()]}
|
||||
components={[]}
|
||||
usage={getUsage()}
|
||||
loading={loading}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
100
src/pbHelpers/Connectivity.tsx
Normal file
100
src/pbHelpers/Connectivity.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import {
|
||||
SignalCellular4Bar,
|
||||
SignalCellularOff,
|
||||
SignalWifi4Bar,
|
||||
SignalWifiOff
|
||||
} from "@mui/icons-material";
|
||||
import moment from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { getTextSecondary } from "theme/text";
|
||||
|
||||
export interface ConnectivityDescriber {
|
||||
icon: any;
|
||||
description: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
export enum Activity {
|
||||
unknown = 0,
|
||||
active = 1,
|
||||
away = 2,
|
||||
missing = 3
|
||||
}
|
||||
|
||||
function getActivity(lastActive: string, checkInPeriod: number, now: moment.Moment): Activity {
|
||||
let secondsSinceLastActive = moment.duration(now.diff(moment(lastActive))).asSeconds();
|
||||
if (secondsSinceLastActive > checkInPeriod * 3) {
|
||||
return Activity.missing;
|
||||
} else if (secondsSinceLastActive > checkInPeriod) {
|
||||
return Activity.away;
|
||||
} else if (isNaN(secondsSinceLastActive)) {
|
||||
return Activity.unknown;
|
||||
}
|
||||
return Activity.active;
|
||||
}
|
||||
|
||||
function getColour(activity: Activity): string {
|
||||
switch (activity) {
|
||||
case Activity.active:
|
||||
return "var(--status-ok)";
|
||||
case Activity.away:
|
||||
return "var(--status-risk)";
|
||||
case Activity.missing:
|
||||
return getTextSecondary();
|
||||
default:
|
||||
return "var(--status-unknown)";
|
||||
}
|
||||
}
|
||||
|
||||
function getIcon(platform: pond.DevicePlatform, activity: Activity): JSX.Element {
|
||||
let colour = getColour(activity);
|
||||
if (
|
||||
platform === pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON ||
|
||||
platform === pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR
|
||||
) {
|
||||
if (activity === Activity.missing) {
|
||||
return <SignalCellularOff style={{ color: colour }} />;
|
||||
}
|
||||
return <SignalCellular4Bar style={{ color: colour }} />;
|
||||
}
|
||||
if (activity === Activity.missing) {
|
||||
return <SignalWifiOff style={{ color: colour }} />;
|
||||
}
|
||||
return <SignalWifi4Bar style={{ color: colour }} />;
|
||||
}
|
||||
|
||||
function getTooltip(platform: pond.DevicePlatform): string {
|
||||
switch (platform) {
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON ||
|
||||
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR ||
|
||||
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK ||
|
||||
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
|
||||
return "Communicates via Cellular";
|
||||
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON ||
|
||||
pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
|
||||
return "Communicates via Wi-Fi";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getDescription(lastActive: string): string {
|
||||
if (lastActive === "") {
|
||||
return "Never active";
|
||||
}
|
||||
return "Active " + moment(lastActive).fromNow();
|
||||
}
|
||||
|
||||
export function describeConnectivity(
|
||||
platform: pond.DevicePlatform,
|
||||
lastActive: string,
|
||||
checkInPeriod: number,
|
||||
now: moment.Moment
|
||||
): ConnectivityDescriber {
|
||||
let activity = getActivity(lastActive, checkInPeriod, now);
|
||||
return {
|
||||
icon: getIcon(platform, activity),
|
||||
description: getDescription(lastActive),
|
||||
tooltip: getTooltip(platform)
|
||||
};
|
||||
}
|
||||
44
src/pbHelpers/FirmwareVersion.tsx
Normal file
44
src/pbHelpers/FirmwareVersion.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import FirmwareIcon from "@mui/icons-material/Security";
|
||||
|
||||
export interface FirmwareVersionHelper {
|
||||
description: string;
|
||||
icon: any;
|
||||
tooltip: string;
|
||||
colour: string;
|
||||
}
|
||||
|
||||
const oldFirmwareColour = "var(--status-warning)";
|
||||
const currentFirmwareColour = "var(--status-ok)";
|
||||
|
||||
export function getFirmwareVersionHelper(
|
||||
version: string,
|
||||
available: string
|
||||
): FirmwareVersionHelper {
|
||||
const firmwareIcon = <FirmwareIcon />;
|
||||
const oldFirmwareIcon = <FirmwareIcon style={{ color: oldFirmwareColour }} />;
|
||||
const currentFirmwareIcon = <FirmwareIcon style={{ color: currentFirmwareColour }} />;
|
||||
let helper = {} as FirmwareVersionHelper;
|
||||
if (!version || version === "") {
|
||||
helper.description = "Unknown version";
|
||||
helper.icon = oldFirmwareIcon;
|
||||
helper.tooltip = "We don't know what version your device is, it may behave unexpectedly";
|
||||
} else if (available !== "" && version !== available) {
|
||||
helper.description = version;
|
||||
helper.icon = oldFirmwareIcon;
|
||||
helper.tooltip =
|
||||
"A firmware upgrade is available, some features may be disabled for out-of-date devices";
|
||||
} else if (available === "") {
|
||||
helper.description = version;
|
||||
helper.icon = firmwareIcon;
|
||||
helper.tooltip = "Running version " + version;
|
||||
} else {
|
||||
helper.description = version;
|
||||
helper.icon = currentFirmwareIcon;
|
||||
helper.tooltip = "Your device is running the latest firmware";
|
||||
}
|
||||
return helper;
|
||||
}
|
||||
|
||||
export function getFirmwareVersionColour(outdated: boolean): string {
|
||||
return outdated ? oldFirmwareColour : currentFirmwareColour;
|
||||
}
|
||||
68
src/pbHelpers/Status.tsx
Normal file
68
src/pbHelpers/Status.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { cyan } from "@mui/material/colors";
|
||||
import AcceptedIcon from "@mui/icons-material/CloudDone";
|
||||
import PendingIcon from "@mui/icons-material/CloudQueueTwoTone";
|
||||
import RejectedIcon from "@mui/icons-material/SmsFailed";
|
||||
import React from "react";
|
||||
|
||||
export interface StatusHelper {
|
||||
description: string;
|
||||
icon: any;
|
||||
}
|
||||
|
||||
const Unknown: StatusHelper = {
|
||||
description: "Status unknown",
|
||||
icon: null
|
||||
};
|
||||
|
||||
const Pending: StatusHelper = {
|
||||
description: "Pending changes",
|
||||
icon: <PendingIcon style={{ color: cyan["600"] }} />
|
||||
};
|
||||
|
||||
const Stale: StatusHelper = {
|
||||
description: "Stale update",
|
||||
icon: null
|
||||
};
|
||||
|
||||
const Accepted: StatusHelper = {
|
||||
description: "Settings synced",
|
||||
icon: <AcceptedIcon style={{ color: "var(--status-ok)" }} />
|
||||
};
|
||||
|
||||
const Rejected: StatusHelper = {
|
||||
description: "Update failed",
|
||||
icon: <RejectedIcon style={{ color: "var(--status-warning)" }} />
|
||||
};
|
||||
|
||||
const Received: StatusHelper = {
|
||||
description: "Settings synced",
|
||||
icon: <AcceptedIcon style={{ color: "var(--status-ok)" }} />
|
||||
};
|
||||
|
||||
const STATUS_MAP = new Map<string, StatusHelper>([
|
||||
["pending", Pending],
|
||||
["stale", Stale],
|
||||
["accepted", Accepted],
|
||||
["rejected", Rejected],
|
||||
["received", Received]
|
||||
]);
|
||||
|
||||
export function getStatusHelper(status: string): StatusHelper {
|
||||
const statuses = Array.from(STATUS_MAP.keys());
|
||||
for (var i = 0; i < statuses.length; i++) {
|
||||
let key = statuses[i];
|
||||
if (status === key) {
|
||||
return STATUS_MAP.get(key) as StatusHelper;
|
||||
}
|
||||
}
|
||||
|
||||
return Unknown;
|
||||
}
|
||||
|
||||
export function getStatusDescription(status: string): string {
|
||||
return getStatusHelper(status).description;
|
||||
}
|
||||
|
||||
export function getStatusIcon(status: string): any {
|
||||
return getStatusHelper(status).icon;
|
||||
}
|
||||
|
|
@ -3,13 +3,14 @@ import { createContext, useContext, useReducer, Dispatch } from "react";
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { User } from "../models/user";
|
||||
import { Team } from "../models/team";
|
||||
import { Firmware } from "models";
|
||||
|
||||
export interface GlobalState {
|
||||
// tags: Tag[];
|
||||
user: User;
|
||||
team: Team;
|
||||
as: string;
|
||||
// firmware: Map<string, Firmware>;
|
||||
firmware: Map<string, Firmware>;
|
||||
// newStructure: boolean;
|
||||
showErrors: boolean;
|
||||
userTeamPermissions: pond.Permission[];
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export {
|
|||
// useDeviceWebsocket,
|
||||
// useFieldAPI,
|
||||
// useFieldMarkerAPI,
|
||||
// useFirmwareAPI,
|
||||
useFirmwareAPI,
|
||||
useGroupAPI,
|
||||
// useHarvestPlanAPI,
|
||||
// useHarvestYearAPI,
|
||||
|
|
@ -25,7 +25,7 @@ export {
|
|||
usePermissionAPI,
|
||||
// usePreferenceAPI,
|
||||
// useSiteAPI,
|
||||
// useTagAPI,
|
||||
useTagAPI,
|
||||
useTeamAPI,
|
||||
useImagekitAPI,
|
||||
// useTaskAPI,
|
||||
|
|
|
|||
205
src/providers/pond/firmwareAPI.tsx
Normal file
205
src/providers/pond/firmwareAPI.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { useHTTP } from "hooks";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useGlobalState } from "providers/StateContainer";
|
||||
import { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { pondURL } from "./pond";
|
||||
|
||||
export interface IFirmwareAPIContext {
|
||||
getFirmwareStatus: (deviceID: number | Long | string, demo?: boolean) => Promise<any>;
|
||||
cancelUpgrade: (deviceID: number | Long | string) => Promise<any>;
|
||||
getLatestFirmware: (
|
||||
platform: pond.DevicePlatform,
|
||||
channel: pond.UpgradeChannel,
|
||||
demo?: boolean
|
||||
) => Promise<any>;
|
||||
getAllLatestFirmware: () => Promise<any>;
|
||||
listFirmware: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
settings?: string,
|
||||
status?: string
|
||||
) => Promise<any>;
|
||||
removeFirmware: (platform: pond.DevicePlatform, version: string) => Promise<any>;
|
||||
downloadInstaller: (platform: pond.DevicePlatform, version: string) => Promise<any>;
|
||||
uploadFirmware: (
|
||||
version: string,
|
||||
channel: pond.UpgradeChannel,
|
||||
platform: pond.DevicePlatform,
|
||||
file: Blob,
|
||||
breaksStorage: boolean
|
||||
) => Promise<any>;
|
||||
updateChannel: (
|
||||
platform: pond.DevicePlatform,
|
||||
version: string,
|
||||
channel: pond.UpgradeChannel
|
||||
) => Promise<any>;
|
||||
upgradeFirmware: (device: number | string) => Promise<any>;
|
||||
}
|
||||
|
||||
export const FirmwareAPIContext = createContext<IFirmwareAPIContext>({} as IFirmwareAPIContext);
|
||||
|
||||
interface Props {}
|
||||
|
||||
function platformRequest(platform: pond.DevicePlatform): string {
|
||||
return platformBody(platform)
|
||||
.toString()
|
||||
.replace("DEVICE_PLATFORM_", "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function channelRequest(channel: pond.UpgradeChannel): string {
|
||||
return channelBody(channel)
|
||||
.replace("UPGRADE_CHANNEL_", "")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function platformBody(platform: pond.DevicePlatform): string {
|
||||
return pond.DevicePlatform[platform].toString();
|
||||
}
|
||||
|
||||
function channelBody(channel: pond.UpgradeChannel): string {
|
||||
return pond.UpgradeChannel[channel].toString();
|
||||
}
|
||||
|
||||
export default function FirmwareProvider(props: PropsWithChildren<Props>) {
|
||||
const { children } = props;
|
||||
const { get, post, put, del, options } = useHTTP();
|
||||
const [{ as }] = useGlobalState();
|
||||
|
||||
const getFirmwareStatus = (deviceID: number | Long | string, demo: boolean = false) => {
|
||||
return get(pondURL("/devices/" + deviceID.toString() + "/firmware", demo));
|
||||
};
|
||||
|
||||
const cancelUpgrade = (deviceID: number | Long | string) => {
|
||||
return post(pondURL("/devices/" + deviceID.toString() + "/cancelUpgrade"));
|
||||
};
|
||||
|
||||
const getLatestFirmware = (
|
||||
platform: pond.DevicePlatform,
|
||||
channel: pond.UpgradeChannel,
|
||||
demo: boolean = false
|
||||
) => {
|
||||
return get(
|
||||
pondURL(
|
||||
"/firmware/latest?platform=" +
|
||||
platformRequest(platform) +
|
||||
"&channel=" +
|
||||
channelRequest(channel),
|
||||
demo
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getAllLatestFirmware = () => {
|
||||
return get(pondURL("/firmware/all"));
|
||||
};
|
||||
|
||||
const listFirmware = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
settings?: string,
|
||||
status?: string
|
||||
) => {
|
||||
return get(
|
||||
pondURL(
|
||||
"/firmware" +
|
||||
"?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + (order ? order : "asc")) +
|
||||
("&by=" + (orderBy ? orderBy : "uploaded")) +
|
||||
(search ? "&search=" + search : "") +
|
||||
(settings ? "&settings=" + settings : "") +
|
||||
(status ? "&status=" + status : "")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const removeFirmware = (platform: pond.DevicePlatform, version: string) => {
|
||||
return del(pondURL("/firmware/" + platformRequest(platform) + "/" + version));
|
||||
};
|
||||
|
||||
const downloadInstaller = (platform: pond.DevicePlatform, version: string) => {
|
||||
let headers = {
|
||||
Accept: "application/octet-stream",
|
||||
...options().headers
|
||||
};
|
||||
headers = { ...headers, ...options().headers };
|
||||
let opt = {
|
||||
responseType: "arraybuffer",
|
||||
headers: headers
|
||||
};
|
||||
return get(
|
||||
pondURL("/firmware/download?platform=" + platformRequest(platform) + "&version=" + version),
|
||||
opt as any
|
||||
);
|
||||
};
|
||||
|
||||
const uploadFirmware = (
|
||||
version: string,
|
||||
channel: pond.UpgradeChannel,
|
||||
platform: pond.DevicePlatform,
|
||||
file: Blob,
|
||||
breaksStorage: boolean
|
||||
) => {
|
||||
// let headers: { "Content-Type": string } = {
|
||||
// "Content-Type": "multipart/form-data"
|
||||
// };
|
||||
let headers = {
|
||||
"Content-Type": "multipart/form-data",
|
||||
...options().headers
|
||||
};
|
||||
headers = { ...headers, ...options().headers };
|
||||
let opt = { headers };
|
||||
let data = new FormData();
|
||||
data.append("version", version);
|
||||
data.append("channel", channelBody(channel));
|
||||
data.append("platform", platformBody(platform));
|
||||
data.append("file", file);
|
||||
data.append("breaksStorage", breaksStorage.toString());
|
||||
return post(pondURL("/firmware"), data, opt);
|
||||
};
|
||||
|
||||
const updateChannel = (
|
||||
platform: pond.DevicePlatform,
|
||||
version: string,
|
||||
channel: pond.UpgradeChannel
|
||||
) => {
|
||||
return put(pondURL("/firmware/channel"), {
|
||||
platform: platformBody(platform),
|
||||
version: version,
|
||||
channel: channelBody(channel)
|
||||
});
|
||||
};
|
||||
|
||||
const upgradeFirmware = (device: number | string) => {
|
||||
return post(pondURL("/devices/" + device + "/upgrade" + (as ? "?as=" + as : "")), {});
|
||||
};
|
||||
|
||||
return (
|
||||
<FirmwareAPIContext.Provider
|
||||
value={{
|
||||
getFirmwareStatus,
|
||||
cancelUpgrade,
|
||||
getLatestFirmware,
|
||||
getAllLatestFirmware,
|
||||
listFirmware,
|
||||
removeFirmware,
|
||||
downloadInstaller,
|
||||
uploadFirmware,
|
||||
updateChannel,
|
||||
upgradeFirmware
|
||||
}}>
|
||||
{children}
|
||||
</FirmwareAPIContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useFirmwareAPI = () => useContext(FirmwareAPIContext);
|
||||
|
|
@ -10,6 +10,8 @@ import NoteProvider, { useNoteAPI } from "./noteAPI";
|
|||
import DeviceProvider, { useDeviceAPI } from "./deviceAPI";
|
||||
import BackpackProvider, { useBackpackAPI } from "./backpackAPI";
|
||||
import GroupProvider, { useGroupAPI } from "./groupAPI";
|
||||
import TagProvider, { useTagAPI } from "./tagAPI";
|
||||
import FirmwareProvider, { useFirmwareAPI } from "./firmwareAPI";
|
||||
// import NoteProvider from "providers/noteAPI";
|
||||
|
||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||
|
|
@ -36,7 +38,11 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
<DeviceProvider>
|
||||
<GroupProvider>
|
||||
<BackpackProvider>
|
||||
{children}
|
||||
<TagProvider>
|
||||
<FirmwareProvider>
|
||||
{children}
|
||||
</FirmwareProvider>
|
||||
</TagProvider>
|
||||
</BackpackProvider>
|
||||
</GroupProvider>
|
||||
</DeviceProvider>
|
||||
|
|
@ -52,7 +58,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
export {
|
||||
useBackpackAPI,
|
||||
useDeviceAPI,
|
||||
useFirmwareAPI,
|
||||
useUserAPI,
|
||||
useTagAPI,
|
||||
useTeamAPI,
|
||||
useImagekitAPI,
|
||||
usePermissionAPI,
|
||||
|
|
|
|||
56
src/providers/pond/tagAPI.tsx
Normal file
56
src/providers/pond/tagAPI.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useHTTP } from "hooks";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { pondURL } from "./pond";
|
||||
|
||||
export interface ITagAPIContext {
|
||||
addTag: (tag: pond.TagSettings) => Promise<any>;
|
||||
updateTag: (tagID: string, tag: pond.TagSettings) => Promise<any>;
|
||||
removeTag: (tagID: string) => Promise<any>;
|
||||
getTag: (tagID: string) => Promise<any>;
|
||||
listTags: () => Promise<any>;
|
||||
}
|
||||
|
||||
export const TagAPIContext = createContext<ITagAPIContext>({} as ITagAPIContext);
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function TagProvider(props: PropsWithChildren<Props>) {
|
||||
const { children } = props;
|
||||
const { get, del, put, post } = useHTTP();
|
||||
|
||||
const addTag = (tag: pond.TagSettings) => {
|
||||
return post(pondURL("/tags"), tag);
|
||||
};
|
||||
|
||||
const updateTag = (tagID: string, tag: pond.TagSettings) => {
|
||||
return put(pondURL("/tags/" + tagID), tag);
|
||||
};
|
||||
|
||||
const removeTag = (tagID: string) => {
|
||||
return del(pondURL("/tags/" + tagID));
|
||||
};
|
||||
|
||||
const getTag = (tagID: string) => {
|
||||
return get(pondURL("/tags/" + tagID));
|
||||
};
|
||||
|
||||
const listTags = () => {
|
||||
return get(pondURL("/tags"));
|
||||
};
|
||||
|
||||
return (
|
||||
<TagAPIContext.Provider
|
||||
value={{
|
||||
addTag,
|
||||
updateTag,
|
||||
removeTag,
|
||||
getTag,
|
||||
listTags
|
||||
}}>
|
||||
{children}
|
||||
</TagAPIContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useTagAPI = () => useContext(TagAPIContext);
|
||||
Loading…
Add table
Add a link
Reference in a new issue