tag stuff
This commit is contained in:
parent
67ea4cbfdc
commit
3ce00facd1
23 changed files with 1621 additions and 18 deletions
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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue