tag stuff

This commit is contained in:
Carter 2025-01-08 14:45:45 -06:00
parent 67ea4cbfdc
commit 3ce00facd1
23 changed files with 1621 additions and 18 deletions

100
package-lock.json generated
View file

@ -25,17 +25,20 @@
"notistack": "^3.0.1",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
"react": "^18.3.1",
"react-color": "^2.19.3",
"react-dom": "^18.3.1",
"react-image": "^4.1.0",
"react-infinite-scroller": "^1.2.6",
"react-phone-input-2": "^2.15.1",
"react-router-dom": "^6.28.0"
"react-router-dom": "^6.28.0",
"react-virtualized-auto-sizer": "^1.0.25"
},
"devDependencies": {
"@babel/types": "^7.25.8",
"@eslint/js": "^9.11.1",
"@types/lodash": "^4.17.13",
"@types/react": "^18.3.10",
"@types/react-color": "^3.0.13",
"@types/react-dom": "^18.3.0",
"@types/react-infinite-scroller": "^1.2.5",
"@vitejs/plugin-react": "^4.3.2",
@ -1211,6 +1214,15 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@ -2152,6 +2164,19 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-color": {
"version": "3.0.13",
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.13.tgz",
"integrity": "sha512-2c/9FZ4ixC5T3JzN0LP5Cke2Mf0MKOP2Eh0NPDPWmuVH3NjPyhEjqNMQpN1Phr5m74egAy+p2lYNAFrX1z9Yrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/reactcss": "*"
},
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.1",
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
@ -2177,6 +2202,16 @@
"@types/react": "*"
}
},
"node_modules/@types/reactcss": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.13.tgz",
"integrity": "sha512-gi3S+aUi6kpkF5vdhUsnkwbiSEIU/BEJyD7kBy2SudWBUuKmJk8AQKE0OVcQQeEy40Azh0lV6uynxlikYIJuwg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@ -3774,6 +3809,12 @@
"version": "4.17.21",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@ -3840,6 +3881,12 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==",
"license": "ISC"
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -3939,9 +3986,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@ -3949,6 +3996,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -4197,6 +4245,7 @@
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#e3d1689e1bf3842b0601cf166be9aff78ef093ff",
"integrity": "sha512-hlkiHxih/Z+kBGMoyWI2Y/vn6afNqRElGuZ/LLGPaLZe2BLXRLKAQOHzR3a7SLfz6p2TkZISNdpmeveK7xgKSg==",
"dependencies": {
"protobufjs": "^6.8.8"
}
@ -4270,6 +4319,24 @@
"node": ">=0.10.0"
}
},
"node_modules/react-color": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"license": "MIT",
"dependencies": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
@ -4374,6 +4441,25 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/react-virtualized-auto-sizer": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.25.tgz",
"integrity": "sha512-YHsksEGDfsHbHuaBVDYwJmcktblcHGafz4ZVuYPQYuSHMUGjpwmUCrAOcvMSGMwwk1eFWj1M/1GwYpNPuyhaBg==",
"license": "MIT",
"peerDependencies": {
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.0.1"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@ -4602,6 +4688,12 @@
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",

View file

@ -34,17 +34,20 @@
"notistack": "^3.0.1",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
"react": "^18.3.1",
"react-color": "^2.19.3",
"react-dom": "^18.3.1",
"react-image": "^4.1.0",
"react-infinite-scroller": "^1.2.6",
"react-phone-input-2": "^2.15.1",
"react-router-dom": "^6.28.0"
"react-router-dom": "^6.28.0",
"react-virtualized-auto-sizer": "^1.0.25"
},
"devDependencies": {
"@babel/types": "^7.25.8",
"@eslint/js": "^9.11.1",
"@types/lodash": "^4.17.13",
"@types/react": "^18.3.10",
"@types/react-color": "^3.0.13",
"@types/react-dom": "^18.3.0",
"@types/react-infinite-scroller": "^1.2.5",
"@vitejs/plugin-react": "^4.3.2",

View 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}
</>
)
}

View file

@ -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}>
<FirmwareLoader>
<main className={classes.appContent}>
<NavigationContainer toggleTheme={toggleTheme} />
</main>
</FirmwareLoader>
</StateProvider>
)
}

View 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
View 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
View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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
View 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)));
}
}

View file

@ -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";

View file

@ -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>
);
}

View 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)
};
}

View 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
View 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;
}

View file

@ -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[];

View file

@ -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,

View 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);

View file

@ -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>
<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,

View 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);