teams list now renders

This commit is contained in:
Carter 2024-11-18 10:10:29 -06:00
parent 17c559bdfc
commit e95b654d7f
81 changed files with 6132 additions and 40 deletions

View file

@ -0,0 +1,35 @@
import { Delete } from "@mui/icons-material";
import { Theme, Button, ButtonProps } from "@mui/material";
import { red } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
deleteButton: {
color: "#fff",
background: red["500"],
"&:hover": {
background: red["600"]
}
},
rightIcon: {
marginLeft: theme.spacing(1)
}
}));
const DeleteButton = (props: ButtonProps) => {
const classes = useStyles();
return (
<Button
variant="contained"
size="small"
{...props}
className={classes.deleteButton}
aria-label="Delete">
{props.children}
<Delete className={classes.rightIcon} fontSize="small" />
</Button>
);
};
export default DeleteButton;

230
src/common/IconPicker.tsx Normal file
View file

@ -0,0 +1,230 @@
import {
Avatar,
CircularProgress,
TextField,
Theme,
Typography,
useTheme
} from "@mui/material";
import React, { useState } from "react";
import { useImagekitAPI } from "providers";
import { SupervisedUserCircle as TeamIcon } from "@mui/icons-material";
import { makeStyles } from "@mui/styles";
const defaults = [
"",
"https://ik.imagekit.io/adaptive/defaults/bird_ty8BTljNA.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663103520016",
"https://ik.imagekit.io/adaptive/defaults/leaf_rF3z5r2tZ.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212283945",
"https://ik.imagekit.io/adaptive/defaults/flower_Uc7QzEQs4R.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663214712629",
"https://ik.imagekit.io/adaptive/defaults/snowleopard_KbnacTZMB.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663103172262",
"https://ik.imagekit.io/adaptive/defaults/car_r5hNMcv7Y.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212283392",
"https://ik.imagekit.io/adaptive/defaults/sunflower_Pg8sac7NK.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663214713051",
"https://ik.imagekit.io/adaptive/defaults/deer__O5DKbszp.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663102676111",
"https://ik.imagekit.io/adaptive/defaults/polarbear_5PMEyQvwH.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208045638",
"https://ik.imagekit.io/adaptive/defaults/beach_i2CM4_uB3.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212283346",
"https://ik.imagekit.io/adaptive/defaults/bunny_ofnih4I6k.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208045524",
"https://ik.imagekit.io/adaptive/defaults/fox_4CPQjzeSw.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208044904",
"https://ik.imagekit.io/adaptive/defaults/frog_An4L9CIIj.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208044711",
"https://ik.imagekit.io/adaptive/defaults/lighthouse_U56RsSQws.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212280873",
"https://ik.imagekit.io/adaptive/defaults/ball_fpYXIQarg.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663214713217",
"https://ik.imagekit.io/adaptive/defaults/dog_bpolRB5HP.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208041090",
"https://ik.imagekit.io/adaptive/defaults/turtle__kcJnFkt1.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663207995148"
];
const useStyles = makeStyles((theme: Theme) => ({
icon: {
textAlign: "center",
margin: "1px"
},
img: {
height: "100%",
width: "auto !important"
},
avatar: {
color: "#fff",
[theme.breakpoints.down("xs")]: {
width: "32px",
height: "32px"
},
[theme.breakpoints.down("lg")]: {
width: "64px",
height: "64px"
}
},
avatarBig: {
[theme.breakpoints.down("lg")]: {
width: "128px",
height: "128px"
}
},
selected: {
backgroundColor: "rgba(150, 150, 150, 0.25)"
}
}));
interface Props {
url: string;
setUrl: React.Dispatch<React.SetStateAction<string>>;
id: string;
style?: any;
title?: string;
}
export function IconPicker(props: Props) {
const { setUrl, id, title, url } = props;
const classes = useStyles();
const theme = useTheme();
const imagekitAPI = useImagekitAPI();
const [loading, setLoading] = useState<boolean>(false);
const onChangeFile = (event: any) => {
event.stopPropagation();
event.preventDefault();
var file = event.target.files[0];
setLoading(true);
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = event => {
resolve(event?.target?.result);
};
reader.onerror = err => {
reject(err);
};
reader.readAsDataURL(file);
}).then((resp: any) => {
let r: string = resp.toString();
imagekitAPI
.uploadImage("icon", r, "/" + id + "/")
.then(resp => {
setUrl(resp.data.url + "?v=" + Math.random());
setLoading(false);
})
.catch(err => {
console.log(err);
setLoading(false);
});
});
};
const top = () => {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "row"
}}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
marginRight: theme.spacing(1)
}}>
<Typography variant="h6">{title ? title : "Icon"}</Typography>
<Avatar
src={url}
style={{
margin: theme.spacing(1)
}}
className={classes.avatarBig}>
<TeamIcon style={{ fontSize: 56 }} />
</Avatar>
</div>
{sources()}
</div>
);
};
const picker = () => {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
flexFlow: "row wrap"
}}>
{defaults.map((src, index) => {
return (
<div
key={index}
style={{
padding: theme.spacing(1),
borderRadius: "4px",
display: "inline-block"
}}
onClick={() => {
setUrl(src);
}}
className={url === src ? classes.selected : undefined}>
<Avatar src={src} variant="rounded" className={classes.avatar}>
<TeamIcon style={{ fontSize: 48 }} />
</Avatar>
</div>
);
})}
</div>
);
};
const sources = () => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}>
<input
name="Upload Image"
type="file"
onChange={event => onChangeFile(event)}
style={{
margin: theme.spacing(1)
}}
/>
{loading && (
<CircularProgress
style={{
marginLeft: theme.spacing(1),
width: theme.spacing(3),
height: theme.spacing(3)
}}
/>
)}
<TextField
id="outlined-basic"
label="Image URL"
variant="outlined"
fullWidth
style={{ margin: theme.spacing(1) }}
onChange={event => {
setUrl(event.currentTarget.value);
}}
/>
</div>
);
};
return (
<React.Fragment>
{top()}
<Typography
variant="h6"
style={{
margin: theme.spacing(1),
textAlign: "center"
}}>
Pick one
</Typography>
{picker()}
</React.Fragment>
);
}

View file

@ -0,0 +1,41 @@
import { createStyles, IconButton, Tooltip } from "@mui/material";
// import { Theme } from "@material-ui/core/styles/createMuiTheme";
// import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import NotificationsEnabledIcon from "@mui/icons-material/NotificationsActive";
import NotificationsDisabledIcon from "@mui/icons-material/NotificationsOff";
import { WithStyles } from "@mui/styles";
const styles = () => createStyles({});
interface Props extends WithStyles<typeof styles> {
notify: boolean;
hidden?: boolean;
onChange: Function;
disabled?: boolean;
tooltip: string;
}
// interface State {}
export default function NotificationButton (props: Props) {
const { notify, onChange, hidden, tooltip, disabled } = props;
if (hidden) {
return null;
}
return (
<Tooltip title={tooltip}>
<span>
<IconButton
aria-label="toggle notifactions"
onClick={() => onChange()}
disabled={disabled}>
{notify ? <NotificationsEnabledIcon /> : <NotificationsDisabledIcon />}
</IconButton>
</span>
</Tooltip>
);
}

View file

@ -0,0 +1,38 @@
import { Dialog, Theme } from "@mui/material";
import { DialogProps } from "@mui/material/Dialog";
// import { TransitionProps } from "@mui/material/transitions";
import { makeStyles } from "@mui/styles";
import { useMobile } from "hooks";
// import React from "react";
const useStyles = makeStyles((theme: Theme) => ({
bg: {
background: theme.palette.background.default
}
}));
// const Transition = React.forwardRef(function Transition(
// props: TransitionProps & { children?: React.ReactElement<any, any> },
// ref: React.Ref<unknown>
// ) {
// return <Slide direction="up" ref={ref} {...props} />;
// });
//renders a fullscren dialog when a mobile viewport is detected
export default function ResponsiveDialog(props: DialogProps) {
const classes = useStyles();
const isMobile = useMobile();
const fullScreen = props.fullScreen ?? isMobile;
return (
<Dialog
fullScreen={fullScreen}
// TransitionComponent={Transition}
{...props}
PaperProps={{
classes: { root: classes.bg }
}}>
{props.children}
</Dialog>
);
}

View file

@ -15,18 +15,18 @@ export {
// useMeasurementsWebsocket,
// useMetricAPI,
// useMineAPI,
// usePermissionAPI,
usePermissionAPI,
// usePreferenceAPI,
// useSecurity,
// useSnackbar,
useSnackbar,
// useTagAPI,
// useUsageAPI,
// useUserAPI
useUserAPI
} from "providers";
// export * from "./useDebounce";
// export * from "./useForceUpdate";
// export * from "./useInterval";
// export * from "./usePrevious";
export * from "./usePrevious";
export * from "./useThemeType";
export * from "./useWidth";
// export * from "./useViewport";

9
src/hooks/usePrevious.ts Normal file
View file

@ -0,0 +1,9 @@
import { useEffect, useRef } from "react";
export const usePrevious = <T extends any>(value: T) => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};

View file

@ -0,0 +1,32 @@
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { cloneDeep } from "lodash";
export class ShareableLink {
public settings: pond.ShareableLinkSettings = pond.ShareableLinkSettings.create();
public status: pond.ShareableLinkStatus = pond.ShareableLinkStatus.create();
public static create(pb?: pond.ShareableLink): ShareableLink {
let my = new ShareableLink();
if (pb) {
my.settings = pond.ShareableLinkSettings.fromObject(cloneDeep(or(pb.settings, {})));
my.status = pond.ShareableLinkStatus.fromObject(cloneDeep(or(pb.status, {})));
}
return my;
}
public static any(data: any): ShareableLink {
return ShareableLink.create(pond.ShareableLink.fromObject(cloneDeep(data)));
}
public static clone(link: ShareableLink): ShareableLink {
let my = new ShareableLink();
my.settings = pond.ShareableLinkSettings.fromObject(cloneDeep(link.settings));
my.status = pond.ShareableLinkStatus.fromObject(cloneDeep(link.status));
return my;
}
public key(): string {
return this.settings.key;
}
}

View file

@ -7,7 +7,7 @@
// export * from "./Group";
// export * from "./Interaction";
export * from "./Scope";
// export * from "./ShareableLink";
export * from "./ShareableLink";
// export * from "./Site";
// export * from "./Tag";
// export * from "./Task";

View file

@ -1,15 +1,15 @@
import { Box } from "@mui/material";
import { useMobile } from "hooks";
import PageContainer from "pages/PageContainer";
// import TeamList from "teams/TeamList";
import TeamList from "teams/TeamList";
export default function Teams() {
const isMobile = useMobile();
return (
<PageContainer>
<Box paddingY={isMobile ? 0.5 : 1} paddingX={isMobile ? 1 : 2}>
{/* <TeamList /> */}
Teams!
<TeamList />
{/* Teams! */}
</Box>
</PageContainer>
);

27
src/pbHelpers/Context.ts Normal file
View file

@ -0,0 +1,27 @@
export function getContextKeys(): string[] {
// Get the url entries without any zero length strings
let entries = document.location.pathname.split("/").filter(key => key.length > 0);
let cutTail = entries.length % 2 === 0;
// Filter out every other entry
entries = entries.filter((_, index) => index % 2 === 1);
if (cutTail) return entries.slice(0, -1);
return entries;
}
export function getContextTypes(): string[] {
// Get the url entries without any zero length strings
let entries = document.location.pathname.split("/").filter(key => key.length > 0);
// Filter out every other entry
entries = entries.filter((_, index) => index % 2 === 0);
// jobsite should just be site and none should end in s anymore
entries = entries.slice(0, -1).map(str => (str.endsWith("s") ? str.slice(0, -1) : str));
entries = entries.map(item => (item === "jobsite" ? "site" : item));
// Return all types but the last (it is the object being loaded, not the context)
return entries;
}

View file

@ -0,0 +1,21 @@
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
export function permissionToString(permission: pond.Permission): string {
return or(
new Map<pond.Permission, string>([
[pond.Permission.PERMISSION_INVALID, "PERMISSION_INVALID"],
[pond.Permission.PERMISSION_READ, "PERMISSION_READ"],
[pond.Permission.PERMISSION_SHARE, "PERMISSION_SHARE"],
[pond.Permission.PERMISSION_USERS, "PERMISSION_USERS"],
[pond.Permission.PERMISSION_WRITE, "PERMISSION_WRITE"],
[pond.Permission.PERMISSION_FILE_MANAGEMENT, "PERMISSION_FILE_MANAGEMENT"],
[pond.Permission.PERMISSION_BILLING, "PERMISSION_BILLING"]
]).get(permission),
"PERMISSION_INVALID"
);
}
export function canWrite(permissions: pond.Permission[]): boolean {
return permissions.includes(pond.Permission.PERMISSION_WRITE);
}

41
src/pbHelpers/User.ts Normal file
View file

@ -0,0 +1,41 @@
import { pond } from "protobuf-ts/pond";
import moment from "moment-timezone";
import { User } from "models";
export function userRoleFromPermissions(permissions: Array<pond.Permission>): string {
//only be an owner if they have all 5
if (
permissions.includes(pond.Permission.PERMISSION_USERS) &&
permissions.includes(pond.Permission.PERMISSION_READ) &&
permissions.includes(pond.Permission.PERMISSION_WRITE) &&
permissions.includes(pond.Permission.PERMISSION_SHARE) &&
permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)
)
return "Owner";
if (permissions.includes(pond.Permission.PERMISSION_WRITE)) return "Admin";
return "User";
}
export function sortUsersByRole(users: User[]): User[] {
let sortedUsers: User[] = users.slice(0).sort((a: User, b: User) => {
if (a.permissions.length === b.permissions.length) {
let aName: string = a.name().toLowerCase();
let bName: string = b.name().toLowerCase();
return aName > bName ? 1 : bName > aName ? -1 : 0;
}
return a.permissions.length - b.permissions.length;
});
return sortedUsers;
}
export function defaultUser(): User {
return User.any({
settings: {
notifyByDefault: true,
notificationMethods: [pond.NotificationMethod.NOTIFICATION_METHOD_EMAIL],
phoneNumber: "",
timezone: moment.tz.guess()
}
});
}

View file

@ -0,0 +1,24 @@
import AddFieldWhite from "assets/products/Ag/addFieldWhite.png";
import AddFieldBlack from "assets/products/Ag/addFieldBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function AddFieldIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? AddFieldWhite : AddFieldBlack;
}
return themeType === "light" ? AddFieldBlack : AddFieldWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import AddMarkerWhite from "assets/products/Ag/addMarkerWhite.png";
import AddMarkerBlack from "assets/products/Ag/addMarkerBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function AddMarkerIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? AddMarkerWhite : AddMarkerBlack;
}
return themeType === "light" ? AddMarkerBlack : AddMarkerWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import AerationFanDarkIcon from "assets/components/aerationFanDark.png";
import AerationFanLightIcon from "assets/components/aerationFanLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
size?: number;
}
export default function AerationFanIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? AerationFanLightIcon : AerationFanDarkIcon;
}
return themeType === "light" ? AerationFanDarkIcon : AerationFanLightIcon;
};
if (size) {
return <img width={size} height={size} alt="fan" src={src()} />;
}
return <ImgIcon alt="fan" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import DeleteWhite from "assets/products/Ag/deleteWhite.png";
import DeleteBlack from "assets/products/Ag/deleteBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function DeleteIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? DeleteWhite : DeleteBlack;
}
return themeType === "light" ? DeleteBlack : DeleteWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,25 @@
import DiseaseLight from "assets/products/Ag/diseaseLight.png";
import DiseaseDark from "assets/products/Ag/diseaseDark.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width: number;
height: number;
type?: "light" | "dark";
}
export default function DiseaseIcon(props: Props) {
const themeType = useThemeType();
const { width, height, type } = props;
const src = () => {
if (type) {
return type === "light" ? DiseaseLight : DiseaseDark;
}
return themeType === "light" ? DiseaseDark : DiseaseLight;
};
return <img width={width} height={height} alt="diseaseIcon" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import EditWhite from "assets/products/Ag/editWhite.png";
import EditBlack from "assets/products/Ag/editBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function EditIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? EditWhite : EditBlack;
}
return themeType === "light" ? EditBlack : EditWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import FieldListWhite from "assets/products/Ag/fieldListWhite.png";
import FieldListBlack from "assets/products/Ag/fieldListBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function FieldListIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? FieldListWhite : FieldListBlack;
}
return themeType === "light" ? FieldListBlack : FieldListWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import FieldMapWhite from "assets/products/Ag/fieldMapWhite.png";
import FieldMapBlack from "assets/products/Ag/fieldMapBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function FieldMapIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? FieldMapWhite : FieldMapBlack;
}
return themeType === "light" ? FieldMapBlack : FieldMapWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import FieldNamesWhite from "assets/products/Ag/fieldNamesWhite.png";
import FieldNamesBlack from "assets/products/Ag/fieldNamesBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function FieldNamesIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? FieldNamesWhite : FieldNamesBlack;
}
return themeType === "light" ? FieldNamesBlack : FieldNamesWhite;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import FieldsIconLight from "assets/products/Ag/FieldsIconLight.png";
import FieldsIconDark from "assets/products/Ag/FieldsIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
size?: number;
}
export default function FieldsIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? FieldsIconLight : FieldsIconDark;
}
return themeType === "light" ? FieldsIconDark : FieldsIconLight;
};
return size ? (
<img alt="fields" height={size} width={size} src={src()} />
) : (
<ImgIcon alt="fields" src={src()} />
);
}

View file

@ -0,0 +1,24 @@
import GearMarkerLight from "assets/products/Ag/gearMarkerLight.png";
import GearMarkerDark from "assets/products/Ag/gearMarkerDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function GearMarkerIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? GearMarkerLight : GearMarkerDark;
}
return themeType === "light" ? GearMarkerDark : GearMarkerLight;
};
return <ImgIcon alt="markerSettings" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import HomeIconLight from "assets/products/Ag/HomeIconLight.png";
import HomeIconDark from "assets/products/Ag/HomeIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function HomeIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? HomeIconLight : HomeIconDark;
}
return themeType === "light" ? HomeIconDark : HomeIconLight;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import MarkerMoveLight from "assets/products/Ag/markerMoveLight.png";
import MarkerMoveDark from "assets/products/Ag/markerMoveDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function MarkerMoveIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? MarkerMoveLight : MarkerMoveDark;
}
return themeType === "light" ? MarkerMoveDark : MarkerMoveLight;
};
return <ImgIcon alt="markerMoveToggle" src={src()} />;
}

View file

@ -0,0 +1,25 @@
import PestsLight from "assets/products/Ag/pestsLight.png";
import PestsDark from "assets/products/Ag/pestsDark.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width: number;
height: number;
type?: "light" | "dark";
}
export default function PestsIcon(props: Props) {
const themeType = useThemeType();
const { width, height, type } = props;
const src = () => {
if (type) {
return type === "light" ? PestsLight : PestsDark;
}
return themeType === "light" ? PestsDark : PestsLight;
};
return <img width={width} height={height} alt="pestsIcon" src={src()} />;
}

View file

@ -0,0 +1,37 @@
import PressureDarkIcon from "assets/components/pressureDark.png";
import PressureLightIcon from "assets/components/pressureLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
heightWidth?: number;
}
export default function TemperatureIcon(props: Props) {
const themeType = useThemeType();
const { type, heightWidth } = props;
const lightIcon = () => {
return PressureLightIcon;
};
const darkIcon = () => {
return PressureDarkIcon;
};
const src = () => {
if (type) {
return type === "light" ? lightIcon() : darkIcon();
}
return themeType === "light" ? darkIcon() : lightIcon();
};
if (heightWidth) {
return <img alt="pressure" src={src()} height={heightWidth} width={heightWidth} />;
}
return <ImgIcon alt="pressure" src={src()} />;
}

View file

@ -0,0 +1,25 @@
import RocksLight from "assets/products/Ag/rocksLight.png";
import RocksDark from "assets/products/Ag/rocksDark.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width: number;
height: number;
type?: "light" | "dark";
}
export default function RocksIcon(props: Props) {
const themeType = useThemeType();
const { width, height, type } = props;
const src = () => {
if (type) {
return type === "light" ? RocksLight : RocksDark;
}
return themeType === "light" ? RocksDark : RocksLight;
};
return <img width={width} height={height} alt="rocksIcon" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import ScoutLight from "assets/products/Ag/scoutLight.png";
import ScoutDark from "assets/products/Ag/scoutDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function ScoutIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? ScoutLight : ScoutDark;
}
return themeType === "light" ? ScoutDark : ScoutLight;
};
return <ImgIcon alt="fields" src={src()} />;
}

View file

@ -0,0 +1,25 @@
import WeedsLight from "assets/products/Ag/weedsLight.png";
import WeedsDark from "assets/products/Ag/weedsDark.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width: number;
height: number;
type?: "light" | "dark";
}
export default function WeedsIcon(props: Props) {
const themeType = useThemeType();
const { width, height, type } = props;
const src = () => {
if (type) {
return type === "light" ? WeedsLight : WeedsDark;
}
return themeType === "light" ? WeedsDark : WeedsLight;
};
return <img width={width} height={height} alt="weedsIcon" src={src()} />;
}

View file

@ -0,0 +1,25 @@
import White from "assets/products/Aviation/AddPlaneIconWhite.png";
import Black from "assets/products/Aviation/AddPlaneIconBlack.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width?: number;
height?: number;
type?: "light" | "dark";
}
export default function AddPlaneIcon(props: Props) {
const themeType = useThemeType();
const { type, width, height } = props;
const src = () => {
if (type) {
return type === "light" ? White : Black;
}
return themeType === "light" ? Black : White;
};
return <img alt="addPlane" src={src()} width={width ?? 25} height={height ?? 25} />;
}

View file

@ -0,0 +1,25 @@
import White from "assets/products/Aviation/AirportMapWhite.png";
import Black from "assets/products/Aviation/AirportMapBlack.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width?: number;
height?: number;
type?: "light" | "dark";
}
export default function AirportMapIcon(props: Props) {
const themeType = useThemeType();
const { type, width, height } = props;
const src = () => {
if (type) {
return type === "light" ? White : Black;
}
return themeType === "light" ? Black : White;
};
return <img alt="airportMap" src={src()} width={width ?? 25} height={height ?? 25} />;
}

View file

@ -0,0 +1,24 @@
import White from "assets/products/Aviation/OmniAirDeviceIconWhite.png";
import Black from "assets/products/Aviation/OmniAirDeviceIconBlack.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
size?: number;
type?: "light" | "dark";
}
export default function OmniAirDeviceIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? White : Black;
}
return themeType === "light" ? Black : White;
};
return <img alt="OmniAirDevice" src={src()} width={size ?? 25} height={size ?? 25} />;
}

View file

@ -0,0 +1,25 @@
import White from "assets/products/Aviation/PlaneWhite.png";
import Black from "assets/products/Aviation/PlaneBlack.png";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
width?: number;
height?: number;
type?: "light" | "dark";
}
export default function PlaneIcon(props: Props) {
const themeType = useThemeType();
const { type, width, height } = props;
const src = () => {
if (type) {
return type === "light" ? White : Black;
}
return themeType === "light" ? Black : White;
};
return <img alt="plane" src={src()} width={width ?? 25} height={height ?? 25} />;
}

View file

@ -0,0 +1,24 @@
import AddBinDark from "assets/products/bindapt/addBin.png";
import AddBinLight from "assets/products/bindapt/addBinLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function AddBinIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? AddBinLight : AddBinDark;
}
return themeType === "light" ? AddBinDark : AddBinLight;
};
return <ImgIcon alt="newBin" src={src()} />;
}

View file

@ -0,0 +1,292 @@
import { ConfigurablePin } from "pbHelpers/AddressTypes";
import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability";
import { quack } from "protobuf-ts/pond";
const BindaptPlusPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 512, label: "C1" }
];
export const BindaptPlusAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptPlusModPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 16, label: "3" },
{ address: 512, label: "C1" }
];
export const BindaptPlusModAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusModPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
//[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptPlusProPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 16, label: "3" },
{ address: 512, label: "C1" },
{ address: 1024, label: "C2" }
];
export const BindaptPlusProAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusProPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptPlusProModPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 16, label: "3" },
{ address: 32, label: "4" },
{ address: 512, label: "C1" },
{ address: 1024, label: "C2" }
];
export const BindaptPlusProModAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusProModPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
//[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptPlusLitePins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" }
];
export const BindaptPlusLiteAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusLitePins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptMiniPins: ConfigurablePin[] = [
{ address: 256, label: "1" },
{ address: 512, label: "2" }
];
export const BindaptMiniAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptMiniPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]]])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptUltimatePins: ConfigurablePin[] = [
{ address: 1, label: "P1" },
{ address: 4, label: "P2" },
{ address: 8, label: "P3" },
{ address: 8192, label: "P4" },
{ address: 16, label: "P5" },
//{ address: 32, label: "P6"}, //for possible expansion if we want a sixth input port on the box
{ address: 128, label: "C1" },
{ address: 256, label: "C2" },
{ address: 512, label: "C3" },
{ address: 1024, label: "C4" }
//{ address: 2048, label: "C5"} //for possible expansion if we want a fifth control port on the box
];
export const BindaptUltimateAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptUltimatePins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BinMonitorPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" }
];
export const BinMonitorAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BinMonitorPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
/** V2 product availability maps */
//TODO: change these pins to match the V2 pin map
const BindaptV2MonitorPins: ConfigurablePin[] = [
{ address: 1, label: "1" },
{ address: 2, label: "2" }
];
export const BindaptV2MonitorAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptV2MonitorPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
//[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
const BindaptV2AutomatePins: ConfigurablePin[] = [
{ address: 1, label: "1" },
{ address: 2, label: "2" },
{ address: 4, label: "3" },
{ address: 8, label: "4" },
{ address: 256, label: "C1" },
{ address: 512, label: "C2" },
{ address: 1024, label: "C3" }
];
export const BindaptV2AutomateAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptV2AutomatePins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
//[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);

View file

@ -0,0 +1,210 @@
import BindaptCellularDarkIcon from "assets/products/bindapt/bindaptCellularDark.png";
import BindaptCellularLightIcon from "assets/products/bindapt/bindaptCellularLight.png";
import BindaptPlusDarkIcon from "assets/products/bindapt/bindaptPlusDark.png";
import BindaptPlusLightIcon from "assets/products/bindapt/bindaptPlusLight.png";
import BindaptPlusProDarkIcon from "assets/products/bindapt/bindaptPlusProDark.png";
import BindaptPlusProLightIcon from "assets/products/bindapt/bindaptPlusProLight.png";
import BindaptWifiDarkIcon from "assets/products/bindapt/bindaptWifiDark.png";
import BindaptWifiLightIcon from "assets/products/bindapt/bindaptWifiLight.png";
import { DeviceProductDescriber, ProductTab } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import {
BindaptPlusAvailability,
BindaptPlusProAvailability,
BindaptPlusLiteAvailability,
BindaptMiniAvailability,
BindaptUltimateAvailability,
BinMonitorAvailability,
BindaptPlusModAvailability,
BindaptPlusProModAvailability,
BindaptV2MonitorAvailability,
BindaptV2AutomateAvailability
} from "./BindaptAvailability";
const getBindaptPlatformIcon = (
platform: pond.DevicePlatform,
theme?: "light" | "dark"
): string | undefined => {
switch (platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON:
return theme === "light" ? BindaptWifiDarkIcon : BindaptWifiLightIcon;
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
return theme === "light" ? BindaptCellularDarkIcon : BindaptCellularLightIcon;
default:
return undefined;
}
};
const getBindaptTabs = (): ProductTab[] => {
return ["presets", "components"];
};
export const BindaptPlus: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS,
label: "Bindapt+",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon;
},
view: "controller",
tabs: getBindaptTabs(),
availability: BindaptPlusAvailability,
defaultAddressMap: new Map<string, string[]>([
["plenum", ["9-2-64"]],
["pressure", ["6-2-24"]],
["heatersFans", ["3-1-512"]] //refers to heaters and fans
])
};
export const BindaptPlusMod: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD,
label: "Bindapt+ V1.5",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon;
},
view: "controller",
tabs: getBindaptTabs(),
availability: BindaptPlusModAvailability
//TODO not sure what the default address map for the modded products should be yet
// defaultAddressMap: new Map<string, string[]>([
// ["plenum", ["9-2-64"]],
// ["heatersFans", ["3-1-512"]] //refers to heaters and fans
// ])
};
export const BindaptPlusPro: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO,
label: "Bindapt+ Pro",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? BindaptPlusProDarkIcon : BindaptPlusProLightIcon;
},
view: "controller",
tabs: getBindaptTabs(),
availability: BindaptPlusProAvailability,
defaultAddressMap: new Map<string, string[]>([
["plenum", ["9-2-64"]],
["pressure", ["6-2-24"]],
["heatersFans", ["3-1-512", "3-1-1024"]] //refers to heaters and fans
])
};
export const BindaptPlusProMod: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD,
label: "Bindapt+ Pro V1.5",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? BindaptPlusProDarkIcon : BindaptPlusProLightIcon;
},
view: "controller",
tabs: getBindaptTabs(),
availability: BindaptPlusProModAvailability
//TODO not sure what the default address map should be yet
// defaultAddressMap: new Map<string, string[]>([
// ["plenum", ["9-2-64"]],
// ["heatersFans", ["3-1-512", "3-1-1024"]] //refers to heaters and fans
// ])
};
export const BindaptPlusLite: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_LITE,
label: "Bindapt+ Lite",
icon: getBindaptPlatformIcon,
tabs: getBindaptTabs(),
availability: BindaptPlusLiteAvailability
};
export const BindaptMini: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI,
label: "Bindapt Mini",
icon: getBindaptPlatformIcon,
tabs: getBindaptTabs(),
availability: BindaptMiniAvailability
};
export const BinUltimate: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BIN_ULTIMATE,
label: "Bindapt Ultimate",
icon: getBindaptPlatformIcon,
tabs: getBindaptTabs(),
view: "controller",
availability: BindaptUltimateAvailability,
defaultAddressMap: new Map<string, string[]>([
["headspace", ["9-2-64", "28-2-98"]], //is both the dht and lidar components in the halo
["pressure", ["6-2-24"]],
["heatersFans", ["3-1-512", "3-1-1024", "3-1-128", "3-1-256"]] //refers to heaters and fans
])
};
export const BinMonitor: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BIN_MONITOR,
label: "Bin Monitor",
icon: getBindaptPlatformIcon,
tabs: getBindaptTabs(),
availability: BinMonitorAvailability,
defaultAddressMap: new Map<string, string[]>([
["headspace", ["9-2-64", "28-2-98"]], //is both the dht and lidar components in the halo
["pressure", ["6-2-24"]]
])
};
export const BinHalo: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BIN_HALO,
label: "Bin Halo",
icon: getBindaptPlatformIcon,
tabs: getBindaptTabs(),
view: "controller",
availability: BindaptPlusAvailability,
defaultAddressMap: new Map<string, string[]>([
["headspace", ["9-2-64", "28-2-98"]], //is both the dht and lidar components in the halo
["pressure", ["6-2-24"]],
["heatersFans", ["3-1-512"]] //refers to heaters and fans
])
};
export const BindaptV2Monitor: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR,
label: "Bindapt+ V2 Monitor",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon;
},
view: "controller",
tabs: getBindaptTabs(),
availability: BindaptV2MonitorAvailability
//TODO not sure what the default address map for these should be yet
// defaultAddressMap: new Map<string, string[]>([
// ["plenum", ["9-2-64"]],
// ["heatersFans", ["3-1-512"]] //refers to heaters and fans
// ])
};
export const BindaptV2Automate: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE,
label: "Bindapt+ V2 Automate",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon;
},
view: "controller",
tabs: getBindaptTabs(),
availability: BindaptV2AutomateAvailability
//TODO not sure what the default address map for these should be yet
// defaultAddressMap: new Map<string, string[]>([
// ["plenum", ["9-2-64"]],
// ["heatersFans", ["3-1-512"]] //refers to heaters and fans
// ])
};
export function IsBindaptDevice(product?: pond.DeviceProduct): boolean {
const bindaptProducts: pond.DeviceProduct[] = [
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_LITE,
pond.DeviceProduct.DEVICE_PRODUCT_BIN_ULTIMATE,
pond.DeviceProduct.DEVICE_PRODUCT_BIN_MONITOR,
pond.DeviceProduct.DEVICE_PRODUCT_BIN_HALO,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR,
pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE
];
return product ? bindaptProducts.includes(product) : false;
}

View file

@ -0,0 +1,39 @@
import BinDarkIcon from "../../assets/products/bindapt/binDark.png";
import BinLightIcon from "../../assets/products/bindapt/binLight.png";
import BinsDarkIcon from "assets/products/bindapt/binsDark.png";
import BinsLightIcon from "assets/products/bindapt/binsLight.png";
import { ImgIcon } from "../../common/ImgIcon";
import { useThemeType } from "../../hooks/useThemeType";
interface Props {
singleBin?: boolean;
type?: "light" | "dark";
size?: number;
}
export default function BinsIcon(props: Props) {
const themeType = useThemeType();
const { type, singleBin, size } = props;
const lightIcon = () => {
return singleBin ? BinLightIcon : BinsLightIcon;
};
const darkIcon = () => {
return singleBin ? BinDarkIcon : BinsDarkIcon;
};
const src = () => {
if (type) {
return type === "light" ? lightIcon() : darkIcon();
}
return themeType === "light" ? darkIcon() : lightIcon();
};
if (size) {
return <img width={size} height={size} alt="bins" src={src()} />;
}
return <ImgIcon alt="bins" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import GrainCableDarkIcon from "assets/components/grainCableDark.png";
import GrainCableLightIcon from "assets/components/grainCableLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function CableIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? GrainCableLightIcon : GrainCableDarkIcon;
}
return themeType === "light" ? GrainCableDarkIcon : GrainCableLightIcon;
};
return <ImgIcon alt="bins" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import DataDuckDark from "assets/common/dataDuckDark.png";
import DataDuckLight from "assets/common/dataDuckLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function DataDuckIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? DataDuckLight : DataDuckDark;
}
return themeType === "light" ? DataDuckDark : DataDuckLight;
};
return <ImgIcon alt="dataDuck" src={src()} />;
}

View file

@ -0,0 +1,11 @@
import ConstructionIconGold from "assets/products/bindapt/constructionIcon.png";
import { ImgIcon } from "common/ImgIcon";
import React from "react";
export default function ConstIcon() {
const src = () => {
return ConstructionIconGold;
};
return <ImgIcon alt="construction" src={src()} />;
}

View file

@ -0,0 +1,4 @@
export * from "./BindaptAvailability";
export * from "./BindaptDescriber";
export * from "./BindaptIcon";
export * from "./BinsIcon";

View file

@ -0,0 +1,24 @@
import CNHiLogoWhite from "assets/marketplaceImages/CNHiWhite.png";
import CNHiLogoBlack from "assets/marketplaceImages/CNHiBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function CNHiIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? CNHiLogoWhite : CNHiLogoBlack;
}
return themeType === "light" ? CNHiLogoBlack : CNHiLogoWhite;
};
return <ImgIcon alt="case-new-holland" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import ContractIconLight from "assets/common/contractWhite.png";
import ContractIconDark from "assets/common/contractBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
size?: number;
}
export default function ContractsIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? ContractIconLight : ContractIconDark;
}
return themeType === "light" ? ContractIconDark : ContractIconLight;
};
return size ? (
<img alt="fields" height={size} width={size} src={src()} />
) : (
<ImgIcon alt="fields" src={src()} />
);
}

View file

@ -0,0 +1,24 @@
import FuelDark from "assets/components/fuelDark.png";
import FuelLight from "assets/components/fuelLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function FuelIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? FuelLight : FuelDark;
}
return themeType === "light" ? FuelDark : FuelLight;
};
return <ImgIcon alt="marketplace" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import graphDark from "assets/common/graphDark.png";
import graphLight from "assets/common/graphLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function GraphIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? graphLight : graphDark;
}
return themeType === "light" ? graphDark : graphLight;
};
return <ImgIcon alt="graph" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import JohnDeereLogoWhite from "assets/marketplaceImages/JDWhite.png";
import JohnDeereLogoBlack from "assets/marketplaceImages/JDBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function JohnDeereIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? JohnDeereLogoWhite : JohnDeereLogoBlack;
}
return themeType === "light" ? JohnDeereLogoBlack : JohnDeereLogoWhite;
};
return <ImgIcon alt="john-deere" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import MarketWhite from "assets/marketplaceImages/marketplaceIconLight.png";
import MarketBlack from "assets/marketplaceImages/marketplaceIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function MarketplaceIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? MarketWhite : MarketBlack;
}
return themeType === "light" ? MarketBlack : MarketWhite;
};
return <ImgIcon alt="marketplace" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import ACHomeLightIcon from "assets/products/Construction/acHomeIconLight.png";
import ACHomeDarkIcon from "assets/products/Construction/acHomeIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function ACHomeIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? ACHomeLightIcon : ACHomeDarkIcon;
}
return themeType === "light" ? ACHomeDarkIcon : ACHomeLightIcon;
};
return <ImgIcon alt="home" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import DeviceGroupLightIcon from "assets/products/Construction/deviceGroupIconLight.png";
import DeviceGroupDarkIcon from "assets/products/Construction/deviceGroupIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
size?: number;
type?: "light" | "dark";
}
export default function DeviceGroupIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? DeviceGroupLightIcon : DeviceGroupDarkIcon;
}
return themeType === "light" ? DeviceGroupDarkIcon : DeviceGroupLightIcon;
};
if (size) {
return <img width={size} height={size} alt="bins" src={src()} />;
}
return <ImgIcon alt="group" src={src()} />;
}

View file

@ -0,0 +1,28 @@
import JobSiteLightIcon from "assets/products/Construction/jobSiteIconLight.png";
import JobSiteDarkIcon from "assets/products/Construction/jobSiteIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
size?: number;
}
export default function JobsiteIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? JobSiteLightIcon : JobSiteDarkIcon;
}
return themeType === "light" ? JobSiteDarkIcon : JobSiteLightIcon;
};
if (size) {
return <img width={size} height={size} alt="jobsite" src={src()}></img>;
}
return <ImgIcon alt="jobsite" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import NexusLightIcon from "assets/products/Construction/nexusIconLight.png";
import NexusDarkIcon from "assets/products/Construction/nexusIconDark.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
size?: number;
type?: "light" | "dark";
}
export default function NexusSTIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? NexusLightIcon : NexusDarkIcon;
}
return themeType === "light" ? NexusDarkIcon : NexusLightIcon;
};
if (size) {
return <img width={size} height={size} alt="bins" src={src()} />;
}
return <ImgIcon alt="nexus" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import HeaterDarkIcon from "assets/components/heaterDark.png";
import HeaterLightIcon from "assets/components/heaterLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function ObjectHeaterIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? HeaterLightIcon : HeaterDarkIcon;
}
return themeType === "light" ? HeaterDarkIcon : HeaterLightIcon;
};
return <ImgIcon alt="group" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import SitesDarkIcon from "assets/products/Construction/sitesIconDark.png";
import SitesLightIcon from "assets/products/Construction/sitesIconLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
size?: number;
type?: "light" | "dark";
}
export default function SitesIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? SitesLightIcon : SitesDarkIcon;
}
return themeType === "light" ? SitesDarkIcon : SitesLightIcon;
};
if (size) {
return <img width={size} height={size} alt="bins" src={src()} />;
}
return <ImgIcon alt="sites" src={src()} />;
}

View file

@ -0,0 +1,29 @@
import TasksDarkIcon from "assets/products/Construction/tasksIconDark.png";
import TasksLightIcon from "assets/products/Construction/tasksIconLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
size?: number;
}
export default function TasksIcon(props: Props) {
const themeType = useThemeType();
const { type, size } = props;
const src = () => {
if (type) {
return type === "light" ? TasksLightIcon : TasksDarkIcon;
}
return themeType === "light" ? TasksDarkIcon : TasksLightIcon;
};
return size ? (
<img alt="tasks" height={size} width={size} src={src()} />
) : (
<ImgIcon alt="tasks" src={src()} />
);
}

View file

@ -0,0 +1,140 @@
import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability";
import { pond, quack } from "protobuf-ts/pond";
import {
BindaptMini,
BindaptPlus,
BindaptPlusLite,
BindaptPlusMod,
BindaptPlusPro,
BindaptPlusProMod,
BindaptV2Monitor,
BindaptV2Automate,
BinHalo,
BinMonitor,
BinUltimate
} from "./Bindapt/BindaptDescriber";
import { NexusST } from "./Nexus/NexusDescriber";
import { MiPCAV2, OmniAir } from "./OmniAir/OmniAirDescriber";
import { ConfigurablePin } from "pbHelpers/AddressTypes";
export type ProductTab = "components" | "presets";
export interface DeviceProductDescriber {
product: pond.DeviceProduct;
label: string;
icon: (platform: pond.DevicePlatform, theme?: "light" | "dark") => string | undefined;
tabs: ProductTab[];
view?: "default" | "controller";
availability?: DeviceAvailabilityMap;
componentExtension?: Boolean;
defaultAddressMap?: Map<string, string[]>;
}
const None: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_NONE,
label: "None",
icon: () => undefined,
tabs: ["components"]
};
//think about making weather describer
const weatherStationPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 16, label: "3" },
{ address: 32, label: "4" }
];
const Weather: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_WEATHER_STATION,
label: "Weather Station",
icon: () => undefined,
tabs: ["components"],
availability: new Map<quack.AddressType, DevicePositions>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, weatherStationPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
])
};
const DEVICE_PRODUCT_MAP = new Map<pond.DeviceProduct, DeviceProductDescriber>([
[pond.DeviceProduct.DEVICE_PRODUCT_NONE, None],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS, BindaptPlus],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO, BindaptPlusPro],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_LITE, BindaptPlusLite],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI, BindaptMini],
[pond.DeviceProduct.DEVICE_PRODUCT_WEATHER_STATION, Weather],
[pond.DeviceProduct.DEVICE_PRODUCT_NEXUS_ST, NexusST],
[pond.DeviceProduct.DEVICE_PRODUCT_OMNIAIR, OmniAir],
[pond.DeviceProduct.DEVICE_PRODUCT_BIN_HALO, BinHalo],
[pond.DeviceProduct.DEVICE_PRODUCT_BIN_ULTIMATE, BinUltimate],
[pond.DeviceProduct.DEVICE_PRODUCT_BIN_MONITOR, BinMonitor],
[pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2, MiPCAV2],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD, BindaptPlusMod],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD, BindaptPlusProMod],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR, BindaptV2Monitor],
[pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE, BindaptV2Automate]
]);
export function ListDeviceProductDescribers(): DeviceProductDescriber[] {
return [...DEVICE_PRODUCT_MAP.values()];
}
function getDescriber(product: pond.DeviceProduct): DeviceProductDescriber {
let describer = DEVICE_PRODUCT_MAP.get(product);
return describer ? describer : None;
}
export function GetDeviceProductLabel(product: pond.DeviceProduct): string {
return getDescriber(product).label;
}
export function GetDeviceProductIcon(
product: pond.DeviceProduct,
platform: pond.DevicePlatform,
theme?: "dark" | "light"
): string | undefined {
return getDescriber(product).icon(platform, theme);
}
export function IsCardController(product?: pond.DeviceProduct): boolean {
return product ? getDescriber(product).view === "controller" : false;
}
export function GetDeviceProductViews(product?: pond.DeviceProduct): boolean {
return product ? getDescriber(product).view === "controller" : false;
}
export function GetDeviceProductTabs(product?: pond.DeviceProduct): ProductTab[] {
return product ? getDescriber(product).tabs : ["components"];
}
export function GetProductAvailability(
product?: pond.DeviceProduct
): DeviceAvailabilityMap | undefined {
return product ? getDescriber(product).availability : undefined;
}
export function IsExtended(product: pond.DeviceProduct): boolean {
let isExtended = getDescriber(product).componentExtension;
if (isExtended) {
return true;
} else {
return false;
}
}
export function GetProductDefaults(product: pond.DeviceProduct): Map<string, string[]> | undefined {
return getDescriber(product).defaultAddressMap;
}

View file

@ -0,0 +1,31 @@
import { ConfigurablePin } from "pbHelpers/AddressTypes";
import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability";
import { quack } from "protobuf-ts/pond";
const NexusSTPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 16, label: "3" },
{ address: 512, label: "C1" }
];
export const NexusSTAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, NexusSTPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);

View file

@ -0,0 +1,17 @@
import { DeviceProductDescriber } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import { NexusSTAvailability } from "./NexusAvailability";
import NexusLightIcon from "assets/products/Construction/nexusIconLight.png";
import NexusDarkIcon from "assets/products/Construction/nexusIconDark.png";
export const NexusST: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_NEXUS_ST,
label: "Nexus ST",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? NexusDarkIcon : NexusLightIcon;
},
view: "controller",
tabs: ["components"],
availability: NexusSTAvailability,
componentExtension: true
};

View file

@ -0,0 +1,59 @@
import { ConfigurablePin } from "pbHelpers/AddressTypes";
import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability";
import { quack } from "protobuf-ts/pond";
const OmniAirPins: ConfigurablePin[] = [
{ address: 4, label: "1" },
{ address: 8, label: "2" },
{ address: 16, label: "3" },
{ address: 512, label: "C1" },
{ address: 1024, label: "C2" }
];
export const OmniAirAvailability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, OmniAirPins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);
/*---V2 Stuff starts here---*/
const MiPCAV2Pins: ConfigurablePin[] = [
{ address: 1, label: "1" },
{ address: 2, label: "2" },
{ address: 4, label: "3" },
{ address: 256, label: "L1" },
{ address: 512, label: "L2" }
];
export const MiPCAV2Availability: DeviceAvailabilityMap = new Map<
quack.AddressType,
DevicePositions
>([
[quack.AddressType.ADDRESS_TYPE_INVALID, []],
[quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, MiPCAV2Pins],
[
quack.AddressType.ADDRESS_TYPE_I2C,
new Map<quack.ComponentType, number[]>([
[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]],
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]]
])
],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]],
[quack.AddressType.ADDRESS_TYPE_GPS, [0]],
[quack.AddressType.ADDRESS_TYPE_MODEM, [0]]
]);

View file

@ -0,0 +1,31 @@
import { DeviceProductDescriber } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import { MiPCAV2Availability, OmniAirAvailability } from "./OmniAirAvailability";
import OmniAirLightIcon from "assets/products/Aviation/OmniAirDeviceIconWhite.png";
import OmniAirDarkIcon from "assets/products/Aviation/OmniAirDeviceIconBlack.png";
export const OmniAir: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_OMNIAIR,
label: "MiPCA V1",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? OmniAirDarkIcon : OmniAirLightIcon;
},
view: "controller",
tabs: ["components"],
availability: OmniAirAvailability,
componentExtension: true
};
/*---V2 Stuff starts here---*/
export const MiPCAV2: DeviceProductDescriber = {
product: pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2,
label: "MiPCA V2",
icon: (_, theme?: "light" | "dark") => {
return theme === "light" ? OmniAirDarkIcon : OmniAirLightIcon;
},
view: "controller",
tabs: ["components"],
availability: MiPCAV2Availability,
componentExtension: true
};

View file

@ -0,0 +1,86 @@
import { Avatar, ButtonBase, createStyles, makeStyles, Theme, Tooltip } from "@material-ui/core";
import { Tune as PresetsIcon, ViewList as ComponentsIcon } from "@material-ui/icons";
import classnames from "classnames";
import { capitalize } from "lodash";
import React from "react";
import { ProductTab } from "./DeviceProduct";
const useStyles = makeStyles((theme: Theme) => {
const themeType = theme.palette.type;
const activeBG = theme.palette.secondary.main;
return createStyles({
avatar: {
color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
active: {
color: theme.palette.getContrastText(activeBG),
backgroundColor: activeBG,
border: 0
},
customIcon: {
padding: theme.spacing(0.5)
}
});
});
interface Props {
tab: ProductTab;
onClick: (selected: ProductTab) => void;
active?: boolean;
}
export default function ProductTabChip(props: Props) {
const classes = useStyles();
const { tab, active, onClick } = props;
const name = () => {
return capitalize(tab);
};
const hasCustomIcon = () => {
switch (tab) {
default:
return false;
}
};
const customIconSrc = () => {
switch (tab) {
default:
return undefined;
}
};
const icon = () => {
switch (tab) {
case "presets":
return <PresetsIcon fontSize="small" />;
case "components":
return <ComponentsIcon fontSize="small" />;
default:
return <React.Fragment />;
}
};
return (
<Tooltip title={name()}>
<Avatar
alt={name()}
src={hasCustomIcon() ? customIconSrc() : undefined}
className={classnames(
classes.avatar,
active && classes.active,
hasCustomIcon() && classes.customIcon
)}
component={ButtonBase}
onClick={() => onClick(tab)}>
{!hasCustomIcon() && icon()}
</Avatar>
</Tooltip>
);
}

View file

@ -0,0 +1,24 @@
import MineDarkIcon from "assets/components/mineIconDark.png";
import MineLightIcon from "assets/components/mineIconLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function VentilationIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? MineLightIcon : MineDarkIcon;
}
return themeType === "light" ? MineDarkIcon : MineLightIcon;
};
return <ImgIcon alt="mine" src={src()} />;
}

View file

@ -0,0 +1,24 @@
import VentilationDarkIcon from "assets/products/ventilation/ventIconDark.png";
import VentilationLightIcon from "assets/products/ventilation/ventIconLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
}
export default function VentilationIcon(props: Props) {
const themeType = useThemeType();
const { type } = props;
const src = () => {
if (type) {
return type === "light" ? VentilationLightIcon : VentilationDarkIcon;
}
return themeType === "light" ? VentilationDarkIcon : VentilationLightIcon;
};
return <ImgIcon alt="ventilation" src={src()} />;
}

141
src/providers/Snackbar.tsx Normal file
View file

@ -0,0 +1,141 @@
import { Button, Theme } from "@mui/material";
import { makeStyles } from "@mui/styles";
import {
ProviderContext as NotistackProviderContext,
SnackbarAction,
SnackbarProvider as NotistackSnackbarProvider,
useSnackbar as useNotistackSnackbar
} from "notistack";
import React, { createContext, PropsWithChildren, useContext } from "react";
const useStyles = makeStyles((theme: Theme) => ({
bottomLeft: {
position: "absolute",
bottom: theme.spacing(7),
left: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
bottom: theme.spacing(1)
}
}
}));
interface ISnackbarContext {
success: (message: string, action?: SnackbarAction) => void;
warning: (message: string, action?: SnackbarAction) => void;
error: (message: string, action?: SnackbarAction) => void;
info: (message: string, action?: SnackbarAction) => void;
openSnack: (message: string, action?: SnackbarAction) => void;
closeSnackbar: (key?: string | number | undefined) => void;
}
type Status = "error" | "success" | "warning" | "info" | "default";
let openExternalSnack: (status: Status, message: string) => void;
export const SnackbarContext = createContext<ISnackbarContext>({} as ISnackbarContext);
interface Props {}
function SnackbarHelper(props: PropsWithChildren<Props>) {
const { children } = props;
const { enqueueSnackbar, closeSnackbar } = useNotistackSnackbar();
const openNewSnack = (status: Status, message: string) => {
switch (status) {
case "success":
success(message);
break;
case "warning":
warning(message);
break;
case "error":
error(message);
break;
case "info":
info(message);
break;
default:
openSnack(message);
break;
}
};
openExternalSnack = openNewSnack;
const openSnack = (message: string, action?: SnackbarAction) => {
enqueueSnackbar(message, { variant: "default", action });
};
const success = (message: string, action?: SnackbarAction) => {
enqueueSnackbar(message, { variant: "success", action });
};
const warning = (message: string, action?: SnackbarAction) => {
enqueueSnackbar(message, { variant: "warning", action });
};
const error = (message: string, action?: SnackbarAction) => {
let msg = message as any;
if (typeof message !== "string") {
if (Object.keys(message).includes("response")) {
msg = msg.response.data.error;
}
}
enqueueSnackbar(msg, { variant: "error", action });
};
const info = (message: string, action?: SnackbarAction) => {
enqueueSnackbar(message, { variant: "info", action });
};
return (
<SnackbarContext.Provider value={{ success, warning, error, info, openSnack, closeSnackbar }}>
{children}
</SnackbarContext.Provider>
);
}
export default function SnackbarProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const classes = useStyles();
const notistackRef = React.createRef<NotistackProviderContext>();
const onClickDismiss = (key: React.ReactText) => () => {
if (notistackRef && notistackRef.current) notistackRef.current.closeSnackbar(key);
};
return (
<NotistackSnackbarProvider
// ref={notistackRef}
maxSnack={1}
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
}}
hideIconVariant
autoHideDuration={7000}
action={key => (
<Button
key={"snackbar-" + key}
size="small"
aria-label="close"
color="inherit"
onClick={onClickDismiss(key)}>
Dismiss
</Button>
)}
classes={{
containerAnchorOriginBottomLeft: classes.bottomLeft
}}>
<SnackbarHelper>{children}</SnackbarHelper>
</NotistackSnackbarProvider>
);
}
export const useSnackbar = () => useContext(SnackbarContext);
// Snackbar access for external sources that are not React components
export const openSnackbar = (status: Status, message: string) => {
if (openExternalSnack === undefined) {
console.warn("openSnackbar is undefined - most likely not loaded into state yet");
} else {
openExternalSnack(status, message);
}
};

View file

@ -4,7 +4,7 @@
export { HTTPContext, useHTTP } from "./http";
export {
// useBackpackAPI,
// useBinAPI,
useBinAPI,
// useBinYardAPI,
// useNoteAPI,
// useComponentAPI,
@ -22,11 +22,12 @@ export {
// useInteractionsAPI,
// useMeasurementsWebsocket,
// useMetricAPI,
// usePermissionAPI,
usePermissionAPI,
// usePreferenceAPI,
// useSiteAPI,
// useTagAPI,
// useTeamAPI,
useTeamAPI,
useImagekitAPI,
// useTaskAPI,
// useUsageAPI,
useUserAPI,
@ -35,7 +36,7 @@ export {
// useMineAPI,
// useKeyManagerAPI,
// useTerminalAPI,
// useGateAPI,
useGateAPI,
// useObjectHeaterAPI,
// useMutationAPI,
// useGrainBagAPI,
@ -43,7 +44,7 @@ export {
// useFileControllerAPI
} from "./pond/pond";
// export { SecurityContext, useSecurity } from "./security";
// export { SnackbarContext, useSnackbar } from "./Snackbar";
export { SnackbarContext, useSnackbar } from "./Snackbar";
export * from "./StateContainer";
// export { TagsContext, TagsProvider, useTags } from "./tags";
// export { UsersContext, UsersProvider, useUsers } from "./users";
// export { UsersContext, UsersProvider, useUsers } from "./users";

View file

@ -0,0 +1,499 @@
import { useHTTP, usePermissionAPI } from "hooks";
import { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { User, binScope } from "models";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { dateRange } from "providers/http";
import { useGlobalState } from "providers";
import { quack } from "protobuf-ts/quack";
export interface IBinAPIContext {
addBin: (bin: pond.BinSettings) => Promise<AxiosResponse<pond.AddBinResponse>>;
updateBin: (key: string, bin: pond.BinSettings) => Promise<AxiosResponse<pond.UpdateBinResponse>>;
bulkBinUpdate: (bins: pond.BinSettings[]) => Promise<AxiosResponse<pond.BulkBinUpdateResponse>>;
updateBinStatus: (
key: string,
binStatus: pond.BinStatus
) => Promise<AxiosResponse<pond.UpdateBinResponse>>;
removeBin: (key: string) => Promise<AxiosResponse<pond.RemoveBinResponse>>;
getBin: (key: string) => Promise<AxiosResponse<pond.Bin>>;
addComponent: (
bin: string,
device: number,
component: string,
preferences?: pond.BinComponentPreferences
) => Promise<AxiosResponse<pond.AddBinComponentResponse>>;
removeComponent: (
bin: string,
component: string
) => Promise<AxiosResponse<pond.RemoveBinComponentResponse>>;
removeAllComponents: (bin: string) => Promise<AxiosResponse<pond.RemoveAllBinComponentsResponse>>;
updateComponentPreferences: (
bin: string,
component: string,
preferences: pond.BinComponentPreferences
) => Promise<AxiosResponse<pond.UpdateBinComponentPreferencesResponse>>;
getBinPageData: (binKey: string, userKey: string, showErrors?: boolean) => Promise<AxiosResponse>;
listBins: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean,
numerical?: boolean,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListBinsResponse>>;
listBinsAndData: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListBinsAndDataResponse>>;
listHistory: (
id: string,
limit: number,
offset: number
) => Promise<AxiosResponse<pond.ListBinHistoryResponse>>;
listHistoryBetween: (
id: string,
limit: number,
start: string,
end: string
) => Promise<AxiosResponse<pond.ListBinHistoryResponse>>;
listBinStatus: (
key: string,
startDate: any,
endDate: any,
limit: number,
offset: number,
order?: "asc" | "desc",
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListBinStatusResponse>>;
listBinComponents: (key: string) => Promise<AxiosResponse<pond.ListBinComponentsResponse>>;
listBinComponentsMeasurements: (
key: string,
start: string,
end: string,
showErrors?: boolean
) => Promise<AxiosResponse<pond.ListBinComponentsMeasurementsResponse>>;
updateBinPermissions: (
key: string,
users: User[]
) => Promise<AxiosResponse<pond.UpdateBinResponse>>;
getBinMetrics: (search?: string) => Promise<AxiosResponse<pond.GetBinMetricsResponse>>;
listBinLiters: (
key: string,
start: string,
end: string,
moisture: number,
plenum: string,
cable: string,
pressure: string,
fan?: string // the fan controller is optional so that if they don't have one the fan is assumed on
) => Promise<AxiosResponse<pond.ListBinLiterResponse>>;
listBinPrefs: (key: string) => Promise<AxiosResponse<pond.ListBinComponentPreferencesResponse>>;
updateTopNodes: (
key: string,
newTopNodes: pond.NewTopNode[]
) => Promise<AxiosResponse<pond.UpdateTopNodesResponse>>;
listBinMeasurements: (
key: string,
startDate: any,
endDate: any,
limit: number,
offset: number,
order: string,
measurementType?: number | quack.MeasurementType,
keys?: string[],
types?: string[],
showErrors?: boolean
) => Promise<AxiosResponse<pond.ListObjectMeasurementsResponse>>;
}
export const BinAPIContext = createContext<IBinAPIContext>({} as IBinAPIContext);
interface Props {}
export default function DeviceProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, post, put, del } = useHTTP();
const permissionAPI = usePermissionAPI();
const [{ as }] = useGlobalState();
const addBin = (bin: pond.BinSettings) => {
if (as) return post<pond.AddBinResponse>(pondURL("/bins?as=" + as), bin);
return post<pond.AddBinResponse>(pondURL("/bins"), bin);
};
const updateBin = (key: string, bin: pond.BinSettings) => {
if (as) return put<pond.UpdateBinResponse>(pondURL("/bins/" + key + "?as=" + as), bin);
return put<pond.UpdateBinResponse>(pondURL("/bins/" + key), bin);
};
const bulkBinUpdate = (bins: pond.BinSettings[]) => {
if (as)
return put<pond.BulkBinUpdateResponse>(pondURL("/bulkBins/update?as=" + as), { bins: bins });
return put<pond.BulkBinUpdateResponse>(pondURL("/bulkBins/update"), { bins: bins });
};
const updateBinStatus = (key: string, binStatus: pond.BinStatus) => {
if (as)
return put<pond.UpdateBinStatusResponse>(
pondURL("/bins/" + key + "/status?as=" + as),
binStatus
);
return put<pond.UpdateBinStatusResponse>(pondURL("/bins/" + key + "/status"), binStatus);
};
const removeBin = (key: string) => {
if (as) return del<pond.RemoveBinResponse>(pondURL("/bins/" + key + "?as=" + as));
return del<pond.RemoveBinResponse>(pondURL("/bins/" + key));
};
const getBin = (key: string) => {
if (as) return get<pond.Bin>(pondURL("/bins/" + key + "?as=" + as));
return get<pond.Bin>(pondURL("/bins/" + key));
};
const addComponent = (
bin: string,
device: number,
component: string,
preferences?: pond.BinComponentPreferences
) => {
let url = "/bins/" + bin + "/addComponent/" + device + "/" + component;
if (as) {
url =
url +
"?as=" +
as +
(preferences ? "&binPref=" + preferences.type + "&fillNode=" + preferences.node : "");
} else if (preferences) {
url = url + "?binPref=" + preferences.type + "&fillNode=" + preferences.node;
}
return post<pond.AddBinComponentResponse>(pondURL(url));
};
const removeComponent = (bin: string, component: string) => {
let url = "/bins/" + bin + "/removeComponent/" + component;
if (as) url = url + "?as=" + as;
return post<pond.RemoveBinComponentResponse>(pondURL(url));
};
const removeAllComponents = (bin: string) => {
let url = "/bins/" + bin + "/removeAllComponents";
if (as) url = url + "?as=" + as;
return post<pond.RemoveAllBinComponentsResponse>(pondURL(url));
};
const updateComponentPreferences = (
bin: string,
component: string,
preferences: pond.BinComponentPreferences
) => {
let url = "/bins/" + bin + "/updateComponent/" + component + "/preferences";
if (as) url = url + "/?as=" + as;
interface request {
preferences: Object;
}
let body: request = { preferences: pond.BinComponentPreferences.toObject(preferences) };
return put<pond.UpdateBinComponentPreferencesResponse>(pondURL(url), body);
};
const getBinPageData = (binKey: string, userKey: string, showErrors = true) => {
return get(
pondURL(
"/bins/" +
binKey +
"/users/" +
userKey +
"?as=" +
as +
(showErrors ? "&showErrors=true" : "&showErrors=false")
)
);
};
const listBins = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean,
numerical?: boolean,
keys?: string[],
types?: string[]
) => {
return get<pond.ListBinsResponse>(
pondURL(
"/bins" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
("&by=" + or(orderBy, "key")) +
(numerical ? "&numerical=true" : "") +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(search ? "&search=" + search : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const listBinsAndData = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
as?: string,
asRoot?: boolean,
keys?: string[],
types?: string[]
) => {
return get<pond.ListBinsAndDataResponse>(
pondURL(
"/binsAndData" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
("&by=" + or(orderBy, "key")) +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
(search ? "&search=" + search : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const listBinStatus = (
key: string,
startDate: any,
endDate: any,
limit: number,
offset: number,
order?: "asc" | "desc",
asRoot?: boolean
) => {
return get<pond.ListBinStatusResponse>(
pondURL(
"/bins/" +
key +
"/status" +
dateRange(startDate, endDate) +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
(asRoot ? "&asRoot=" + asRoot.toString() : "")
)
);
};
const listBinComponents = (key: string) => {
return get<pond.ListBinComponentsResponse>(pondURL("/bins/" + key + "/components"));
};
const listBinComponentsMeasurements = (
key: string,
start: string,
end: string,
showErrors = false
) => {
return get<pond.ListBinComponentsMeasurementsResponse>(
pondURL(
"/bins/" +
key +
"/components/measurements?start=" +
start +
"&end=" +
end +
"&as=" +
as +
(showErrors ? "&showErrors=true" : "&showErrors=false")
)
);
};
const listHistory = (id: string, limit: number, offset: number) => {
return get<pond.ListBinHistoryResponse>(
pondURL("/bins/" + id + "/history?limit=" + limit + "&offset=" + offset)
);
};
const listHistoryBetween = (id: string, limit: number, start: string, end: string) => {
return get<pond.ListBinHistoryResponse>(
pondURL("/bins/" + id + "/historyBetween?limit=" + limit + "&start=" + start + "&end=" + end)
);
};
const updateBinPermissions = (key: string, users: User[]) => {
return permissionAPI.updatePermissions(binScope(key.toString()), users);
};
const getBinMetrics = (search?: string) => {
if (as) {
if (search)
return get<pond.GetBinMetricsResponse>(
pondURL("/metrics/bins/?search=" + search + "&as=" + as)
);
return get<pond.GetBinMetricsResponse>(pondURL("/metrics/bins/?as=" + as));
}
return get<pond.GetBinMetricsResponse>(
pondURL("/metrics/bins" + (search ? "?search=" + search : ""))
);
};
const listBinLiters = (
key: string,
start: string,
end: string,
moisture: number,
plenum: string,
cable: string,
pressure: string,
fan?: string
) => {
if (as) {
return get<pond.ListBinLiterResponse>(
pondURL(
"/bins/" +
key +
"/liters?start=" +
start +
"&end=" +
end +
"&moisture=" +
moisture +
"&plenum=" +
plenum +
"&cable=" +
cable +
"&pressure=" +
pressure +
(fan ? "&fan=" + fan : "") +
"&as=" +
as
)
);
}
return get<pond.ListBinLiterResponse>(
pondURL(
"/bins/" +
key +
"/liters?start=" +
start +
"&end=" +
end +
"&moisture=" +
moisture +
"&plenum=" +
plenum +
"&cable=" +
cable +
"&pressure=" +
pressure +
(fan ? "&fan=" + fan : "")
)
);
};
const listBinPrefs = (key: string) => {
return get<pond.ListBinComponentPreferencesResponse>(
pondURL("/bins/" + key + "/componentPreferences?as=" + as)
);
};
const updateTopNodes = (key: string, newTopNodes: pond.NewTopNode[]) => {
let body: pond.TopNodeList = pond.TopNodeList.create({ newTopNodes: newTopNodes });
return post<pond.UpdateTopNodesResponse>(
pondURL("/bins/" + key + "/updateTopNodes" + (as ? "?as=" + as : "")),
body
);
};
const listBinMeasurements = (
key: string,
startDate: any,
endDate: any,
limit: number,
offset: number,
order: string,
measurementType?: number | quack.MeasurementType,
keys?: string[],
types?: string[],
showErrors?: boolean
) => {
const url = pondURL(
"/objects/" +
key +
"/1/unitMeasurements" +
dateRange(startDate, endDate) +
"&limit=" +
limit +
"&offset=" +
offset +
"&order=" +
order +
(measurementType ? "&type=" + measurementType : "") +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys : "") +
(types ? "&types=" + types : "") +
(showErrors ? "&showErrors=true" : "&showErrors=false")
);
return get<pond.ListObjectMeasurementsResponse>(url);
};
return (
<BinAPIContext.Provider
value={{
addBin,
updateBin,
bulkBinUpdate,
updateBinStatus,
removeBin,
listBinComponents,
listBinComponentsMeasurements,
listBinStatus,
getBin,
addComponent,
removeComponent,
removeAllComponents,
updateComponentPreferences,
getBinPageData,
listBins,
listBinsAndData,
listHistory,
updateBinPermissions,
getBinMetrics,
listBinLiters,
listHistoryBetween,
listBinPrefs,
updateTopNodes,
listBinMeasurements
}}>
{children}
</BinAPIContext.Provider>
);
}
export const useBinAPI = () => useContext(BinAPIContext);

View file

@ -0,0 +1,267 @@
import { useHTTP } from "hooks";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
export interface IGateInterface {
addGate: (
name: string,
settings: pond.GateSettings
) => Promise<AxiosResponse<pond.AddGateResponse>>;
listGates: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
pcaStatus?: boolean,
keys?: string[],
types?: string[],
numerical?: boolean,
specificUser?: string
) => Promise<AxiosResponse<pond.ListGatesResponse>>;
updateGate: (
key: string,
name: string,
settings: pond.GateSettings
) => Promise<AxiosResponse<pond.UpdateGateResponse>>;
getGate: (key: string) => Promise<AxiosResponse<pond.GetGateResponse>>;
removeGate: (key: string) => Promise<AxiosResponse<pond.RemoveGateResponse>>;
updateLink: (
parentKey: string,
parentType: string,
objectID: string,
objectType: string,
permissions: string[]
) => Promise<any>;
updatePrefs: (
gateKey: string,
childType: string,
childKey: string,
gatePref: pond.GateComponentType | pond.GateDeviceType,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.UpdateGatePreferencesResponse>>;
getGatePageData: (key: string) => Promise<AxiosResponse<pond.GetGatePageDataResponse>>;
listGateAirflow: (
gate: string,
device: number | string,
ambientComponent: string,
pressureComponent: string,
start: string,
end: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListGateAirflowResponse>>;
listGateMeasurements: (
key: string,
start: string,
end: string
) => Promise<AxiosResponse<pond.ListGateMeasurementsResponse>>;
}
export const GateAPIcontext = createContext<IGateInterface>({} as IGateInterface);
interface Props {}
export default function GateProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addGate = (name: string, settings: pond.GateSettings) => {
if (as) {
return post<pond.AddGateResponse>(pondURL("/gates?name=" + name + "&as=" + as), settings);
}
return post<pond.AddGateResponse>(pondURL("/gates?name=" + name + "&as=" + as), settings);
};
const listGates = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string,
pcaStatus?: boolean,
keys?: string[],
types?: string[],
numerical?: boolean,
specificUser?: string
) => {
let asText = "";
if (as) asText = "&as=" + as;
if (specificUser) asText = "&as=" + specificUser;
return get<pond.ListGatesResponse>(
pondURL(
"/gates?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(numerical ? "&numerical=true" : "") +
(search ? "&search=" + search : "") +
(pcaStatus ? "&getPCA=" + pcaStatus.toString() : "") +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "") +
asText
)
);
};
const updateGate = (key: string, name: string, settings: pond.GateSettings) => {
if (as) {
return put<pond.UpdateGateResponse>(
pondURL("/gates/" + key + "?as=" + as + "&name=" + name),
settings
);
}
return put<pond.UpdateGateResponse>(pondURL("/gates/" + key + "?name=" + name), settings);
};
const removeGate = (key: string) => {
if (as) {
return del<pond.RemoveGateResponse>(pondURL("/gates/" + key + "?as=" + as));
}
return del<pond.RemoveGateResponse>(pondURL("/gates/" + key + "?as=" + as));
};
const getGate = (key: string) => {
if (as) {
return get<pond.GetGateResponse>(pondURL("/gates/" + key + "?as=" + as));
}
return get<pond.GetGateResponse>(pondURL("/gates/" + key));
};
const getGatePageData = (key: string) => {
if (as) {
return get<pond.GetGatePageDataResponse>(pondURL("/gatePage/" + key + "?as=" + as));
}
return get<pond.GetGatePageDataResponse>(pondURL("/gatePage/" + key));
};
const updateLink = (
parentID: string,
parentType: string,
objectID: string,
objectType: string,
permissions: string[]
) => {
if (as)
return post(pondURL(`/gates/` + parentID + `/link?as=${as}`), {
Key: objectID,
Type: objectType,
Parent: parentID,
ParentType: parentType,
Permissions: permissions
});
return post(pondURL(`/gates/` + parentID + `/link`), {
Key: objectID,
Type: objectType,
Parent: parentID,
ParentType: parentType,
Permissions: permissions
});
};
const updatePrefs = (
gateKey: string,
childType: string,
childKey: string,
gatePref: pond.GateComponentType | pond.GateDeviceType,
keys?: string[],
types?: string[]
) => {
if (as) {
return put<pond.UpdateGatePreferencesResponse>(
pondURL(
`/gatePrefs/` +
gateKey +
"?childType=" +
childType +
"&childKey=" +
childKey +
"&gatePreference=" +
gatePref +
`&as=` +
as +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
);
}
return put<pond.UpdateGatePreferencesResponse>(
pondURL(
`/gatePrefs/` +
gateKey +
"?childType=" +
childType +
"&childKey=" +
childKey +
"&gatePreference=" +
gatePref +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
);
};
const listGateAirflow = (
gate: string,
device: number | string,
ambientComponent: string,
pressureComponent: string,
start: string,
end: string,
keys?: string[],
types?: string[]
) => {
return get<pond.ListGateAirflowResponse>(
pondURL(
"/gates/" +
gate +
"/airflow?device=" +
device +
"&ambient=" +
ambientComponent +
"&pressure=" +
pressureComponent +
"&start=" +
start +
"&end=" +
end +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
);
};
const listGateMeasurements = (key: string, start: string, end: string) => {
return get<pond.ListGateMeasurementsResponse>(
pondURL("/gates/" + key + "/measurements?start=" + start + "&end=" + end + "&as=" + as)
);
};
return (
<GateAPIcontext.Provider
value={{
addGate,
listGates,
updateGate,
removeGate,
getGate,
updateLink,
getGatePageData,
updatePrefs,
listGateAirflow,
listGateMeasurements
}}>
{children}
</GateAPIcontext.Provider>
);
}
export const useGateAPI = () => useContext(GateAPIcontext);

View file

@ -0,0 +1,45 @@
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { createContext, PropsWithChildren, useContext } from "react";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
export interface IImagekitAPIContext {
uploadImage: (
filename: string,
file: any,
filepath?: string
) => Promise<AxiosResponse<pond.UploadImageResponse>>;
}
export const ImagekitAPIContext = createContext<IImagekitAPIContext>({} as IImagekitAPIContext);
interface Props {}
export default function ImagekitProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { post } = useHTTP();
const [{ as }] = useGlobalState();
const uploadImage = (filename: string, file: any, filepath = "/") => {
let request = new pond.UploadImageRequest();
request.file = file;
request.fileName = filename;
request.filePath = filepath;
if (as) return post<pond.UploadImageResponse>(pondURL("/images/upload?as=" + as), request);
return post<pond.UploadImageResponse>(pondURL("/images/upload"), request);
};
return (
<ImagekitAPIContext.Provider
value={{
uploadImage
}}>
{children}
</ImagekitAPIContext.Provider>
);
}
export const useImagekitAPI = () => useContext(ImagekitAPIContext);

View file

@ -0,0 +1,153 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { Scope, Team, User } from "models";
import { permissionToString } from "pbHelpers/Permission";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { createContext, PropsWithChildren, useContext } from "react";
import { objectQueryParams, pondURL } from "./pond";
export interface IPermissionAPIContext {
getPermissions: (user: string, scope: Scope) => Promise<AxiosResponse<any>>;
removePermissions: (user: string, scope: Scope) => Promise<AxiosResponse<any>>;
updatePermissions: (scope: Scope, users: User[] | Team[]) => Promise<AxiosResponse<any>>;
updateRelativePermissions: (
parentScope: Scope,
childScope: Scope,
permissions: pond.Permission[]
) => Promise<AxiosResponse<any>>;
shareObject: (
scope: Scope,
email: string,
permissions: pond.Permission[],
useImitation?: boolean
) => Promise<AxiosResponse<any>>;
shareObjectByKey: (
scope: Scope,
key: string,
permissions: pond.Permission[]
) => Promise<AxiosResponse<any>>;
addShareableLink: (scope: Scope, expiration: string) => Promise<AxiosResponse<any>>;
removeShareableLink: (scope: Scope, code: string) => Promise<AxiosResponse<any>>;
listShareableLinks: (scope: Scope) => Promise<AxiosResponse<any>>;
getShareableLink: (scope: Scope, code: string) => Promise<AxiosResponse<any>>;
}
export const PermissionAPIContext = createContext<IPermissionAPIContext>(
{} as IPermissionAPIContext
);
interface Props {}
export default function PermissionProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, put, post } = useHTTP();
const [{ as }] = useGlobalState();
const getPermissions = (user: string, scope: Scope) => {
return get(pondURL("/users/" + user + "/permissions" + objectQueryParams(scope)));
};
const removePermissions = (user: string, scope: Scope) => {
return del(pondURL("/users/" + user + "/permissions" + objectQueryParams(scope)));
};
const updatePermissions = (scope: Scope, users: User[] | Team[]) => {
interface user {
id: string;
permissions: string[];
}
interface request {
users: user[];
}
let body: request = { users: [] };
users.forEach((user: User | Team) => {
body.users.push({
id: user.id(),
permissions: user.permissions.map(permission => permissionToString(permission))
});
});
return put(pondURL("/" + scope.kind + "s/" + scope.key + "/permissions"), body);
};
const shareObject = (
scope: Scope,
email: string,
permissions: pond.Permission[],
useImitation = true
) => {
let sql = "/" + scope.kind + "s/" + scope.key + "/share";
if (as && useImitation) sql = sql + "?as=" + as;
return post(pondURL(sql), {
email: email,
permissions: permissions.map(permission => permissionToString(permission))
});
};
const updateRelativePermissions = (
parentScope: Scope,
childScope: Scope,
permissions: pond.Permission[]
) => {
let sql = "/updateRelativePermissions";
if (as) sql = sql + "?as=" + as;
return put(pondURL(sql), {
parent: parentScope.key,
parentType: parentScope.kind,
child: childScope.key,
childType: childScope.kind,
permissions: permissions.map(permission => permissionToString(permission))
});
};
const shareObjectByKey = (scope: Scope, key: string, permissions: pond.Permission[]) => {
if (as)
return post(pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`), {
key: key,
permissions: permissions.map(permission => permissionToString(permission))
});
return post(pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey"), {
key: key,
permissions: permissions.map(permission => permissionToString(permission))
});
};
const addShareableLink = (scope: Scope, expiration: string) => {
let shareBody: pond.ShareableLinkSettings = pond.ShareableLinkSettings.create({
expiration: expiration
});
return post(pondURL("/" + scope.kind + "s/" + scope.key + "/links"), shareBody);
};
const removeShareableLink = (scope: Scope, code: string) => {
return del(pondURL("/" + scope.kind + "s/" + scope.key + "/links/" + code));
};
const listShareableLinks = (scope: Scope) => {
return get(pondURL("/" + scope.kind + "s/" + scope.key + "/links"));
};
const getShareableLink = (scope: Scope, code: string) => {
return get(pondURL("/" + scope.kind + "s/" + scope.key + "/links/" + code));
};
return (
<PermissionAPIContext.Provider
value={{
getPermissions,
removePermissions,
updatePermissions,
updateRelativePermissions,
shareObject,
shareObjectByKey,
addShareableLink,
removeShareableLink,
listShareableLinks,
getShareableLink
}}>
{children}
</PermissionAPIContext.Provider>
);
}
export const usePermissionAPI = () => useContext(PermissionAPIContext);

View file

@ -1,5 +1,11 @@
import { PropsWithChildren } from "react";
import UserProvider, { useUserAPI } from "./userAPI";
import TeamProvider, { useTeamAPI } from "./teamAPI";
import { useImagekitAPI } from "./imagekitAPI";
import { usePermissionAPI } from "./permissionAPI";
import { Scope } from "models";
import { useBinAPI } from "./binAPI";
import { useGateAPI } from "./gateAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
let url = import.meta.env.VITE_APP_API_URL + (demo ? "/demo" : "") + partial;
@ -8,20 +14,27 @@ export const pondURL = (partial: string, demo: boolean = false): string => {
return url;
};
// export const objectQueryParams = (scope: Scope) => {
// return "?" + scope.kind + "_id=" + scope.key;
// };
export const objectQueryParams = (scope: Scope) => {
return "?" + scope.kind + "_id=" + scope.key;
};
export default function PondProvider(props: PropsWithChildren<any>) {
const { children } = props;
return (
<UserProvider>
{children}
<TeamProvider>
{children}
</TeamProvider>
</UserProvider>
);
}
export {
useUserAPI,
useTeamAPI,
useImagekitAPI,
usePermissionAPI,
useBinAPI,
useGateAPI
};

View file

@ -73,8 +73,8 @@ export default function TeamProvider(props: PropsWithChildren<Props>) {
const updatePreferences = (
key: string,
preferences: pond.TeamPreferences,
keys?: string[],
types?: string[]
// keys?: string[],
// types?: string[]
) => {
return put(pondURL("/teams/" + key + "/preferences"), preferences);
};

View file

@ -2,15 +2,15 @@
// import { Scope, User } from "models";
// import { pond } from "protobuf-ts/pond";
import { createContext, PropsWithChildren, useContext } from "react";
// import { objectQueryParams, pondURL } from "./pond";
import { objectQueryParams, pondURL } from "./pond";
// import { or } from "utils";
// import { useGlobalState } from "providers";
import { useGlobalState } from "providers";
// import { AxiosResponse } from "axios";
import { useHTTP } from "../http";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { pond } from "protobuf-ts/pond";
import { User } from "../../models/user";
import { Scope } from "models";
// export interface ListUsersResponse {
// users: User[];
@ -26,9 +26,9 @@ export interface IUserAPIContext {
// search?: string
// ) => Promise<ListUsersResponse>;
// listProfiles: () => Promise<pond.UserProfile[]>;
// listObjectUsers: (scope: Scope) => Promise<any>;
// updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise<any>;
getUser: (id: string) => Promise<User>;
listObjectUsers: (scope: Scope) => Promise<any>;
updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise<any>;
getUser: (id: string, scope?: Scope) => Promise<User>;
getUserWithTeam: (
id: string,
team?: string,
@ -53,8 +53,8 @@ export const UserAPIContext = createContext<IUserAPIContext>({} as IUserAPIConte
export default function UserProvider(props: PropsWithChildren<any>) {
const { children } = props;
const { get, /*put*/ } = useHTTP();
// const [{ as }] = useGlobalState();
const { get, put } = useHTTP();
const [{ as }] = useGlobalState();
// const listUsers = (
// limit: number,
@ -104,20 +104,20 @@ export default function UserProvider(props: PropsWithChildren<any>) {
// });
// };
// const listObjectUsers = (scope: Scope) => {
// let sql = "/" + scope.kind + "s/" + scope.key + "/users";
// if (as) sql = sql + "?as=" + as;
// return get(pondURL(sql));
// };
const listObjectUsers = (scope: Scope) => {
let sql = "/" + scope.kind + "s/" + scope.key + "/users";
if (as) sql = sql + "?as=" + as;
return get(pondURL(sql));
};
// const updateObjectUsers = (scope: Scope, users: pond.IUser[]) => {
// return put(pondURL("/users" + objectQueryParams(scope)), { users });
// };
const updateObjectUsers = (scope: Scope, users: pond.IUser[]) => {
return put(pondURL("/users" + objectQueryParams(scope)), { users });
};
// const getUser = (id: string): Promise<User> => {
const getUser = (id: string): Promise<any> => {
const getUser = (id: string, scope?: Scope): Promise<any> => {
let partial = "/users/" + id;
// if (scope) partial += "?kind=" + scope.kind + "&key=" + scope.key;
if (scope) partial += "?kind=" + scope.kind + "&key=" + scope.key;
return new Promise(function(resolve, reject) {
get(pondURL(partial))
.then((response: any) => resolve(response.data))
@ -218,8 +218,8 @@ export default function UserProvider(props: PropsWithChildren<any>) {
value={{
// listUsers,
// listProfiles,
// listObjectUsers,
// updateObjectUsers,
listObjectUsers,
updateObjectUsers,
getUser,
getUserWithTeam,
// getProfile,

197
src/teams/TeamActions.tsx Normal file
View file

@ -0,0 +1,197 @@
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip
} from "@mui/material";
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
import MoreIcon from "@mui/icons-material/MoreVert";
import GroupSettingsIcon from "@mui/icons-material/Settings";
import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import { Team, teamScope } from "models";
import { pond } from "protobuf-ts/pond";
import React, { useState } from "react";
import ObjectUsers from "user/ObjectUsers";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import TeamSettings from "./TeamSettings";
import NotificationButton from "common/NotificationButton";
import { blue } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles(() => ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
}
}));
interface Props {
team: Team;
permissions: pond.Permission[];
refreshCallback: () => void;
preferences: pond.TeamPreferences;
toggleNotificationPreference: () => void;
}
interface OpenState {
share: boolean;
users: boolean;
teams: boolean;
settings: boolean;
removeSelf: boolean;
}
export default function TeamActions(props: Props) {
const classes = useStyles();
const { team, permissions, refreshCallback, preferences, toggleNotificationPreference } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [openState, setOpenState] = useState<OpenState>({
share: false,
users: false,
teams: false,
settings: false,
removeSelf: false
});
const groupMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{!isOffline() && canShare && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, share: true });
setAnchorEl(null);
}}
// button
>
<ListItemIcon>
<ShareObjectIcon className={classes.shareIcon} />
</ListItemIcon>
<ListItemText primary="Share" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, users: true });
setAnchorEl(null);
}}
// button
>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, removeSelf: true });
setAnchorEl(null);
}}
// button
>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const dialogs = () => {
const key = team.key();
const label = team.name();
return (
<React.Fragment>
<TeamSettings
team={team}
permissions={permissions}
teamDialogOpen={openState.settings}
closeTeamDialogCallback={() => {
setOpenState({ ...openState, settings: false });
}}
refreshCallback={refreshCallback}
/>
<ShareObject
scope={teamScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.share}
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
/>
<ObjectUsers
scope={teamScope(key)}
label={label}
permissions={permissions}
isDialogOpen={openState.users}
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
<RemoveSelfFromObject
scope={teamScope(key)}
label={label}
isDialogOpen={openState.removeSelf}
closeDialogCallback={() => setOpenState({ ...openState, removeSelf: false })}
/>
</React.Fragment>
);
};
const canWrite = permissions.includes(pond.Permission.PERMISSION_WRITE);
return (
<React.Fragment>
{/* <NotificationButton
notify={preferences.notify}
onChange={toggleNotificationPreference}
tooltip={
"Notifications for " +
team.name() +
" are " +
(preferences.notify ? "enabled" : "disabled")
}
// hidden={isLoading}
/> */}
{canWrite && (
<Tooltip title="Team Settings">
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
<GroupSettingsIcon />
</IconButton>
</Tooltip>
)}
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
{groupMenu()}
{dialogs()}
</React.Fragment>
);
}

360
src/teams/TeamList.tsx Normal file
View file

@ -0,0 +1,360 @@
import React, { useCallback, useEffect, useState } from "react";
import {
Card,
Theme,
List,
ListItem,
ListItemAvatar,
Avatar,
ListItemText,
Typography,
ListItemSecondaryAction,
Divider,
useTheme,
TextField,
InputAdornment,
CircularProgress,
IconButton,
MenuItem,
} from "@mui/material";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useTeamAPI, useUserAPI } from "providers";
import { Team, teamScope } from "models";
import { useNavigate } from "react-router";
// import TeamActions from "./TeamActions";
import SearchIcon from '@mui/icons-material/Search';
import NextIcon from "@mui/icons-material/NavigateNext";
import PrevIcon from "@mui/icons-material/NavigateBefore";
import LastIcon from "@mui/icons-material/SkipNext";
import FirstIcon from "@mui/icons-material/SkipPrevious";
import TeamSettings from "./TeamSettings";
import { Add } from "@mui/icons-material";
import { cloneDeep } from "lodash";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { makeStyles, styled } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
cardContent: {
padding: theme.spacing(1)
},
clickable: {
"&:hover": {
textDecoration: "underline",
cursor: "pointer"
}
},
input: {
marginLeft: "auto",
marginRight: theme.spacing(2),
marginTop: theme.spacing(1)
}
}))
const limits = [
{
value: 5,
label: "5"
},
{
value: 10,
label: "10"
},
{
value: 15,
label: "15"
},
{
value: 20,
label: "20"
}
];
interface Props {}
export default function TeamList(props: Props) {
const classes = useStyles();
const [teams, setTeams] = useState<pond.Team[]>([]);
const teamAPI = useTeamAPI();
const userAPI = useUserAPI();
const history = useNavigate();
const theme = useTheme();
const [{ user, as }] = useGlobalState();
const [teamPerms, setTeamPerms] = useState<pond.Permission[][]>([]);
const [teamPrefs, setTeamPrefs] = useState<pond.TeamPreferences[]>([]);
const [total, setTotal] = useState<number>(0);
const [limit, setLimit] = useState<number>(5);
const [page, setPage] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
const [search, setSearch] = useState<string>("");
const [teamDialogOpen, setTeamDialogOpen] = useState<boolean>(false);
const [asUser, setAsUser] = useState<string | undefined>(undefined);
const [userId, setUserId] = useState<string | undefined>(undefined);
const [isUser, setIsUser] = useState<boolean>(true);
// const ColorAdd = styled(Add)((theme: Theme) => ({
// color: theme.palette.getContrastText(theme.palette.primary.main),
// backgroundColor: theme.palette.primary.main,
// marginTop: 'auto',
// marginBottom: 'auto',
// borderRadius: '4px',
// '&:hover': {
// backgroundColor: theme.palette.primary.light,
// cursor: 'pointer',
// },
// '&:disabled': {
// backgroundColor: 'rgba(0,0,0,0)',
// color: theme.palette.getContrastText(theme.palette.primary.main),
// },
// }));
useEffect(() => {
if (as.includes("auth0")) setAsUser(as);
}, [as]);
useEffect(() => {
setUserId(user.id());
}, [user]);
useEffect(() => {
if (!asUser) {
setIsUser(true);
return;
}
setIsUser(userId === asUser);
}, [userId, asUser]);
const loadTeams = useCallback(() => {
setLoading(true);
let newTeamPerms: pond.Permission[][] = [];
let newTeamPrefs: pond.TeamPreferences[] = [];
let searchString = search;
teamAPI
.listTeams(limit, limit * page, undefined, "name", searchString, undefined, asUser)
.then(resp => {
setTotal(resp.data.total);
resp.data.teams.forEach((team, index) => {
let u = asUser ? asUser : user.id();
if (team.settings && u !== "") {
userAPI
.getUser(u, teamScope(team.settings?.key))
.then(resp => {
newTeamPerms[index] = resp.permissions;
newTeamPrefs[index] = resp.preferences;
})
.finally(() => {
if (newTeamPerms[index]) setTeamPerms([...newTeamPerms]);
if (newTeamPrefs[index]) setTeamPrefs(newTeamPrefs);
});
}
});
setTeams(resp.data.teams);
})
.finally(() => {
setLoading(false);
});
}, [user, teamAPI, userAPI, limit, page, search, asUser]);
useEffect(() => {
loadTeams();
}, [user, teamAPI, userAPI, limit, page, search, asUser, loadTeams]);
const drawTeams = () => {
return (
<List>
{teams.map((team, index) => {
let permissions = teamPerms[index];
if (!permissions) {
permissions = [];
}
let preferences = teamPrefs[index];
if (!preferences) {
preferences = pond.TeamPreferences.create();
}
return (
<React.Fragment key={index}>
<ListItem alignItems="flex-start">
<ListItemAvatar>
<Avatar
onClick={() => {
if (team.settings) history("teams/" + team.settings.key);
}}
src={team.settings?.avatar}
alt=""
className={classes.clickable}
/>
</ListItemAvatar>
<ListItemText
primary={
<div
className={classes.clickable}
onClick={() => {
if (team.settings) history("teams/" + team.settings.key);
}}>
{team.settings?.name}
</div>
}
secondary={
<React.Fragment>
<Typography component="span" variant="body2" color="textPrimary">
{team.settings?.info}
</Typography>
</React.Fragment>
}
/>
{/* <ListItemSecondaryAction>
<TeamActions
team={Team.create(team)}
permissions={permissions}
refreshCallback={loadTeams}
preferences={preferences}
toggleNotificationPreference={() => {
if (preferences) {
let newTeamPrefs = cloneDeep(teamPrefs);
newTeamPrefs[index].notify = !newTeamPrefs[index].notify;
console.log(newTeamPrefs[index]);
teamAPI
.updatePreferences(
Team.create(team).key(),
newTeamPrefs[index],
getContextKeys(),
getContextTypes()
)
.then(resp => {
setTeamPrefs(newTeamPrefs);
});
// setTeamPrefs(newTeamPrefs)
}
}}
/>
</ListItemSecondaryAction> */}
</ListItem>
<Divider variant="inset" component="li" />
</React.Fragment>
);
})}
</List>
);
};
const showProgress = () => {
return (
<div
style={{
width: "100%",
textAlign: "center"
}}>
<CircularProgress
style={{
textAlign: "center",
marginTop: theme.spacing(12),
marginBottom: theme.spacing(12)
}}
/>
</div>
);
};
const firstPage = () => {
setPage(0);
};
const lastPage = () => {
setPage(Math.round(total / limit));
};
const nextPage = () => {
if (limit * (page + 1) < total) setPage(page + 1);
};
const prevPage = () => {
if (page > 0) setPage(page - 1);
};
const handleChange = (event: any) => {
setLimit(event.target.value);
};
const showPageButtons = () => {
return (
<div style={{ display: "flex", float: "right", alignItems: "center" }}>
<Typography style={{ marginRight: theme.spacing(2) }}>Teams per page:</Typography>
<TextField
select
value={limit}
onChange={handleChange}
style={{ marginRight: theme.spacing(1) }}>
{limits.map(option => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<IconButton disabled={page < 1} size="small" color="inherit" onClick={firstPage}>
<FirstIcon />
</IconButton>
<IconButton disabled={page < 1} size="small" color="inherit" onClick={prevPage}>
<PrevIcon />
</IconButton>
<Typography
variant="subtitle1"
style={{ marginRight: theme.spacing(1), marginLeft: theme.spacing(1) }}>
{limit * page + 1} {limit * page + limit <= limit ? limit * page + limit : limit + 1} of{" "}
{total}
</Typography>
<IconButton
disabled={limit * (page + 1) >= total}
size="small"
color="inherit"
onClick={nextPage}>
<NextIcon />
</IconButton>
<IconButton
disabled={limit * (page + 1) >= total}
size="small"
color="inherit"
onClick={lastPage}>
<LastIcon />
</IconButton>
</div>
);
};
return (
<Card className={classes.cardContent}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography
variant={"h6"}
style={{
marginLeft: theme.spacing(1),
marginTop: "auto",
marginBottom: "auto",
marginRight: theme.spacing(2)
}}>
{isUser ? user.name() + "'s " : "Someone's "} Teams
</Typography>
<Add onClick={() => setTeamDialogOpen(true)} />
<TextField
className={classes.input}
value={search}
onChange={event => setSearch(event?.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}></TextField>
</div>
<Typography variant={"subtitle2"} style={{ marginLeft: theme.spacing(2) }}>
{isUser ? "Viewing all teams that you are a part of" : "These are someone else's teams"}
</Typography>
{!loading ? drawTeams() : showProgress()}
{showPageButtons()}
<TeamSettings
teamDialogOpen={teamDialogOpen}
closeTeamDialogCallback={() => setTeamDialogOpen(false)}
refreshCallback={loadTeams}
/>
</Card>
);
}

265
src/teams/TeamSettings.tsx Normal file
View file

@ -0,0 +1,265 @@
import {
Box,
Button,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Tab,
Tabs,
TextField,
Typography,
useTheme,
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Team } from "models";
import { pond } from "protobuf-ts/pond";
import { useSnackbar, useTeamAPI } from "providers";
import React, { useEffect, useState } from "react";
import DeleteButton from "common/DeleteButton";
import { userRoleFromPermissions } from "pbHelpers/User";
import { IconPicker } from "common/IconPicker";
interface Props {
teamDialogOpen: boolean;
closeTeamDialogCallback: () => void;
refreshCallback?: () => void;
team?: Team;
permissions?: pond.Permission[];
}
export default function TeamSettings(props: Props) {
const theme = useTheme();
const teamAPI = useTeamAPI();
const { teamDialogOpen, closeTeamDialogCallback, permissions, refreshCallback } = props;
const [loading, setLoading] = useState<boolean>(false);
const [nameField, setNameField] = useState<string>("");
const [infoField, setInfoField] = useState<string>("");
const [url, setUrl] = useState<string>("");
const snackbar = useSnackbar();
const [tab, setTab] = useState(0);
const [team, setTeam] = useState<Team>();
const [teamKey, setTeamKey] = useState<string>();
const [openDelete, setOpenDelete] = useState(false);
useEffect(() => {
if (props.team) {
setTeam(props.team);
setTeamKey(props.team.key());
}
}, [props.team]);
useEffect(() => {
if (team) {
setNameField(team.name());
setInfoField(team.settings.info);
setUrl(team.settings.avatar);
}
}, [team]);
// const ColorButton = withStyles((theme: Theme) => ({
// root: {
// color: theme.palette.getContrastText(theme.palette.primary.main),
// backgroundColor: theme.palette.primary.main,
// "&:hover": {
// backgroundColor: theme.palette.primary.light
// },
// "&:disabled": {
// backgroundColor: "rgba(0,0,0,0)",
// color: theme.palette.getContrastText(theme.palette.primary.main)
// }
// }
// }))(Button);
const removeTeam = () => {
if (team?.key())
teamAPI
.removeTeam(team?.key())
.then(() => {
snackbar.info("Team removed successfully.");
if (refreshCallback) refreshCallback();
closeTeamDialogCallback();
})
.catch(err => {
console.log(err);
});
};
const addTeam = () => {
let team = pond.TeamSettings.create();
team.name = nameField;
team.info = infoField;
setLoading(true);
teamAPI
.addTeam(team)
.then(resp => {
snackbar.success("Team added successfully!");
setTab(1);
let t = pond.Team.create();
t.settings = team;
setTeam(Team.create(t));
setTeamKey(resp.data.team);
if (refreshCallback) refreshCallback();
})
.catch(err => {
snackbar.error(err);
})
.finally(() => {
setLoading(false);
});
};
const updateTeam = () => {
let newTeam = pond.TeamSettings.create();
newTeam.name = nameField;
newTeam.info = infoField;
newTeam.avatar = url;
if (teamKey) {
teamAPI
.updateTeam(teamKey, newTeam)
.then(() => {
snackbar.success("Team update successfully!");
})
.catch(err => {
snackbar.error(err);
})
.finally(() => {
if (refreshCallback) refreshCallback();
setNameField("");
setInfoField("");
setUrl("");
setTeamKey(undefined);
setTab(0);
closeTeamDialogCallback();
});
}
};
const disableButton = () => {
if (loading) return true;
if (nameField === "" || infoField === "") return true;
};
const close = () => {
setNameField("");
setInfoField("");
setUrl("");
closeTeamDialogCallback();
};
const isOwner = () => {
if (permissions) {
return userRoleFromPermissions(permissions) === "Owner";
}
return false;
};
const deleteConfirmation = () => {
return (
<ResponsiveDialog
open={openDelete}
onClose={() => {
setOpenDelete(false);
}}>
<DialogTitle>Delete Team</DialogTitle>
<DialogContent>
<Typography>
Performing this action will delete this team and and any file attachments associated
with it.
</Typography>
<Typography>Are you sure you wish to continue.</Typography>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDelete(false);
}}>
Close
</Button>
<DeleteButton onClick={removeTeam}>Confirm</DeleteButton>
</DialogActions>
</ResponsiveDialog>
);
};
const teamSettings = () => {
return (
<DialogContent>
<DialogContentText style={{ margin: theme.spacing(1) }}>
Add more info about your team, business, or company. You can have multiple teams if
needed.
</DialogContentText>
<TextField
label="Name"
value={nameField}
onChange={event => setNameField(event?.target.value)}
variant="outlined"
style={{ margin: theme.spacing(1) }}
fullWidth
/>
<div style={{ marginTop: theme.spacing(1) }} />
<TextField
label="Info"
value={infoField}
onChange={event => setInfoField(event?.target.value)}
variant="outlined"
style={{ margin: theme.spacing(1) }}
fullWidth
/>
</DialogContent>
);
};
let disable = disableButton();
return (
<React.Fragment>
<ResponsiveDialog open={teamDialogOpen} onClose={close}>
<DialogTitle>{team ? "Update " + team.name() + "?" : "Create New Team?"}</DialogTitle>
<Box style={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={tab}
onChange={(_, value) => setTab(value)}
aria-label="basic tabs example"
centered>
<Tab label="Name & Info" key={1} />
<Tab
label="Team Icon"
key={2}
disabled={team?.key() === undefined || team?.key() === ""}
/>
</Tabs>
</Box>
{tab === 0 && teamSettings()}
{tab === 1 && teamKey !== undefined && (
<DialogContent>
<IconPicker setUrl={setUrl} url={url} id={teamKey} />
</DialogContent>
)}
<DialogActions>
{isOwner() && (
<DeleteButton
onClick={() => {
setOpenDelete(true);
}}
aria-label="Cancel"
style={{ marginRight: "auto", marginLeft: theme.spacing(2) }}>
Delete
</DeleteButton>
)}
<Button onClick={close} color="primary" aria-label="Cancel">
Cancel
</Button>
<Button
disabled={disable}
onClick={teamKey ? updateTeam : addTeam}
style={{ marginRight: theme.spacing(2) }}>
<Typography variant="subtitle2" color="textPrimary">
{teamKey ? "Update" : "Submit"}
</Typography>
</Button>
</DialogActions>
</ResponsiveDialog>
{deleteConfirmation()}
</React.Fragment>
);
}

719
src/user/ObjectUsers.tsx Normal file
View file

@ -0,0 +1,719 @@
import {
Avatar,
Box,
Button,
Checkbox,
CircularProgress,
DialogActions,
DialogContent,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid,
Grid2,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
PaletteColor,
Stack,
Theme,
Tooltip,
Typography,
useTheme
} from "@mui/material";
// import { Theme } from "@material-ui/core/styles/createMuiTheme";
// import { PaletteColor } from "@material-ui/core/styles/createPalette";
import RemoveUserIcon from "@mui/icons-material/RemoveCircle";
import EditIcon from "@mui/icons-material/EditOutlined";
import ShareIcon from "@mui/icons-material/Share";
import ResponsiveDialog from "common/ResponsiveDialog";
import { usePermissionAPI, usePrevious, useSnackbar, useUserAPI } from "hooks";
import { cloneDeep, isEqual } from "lodash";
import { Scope, User } from "models";
import { sortUsersByRole, userRoleFromPermissions } from "pbHelpers/User";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { or } from "utils/types";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { useGlobalState } from "providers";
import { red, blue } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => {
const avatarBG = theme.palette.secondary["700" as keyof PaletteColor];
return {
dialogContent: {
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
[theme.breakpoints.up("sm")]: {
padding: `${theme.spacing(1)}px ${theme.spacing(3)}px`
}
},
userAvatar: {
color: "#fff",
backgroundColor: avatarBG
},
userPermissions: {
paddingLeft: theme.spacing(4),
paddingBottom: theme.spacing(1)
},
textOverflowEllipsis: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
},
removeIcon: {
color: red["500"],
"&:hover": {
color: red["600"]
}
},
verticalSpacing: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(3)
},
lessSpacing: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1)
},
iconButton: {
margin: theme.spacing(1)
},
shareIcon: {
color: blue["600"],
"&:hover": {
color: blue["700"]
}
},
editIcon: {
color: blue["300"],
"&:hover": {
color: blue["400"]
}
}
};
});
interface Props {
scope: Scope;
label: string;
permissions: pond.Permission[];
isDialogOpen: boolean;
closeDialogCallback: Function;
refreshCallback: Function;
userCallback?: (users?: User[] | undefined) => void;
dialog?: string;
cardMode?: boolean;
useImitation?: boolean;
}
export default function ObjectUsers(props: Props) {
const classes = useStyles();
const theme = useTheme();
const permissionAPI = usePermissionAPI();
const [{ user, team }, dispatch] = useGlobalState();
const canProvision = user.allowedTo("provision");
const userAPI = useUserAPI();
const { error, success, warning } = useSnackbar();
const {
scope,
label,
permissions,
isDialogOpen,
closeDialogCallback,
refreshCallback,
userCallback,
dialog,
cardMode
//useImitation
} = props;
const prevPermissions = usePrevious(permissions);
const prevIsDialogOpen = usePrevious(isDialogOpen);
const [initialUsers, setInitialUsers] = useState<User[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [canManageUsers, setCanManageUsers] = useState<boolean>(
permissions.includes(pond.Permission.PERMISSION_USERS)
);
const [canShare, setCanShare] = useState<boolean>(
permissions.includes(pond.Permission.PERMISSION_SHARE)
);
const [canShareBilling, setCanShareBilling] = useState<boolean>(
permissions.includes(pond.Permission.PERMISSION_SHARE) &&
permissions.includes(pond.Permission.PERMISSION_USERS) &&
permissions.includes(pond.Permission.PERMISSION_WRITE)
);
const [removedUsers, setRemovedUsers] = useState<string[]>([]);
const [removeSelfDialogIsOpen, setRemoveSelfDialogIsOpen] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isShareObjectDialogOpen, setIsShareObjectDialogOpen] = useState<boolean>(false);
const [numOwners, setNumOwners] = useState(0);
const [useImitation] = useState<boolean>(
props.useImitation !== undefined ? props.useImitation : true
);
// const [editUserDialog, setEditUserDialog] = useState(false);
const [userForDialog, setUserForDialog] = useState<User | undefined>(undefined);
const setDefaultState = () => {
setInitialUsers([]);
setUsers([]);
setRemovedUsers([]);
};
const load = useCallback(() => {
setIsLoading(true);
userAPI
.listObjectUsers(scope)
.then((response: any) => {
let rUsers: User[] = [];
or(response.data, { users: [] }).users.forEach((user: any) => {
rUsers.push(User.any(user));
});
rUsers = rUsers.filter(u => u.permissions.length > 0);
setInitialUsers(cloneDeep(rUsers));
let numOwners = 0;
rUsers.forEach(user => {
if (userRoleFromPermissions(user.permissions) === "Owner") numOwners++;
});
setNumOwners(numOwners);
setUsers(rUsers);
})
.catch((err: any) => {
console.log(err);
setInitialUsers([]);
setUsers([]);
})
.finally(() => setIsLoading(false));
}, [scope, userAPI]);
useEffect(() => {
if (!prevIsDialogOpen && isDialogOpen) {
load();
}
if (prevPermissions !== permissions) {
setCanManageUsers(permissions.includes(pond.Permission.PERMISSION_USERS));
setCanShare(permissions.includes(pond.Permission.PERMISSION_SHARE));
setCanShareBilling(
permissions.includes(pond.Permission.PERMISSION_SHARE) &&
permissions.includes(pond.Permission.PERMISSION_USERS) &&
permissions.includes(pond.Permission.PERMISSION_WRITE)
);
}
}, [isDialogOpen, load, permissions, prevIsDialogOpen, prevPermissions]);
const closeRemoveSelfDialog = () => {
setRemoveSelfDialogIsOpen(false);
};
const close = () => {
closeRemoveSelfDialog();
closeDialogCallback();
setDefaultState();
};
const submit = () => {
console.log(users);
permissionAPI
.updatePermissions(scope, users)
.then((response: any) => {
success("Users were sucessfully updated for " + label);
close();
refreshCallback();
userCallback && userCallback(users);
users.forEach(u => {
if (u.id() === user.id()) {
if (scope.kind === "team" && scope.key === team.key()) {
dispatch({ key: "userTeamPermissions", value: u.permissions });
}
}
});
if (cardMode) {
load();
}
})
.catch((err: any) => {
err.response.data.error
? warning(err.response.data.error)
: error("Error occured when updating users for " + label);
close();
})
.finally(() => setUserForDialog(undefined));
};
const changeUserPermissions = (user: pond.IUser) => (event: any) => {
let updatedUsers = cloneDeep(users);
let permissionMapping = new Map<string, pond.Permission>([
["2", pond.Permission.PERMISSION_READ],
["3", pond.Permission.PERMISSION_WRITE],
["4", pond.Permission.PERMISSION_SHARE],
["1", pond.Permission.PERMISSION_USERS],
["5", pond.Permission.PERMISSION_BILLING],
["6", pond.Permission.PERMISSION_FILE_MANAGEMENT]
]);
let permission = or(
permissionMapping.get(event.target.value),
pond.Permission.PERMISSION_INVALID
);
for (let i = 0; i < updatedUsers.length; i++) {
let currUser = updatedUsers[i];
if (
user &&
user.settings &&
currUser &&
currUser.settings &&
currUser.settings.id === user.settings.id
) {
let permissions = user.permissions ? cloneDeep(user.permissions) : [];
if (permissions.includes(permission)) {
permissions = permissions.filter(
(curPermission: pond.Permission) => curPermission !== permission
);
} else {
permissions.push(permission);
}
updatedUsers[i].permissions = permissions;
user.permissions = permissions;
break;
}
}
setUsers(updatedUsers);
};
const removeUser = (user: string) => {
let updatedUsers = cloneDeep(users);
let updatedRemovedUsers = cloneDeep(removedUsers);
for (let i = 0; i < updatedUsers.length; i++) {
let currUser = updatedUsers[i];
if (currUser && currUser.settings && currUser.settings.id === user) {
updatedRemovedUsers.push(currUser.settings.id);
updatedUsers[i].permissions = [];
break;
}
}
setUsers(updatedUsers);
setRemovedUsers(updatedRemovedUsers);
};
const checkIsSelf = (checkUser: pond.IUser): boolean => {
const id = checkUser && checkUser.settings ? checkUser.settings.id : "";
return user.id() === id;
};
const userIsRemoved = (user: pond.IUser): boolean => {
const id = user && user.settings ? user.settings.id : "";
return removedUsers.includes(or(id, ""));
};
const objectUsersUnchanged = (): boolean => {
return isEqual(initialUsers, users);
};
const openRemoveSelfDialog = () => {
setRemoveSelfDialogIsOpen(true);
};
const openShareObjectDialog = () => {
setIsShareObjectDialogOpen(true);
};
const closeShareObjectDialog = (shared: boolean | undefined) => {
setIsShareObjectDialogOpen(false);
if (shared) {
load();
}
};
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const title = () => {
return (
// <Grid container direction="column" justify="space-between">
// <Grid container direction="row" justify="space-between">
// <Grid item xs={8}>
// Users
// <Typography variant="body2" color="textSecondary">
// {label}
// </Typography>
// </Grid>
// <Grid item xs={4} container justify="flex-end">
// {canShare && (
// <Tooltip title={"Share " + label}>
// <IconButton
// aria-label="Share"
// className={classes.iconButton}
// onClick={openShareObjectDialog}>
// <ShareIcon className={classes.shareIcon} />
// </IconButton>
// </Tooltip>
// )}
// </Grid>
// </Grid>
// {dialog && (
// <Typography variant="body2" color="textSecondary" style={{ marginTop: "4px" }}>
// {dialog}
// </Typography>
// )}
// </Grid>
<Stack direction="column" spacing={2}>
<Stack direction="row" spacing={2} justifyContent="space-between">
<Box>
Users
<Typography variant="body2" color="textSecondary">
{label}
</Typography>
</Box>
<Box display="flex" justifyContent="flex-end">
{canShare && (
<Tooltip title={`Share ${label}`}>
<IconButton
aria-label="Share"
onClick={openShareObjectDialog}>
<ShareIcon />
</IconButton>
</Tooltip>
)}
</Box>
</Stack>
{dialog && (
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
{dialog}
</Typography>
)}
</Stack>
);
};
const userPermissionManagement = (user: User) => {
let permissions = user.permissions;
if (
!canManageUsers ||
(checkIsSelf(user) && !canShareBilling) ||
userIsRemoved(user) ||
(permissions.includes(pond.Permission.PERMISSION_USERS) &&
permissions.includes(pond.Permission.PERMISSION_READ) &&
permissions.includes(pond.Permission.PERMISSION_WRITE) &&
permissions.includes(pond.Permission.PERMISSION_SHARE) &&
permissions.includes(pond.Permission.PERMISSION_BILLING) &&
permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) &&
!cardMode)
) {
return null;
}
const canRead = permissions.includes(pond.Permission.PERMISSION_READ);
const xs = cardMode ? 12 : 4;
const sm = cardMode ? 4 : 3;
const justify = cardMode ? "flex-start" : "center";
return (
<FormControl component="fieldset" fullWidth className={classes.userPermissions}>
<FormLabel component="legend"></FormLabel>
<FormGroup>
<Grid2 container direction="row" justifyContent={justify} spacing={2}>
<Grid2 sx={{ gridColumn: `span ${xs} / span ${sm}` }}>
<FormControlLabel
control={
<Checkbox
checked={permissions.includes(pond.Permission.PERMISSION_READ)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_READ}
/>
}
label="View"
labelPlacement="end"
/>
</Grid2>
<Grid2 sx={{ gridColumn: `span ${xs} / span ${sm}` }}>
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_WRITE)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_WRITE}
/>
}
label="Edit"
labelPlacement="end"
/>
</Grid2>
<Grid2 sx={{ gridColumn: `span ${xs} / span ${sm}` }}>
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_SHARE)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_SHARE}
/>
}
label="Share"
labelPlacement="end"
/>
</Grid2>
<Grid2 sx={{ gridColumn: `span ${xs} / span ${sm}` }}>
<FormControlLabel
control={
<Checkbox
disabled={!canRead}
checked={permissions.includes(pond.Permission.PERMISSION_USERS)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_USERS}
/>
}
label="Manage Users"
labelPlacement="end"
/>
</Grid2>
<Grid2 sx={{ gridColumn: 'span 6 / span 3' }}>
<FormControlLabel
control={
<Checkbox
checked={permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_FILE_MANAGEMENT}
/>
}
label="Files"
labelPlacement="end"
/>
</Grid2>
<Grid2 sx={{ gridColumn: `span ${xs} / span ${sm}` }}>
<FormControlLabel
control={
<Checkbox
disabled={!canShareBilling}
checked={permissions.includes(pond.Permission.PERMISSION_BILLING)}
onChange={changeUserPermissions(user)}
value={pond.Permission.PERMISSION_BILLING}
/>
}
label="Billing"
labelPlacement="end"
/>
</Grid2>
</Grid2>
</FormGroup>
</FormControl>
);
};
const userListItem = (user: User) => {
let isSelf = checkIsSelf(user);
let isRemoved = userIsRemoved(user);
let name = user.name();
let permissions = user.permissions;
let userSettings = pond.UserSettings.create(user.settings !== null ? user.settings : undefined);
return (
<ListItem alignItems="flex-start" >
<ListItemAvatar>
<Avatar
alt={name}
src={
userSettings.avatar && userSettings.avatar !== "" ? userSettings.avatar : undefined
}
className={classes.userAvatar}>
{!(userSettings.avatar && userSettings.avatar !== "") && name}
</Avatar>
</ListItemAvatar>
<ListItemText
className={classes.textOverflowEllipsis}
disableTypography
primary={
<Typography component="div" variant="body1" color="textPrimary">
{name + (isSelf ? " (you)" : "")}
</Typography>
}
secondary={
<React.Fragment>
<Typography component="div" variant="caption" color="textSecondary">
{user.settings.email}
</Typography>
<Typography component="div" variant="caption" color="textSecondary">
{isRemoved ? "REMOVED " : userRoleFromPermissions(permissions)}
</Typography>
</React.Fragment>
}
/>
{isSelf && !canShareBilling ? (
<ListItemSecondaryAction>
<Tooltip title={"Remove yourself from " + label}>
<IconButton
edge="end"
aria-label={"Remove yourself from " + label}
onClick={openRemoveSelfDialog}>
<RemoveUserIcon className={classes.removeIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
) : (canManageUsers || canProvision) && !isRemoved && !cardMode ? (
<ListItemSecondaryAction>
<Tooltip title="Revoke user's access">
<IconButton
edge="end"
aria-label="Remove user from device"
onClick={() => removeUser(or(user.settings, { id: "" }).id)}>
<RemoveUserIcon className={classes.removeIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
) : (
(canManageUsers || canProvision) && (
<ListItemSecondaryAction>
<Tooltip title="Edit user's permissions">
<IconButton
edge="end"
aria-label="Edit user permissions"
disabled={!canManageUsers}
onClick={() => setUserForDialog(user)}>
<EditIcon className={classes.editIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
)
)}
</ListItem>
);
};
const objectUsersList = () => {
const sortedUsers = sortUsersByRole(users);
let userListItems: any = [];
for (var i = 0; i < sortedUsers.length; i++) {
let user = sortedUsers[i];
if (user) {
userListItems.push(
<React.Fragment key={i}>
{userListItem(user)}
{!cardMode && userPermissionManagement(user)}
<Divider />
</React.Fragment>
);
}
}
return <List>{userListItems}</List>;
};
const content = () => {
if (isLoading) {
return (
<Grid2 container justifyContent="center" alignContent="center" className={classes.verticalSpacing}>
<CircularProgress />
</Grid2>
);
}
if (users.length < 1) {
return (
<React.Fragment>
<Typography
variant="h6"
align="center"
color="textSecondary"
className={!cardMode ? classes.verticalSpacing : ""}
style={{ padding: cardMode ? 0 : "" }}>
No users found.
</Typography>
</React.Fragment>
);
}
return objectUsersList();
};
const actions = () => {
return (
<Grid2 container direction="row" justifyContent="space-between">
<Grid2 sx={{ gridColumn: `span ${4} ` }} ></Grid2>
<Grid2 sx={{ gridColumn: `span ${8} ` }} container justifyContent="flex-end">
{!cardMode && (
<Button onClick={close} color="primary">
Cancel
</Button>
)}
{canManageUsers && !cardMode && (
<Button onClick={submit} color="primary" disabled={objectUsersUnchanged()}>
{cardMode ? "Update" : "Submit"}
</Button>
)}
</Grid2>
</Grid2>
);
};
const dialogs = () => {
return (
<React.Fragment>
<RemoveSelfFromObject
scope={scope}
label={label}
isDialogOpen={removeSelfDialogIsOpen}
closeDialogCallback={closeRemoveSelfDialog}
numberOfOwners={numOwners}
/>
<ShareObject
scope={scope}
label={label}
permissions={permissions}
isDialogOpen={isShareObjectDialogOpen}
closeDialogCallback={closeShareObjectDialog}
useImitation={useImitation}
sharedUsers={users}
/>
</React.Fragment>
);
};
if (cardMode) {
return (
<React.Fragment>
<Typography variant="h6">{title()}</Typography>
<div style={{ overflowY: "scroll" }}>
<Divider />
{content()}
{actions()}
{dialogs()}
<div style={{ marginTop: theme.spacing(2) }} />
</div>
<ResponsiveDialog
open={userForDialog !== undefined}
onClose={() => setUserForDialog(undefined)}>
<DialogTitle>Change {userForDialog?.name()}'s Permissions?</DialogTitle>
<DialogContent>{userForDialog && userPermissionManagement(userForDialog)}</DialogContent>
<DialogActions>
<Button onClick={() => setUserForDialog(undefined)}>Close</Button>
<Button color="primary" onClick={submit}>
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
</React.Fragment>
);
}
return (
<ResponsiveDialog
fullWidth
maxWidth="xs"
open={props.isDialogOpen}
onClose={close}
aria-labelledby="object-users-dialog">
<DialogTitle id="object-users-title">{title()}</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
<DialogActions>{actions()}</DialogActions>
{dialogs()}
</ResponsiveDialog>
);
}

View file

@ -0,0 +1,111 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography
} from "@mui/material";
import { Scope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, usePermissionAPI, useSnackbar } from "providers";
import React from "react";
import { useNavigate } from "react-router";
interface Props {
scope: Scope;
label: string;
isDialogOpen: boolean;
closeDialogCallback: (removed: boolean) => void;
path?: string;
numberOfOwners?: number; //including the number of owners will prevent the user from leaving if they are the final owner
}
export default function RemoveSelfFromObject(props: Props) {
const navigate = useNavigate();
const permissionAPI = usePermissionAPI();
const [{ user }] = useGlobalState();
const { error, success, warning } = useSnackbar();
const { scope, label, isDialogOpen, closeDialogCallback, path, numberOfOwners } = props;
const userID = user.id()
const close = (removed: boolean) => {
closeDialogCallback(removed);
};
const submit = () => {
permissionAPI
.updatePermissions(scope, [
User.create(
pond.User.create({
settings: pond.UserSettings.create({
id: userID
}),
permissions: []
})
)
])
.then((response: any) => {
success("Successfully left " + label);
close(true);
navigate("/" + (path !== undefined ? path : scope.kind) + "s");
})
.catch((err: any) => {
err.response.data.error
? warning(err.response.data.error)
: error("Error occured when trying to remove yourself from " + label);
close(false);
});
};
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const removeSelfDialog = () => {
return (
<React.Fragment>
<DialogTitle id="remove-self-from-object">{"Leave " + label + "?"}</DialogTitle>
<DialogContent>
<Typography color="textSecondary" align="left" variant="subtitle1">
You will no longer be able to access {label}.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => close(false)} color="primary">
Cancel
</Button>
<Button onClick={submit} color="primary" autoFocus>
Leave
</Button>
</DialogActions>
</React.Fragment>
);
};
const onlyOwnerDialog = () => {
return (
<React.Fragment>
<DialogTitle>{"Cannot Leave"}</DialogTitle>
<DialogContent>
<Typography color="textSecondary" align="left" variant="subtitle1">
You are currently the only owner. To leave, share it to another user with full
permissions to make them an owner first and then leave.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => close(false)} color="primary">
Close
</Button>
</DialogActions>
</React.Fragment>
);
};
return (
<Dialog
open={isDialogOpen}
onClose={() => close(false)}
aria-labelledby="remove-self-from-object">
{numberOfOwners && numberOfOwners <= 1 ? onlyOwnerDialog() : removeSelfDialog()}
</Dialog>
);
}

601
src/user/ShareObject.tsx Normal file
View file

@ -0,0 +1,601 @@
import {
Button,
Checkbox,
CircularProgress,
DialogActions,
DialogContent,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
IconButton,
Link,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
Tab,
Tabs,
TextField,
Theme,
Tooltip,
Typography
} from "@mui/material";
import { green, red } from "@mui/material/colors";
// import { Theme } from "@mui/material/styles/createMuiTheme";
import {
Add as AddIcon,
Link as LinkIcon,
LinkOff,
RemoveCircle as RemoveCircleIcon
} from "@mui/icons-material";
// import { MobileDateTimePicker } from "@mui/utils/pi";
import { MobileDateTimePicker } from '@mui/x-date-pickers';
import ResponsiveDialog from "common/ResponsiveDialog";
import { usePermissionAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { binScope, Scope, ShareableLink, User } from "models";
import moment, { Moment } from "moment";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { openSnackbar } from "providers/Snackbar";
// import { Status } from "@sentry/react";
import { useBinAPI, useGateAPI } from "providers";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => ({
dialogContent: {
marginTop: theme.spacing(2)
},
formContainer: {
display: "flex",
flexWrap: "wrap"
},
formControl: {
margin: theme.spacing(1),
minWidth: 160,
width: "100%"
},
emailForm: {
paddingBottom: theme.spacing(2)
},
fabContainer: {
position: "relative",
minHeight: "64px"
},
addIcon: {
color: green[500],
"&:hover": {
color: green[600]
}
},
removeIcon: {
color: red[500],
"&:hover": {
color: red[600]
}
},
grey: {
color: theme.palette.text.secondary
}
}));
interface Props {
scope: Scope;
label: string;
permissions: pond.Permission[];
isDialogOpen: boolean;
closeDialogCallback: Function;
useImitation?: boolean;
sharedUsers?: User[]; //including the users that the object/team is already shared to will prevent users from sharing again to an existing user
}
export default function ShareObject(props: Props) {
const binAPI = useBinAPI();
const classes = useStyles();
const permissionAPI = usePermissionAPI();
const { info, success, error } = useSnackbar();
const { scope, label, permissions, isDialogOpen, closeDialogCallback, sharedUsers } = props;
const [email, setEmail] = useState<string>("");
const [sharedPermissions, setSharedPermissions] = useState<pond.Permission[]>([
pond.Permission.PERMISSION_READ
]);
const [tab, setTab] = useState<number>(0);
const prevTab = usePrevious(tab);
const [isLoadingLinks, setIsLoadingLinks] = useState<boolean>(false);
const [shareableLinks, setShareableLinks] = useState<ShareableLink[]>([]);
const [newLinkExpiration, setNewLinkExpiration] = useState<Moment>(moment().add(1, "week"));
const [isNewLinkInfinite, setIsNewLinkInfinite] = useState<boolean>(false);
const [openExistingUser, setOpenExistingUser] = useState(false);
const [useImitation] = useState<boolean>(
props.useImitation !== undefined ? props.useImitation : true
);
const gateAPI = useGateAPI();
const share = () => {
permissionAPI
.shareObject(scope, email, sharedPermissions, useImitation)
.then((result: any) => {
let shareBins = true;
if (result && result.data && result.data.existing) {
success(label + " was shared with " + email);
} else {
success(email + " was sent an email to sign up");
shareBins = false;
}
let successBins = true;
if (scope.kind === "binyard" && shareBins) {
binAPI.listBins(1000, 0, "asc", "name", scope.key).then(resp => {
resp.data.bins.forEach(bin => {
if (bin.settings) {
let newScope = binScope(bin.settings?.key);
permissionAPI.shareObject(newScope, email, sharedPermissions).catch(err => {
successBins = false;
});
}
});
});
if (!successBins) {
openSnackbar("error", "One or more bins failed to share with " + email);
} else {
success("Bins shared with " + email);
}
}
let successGates = true;
if (scope.kind === "terminal" && shareBins) {
gateAPI
.listGates(
1000,
0,
undefined,
undefined,
undefined,
undefined,
[scope.key],
[scope.kind]
)
.then(resp => {
resp.data.gates.forEach(gate => {
if (gate) {
let newScope = { key: gate.key, kind: "gate" } as Scope;
permissionAPI.shareObject(newScope, email, sharedPermissions).catch(err => {
successGates = false;
});
}
});
});
if (!successGates) {
openSnackbar("error", "One or more Gates failed to share");
} else {
success("Gates shared");
}
}
closeAfterShare();
})
.catch((err: any) => {
error("Unable to share " + label + " with " + email);
close();
});
};
const loadShareableLinks = useCallback(() => {
setIsLoadingLinks(true);
permissionAPI
.listShareableLinks(scope)
.then((response: any) => {
let rawShareableLinks = response.data.links ? response.data.links : [];
let rShareableLinks = rawShareableLinks.map((rawShareableLink: any) =>
ShareableLink.any(rawShareableLink)
);
setShareableLinks(rShareableLinks);
})
.catch((error: any) => {
setShareableLinks([]);
console.log("Error occured while loading shareable links");
})
.finally(() => {
setIsLoadingLinks(false);
});
}, [permissionAPI, scope]);
useEffect(() => {
if (tab === 1 && prevTab !== tab) {
loadShareableLinks();
}
}, [loadShareableLinks, prevTab, tab]);
const getNeverExpires = () => {
return "";
};
const createShareableLink = () => {
const expiration = isNewLinkInfinite ? getNeverExpires() : newLinkExpiration.toISOString();
permissionAPI
.addShareableLink(scope, expiration)
.then((response: any) => {
loadShareableLinks();
})
.catch((error: any) => {
console.log("Error occured while creating shareable link");
});
};
const revokeShareableLink = (code: string) => {
permissionAPI
.removeShareableLink(scope, code)
.then((response: any) => {
loadShareableLinks();
})
.catch((error: any) => {
console.log("Error occured while removing the shareable link");
});
};
const getShareableLinkURL = (code: string): string => {
const base = window.location.origin;
return base + "/" + scope.kind + "s/" + code;
};
const copyShareableLinkToClipboard = (code: string) => {
const url = getShareableLinkURL(code);
navigator.clipboard.writeText(url);
info("Copied link to clipboard");
};
const closeAfterShare = () => {
closeDialogCallback(true);
};
const close = () => {
closeDialogCallback(false);
};
const changeEmail = (event: any) => {
setEmail(event.target.value);
};
const changePermissions = (event: any) => {
let updatedSharedPermissions: Array<pond.Permission> = cloneDeep(sharedPermissions);
let permission: pond.Permission;
switch (event.target.value) {
case "1":
permission = pond.Permission.PERMISSION_USERS;
break;
case "2":
permission = pond.Permission.PERMISSION_READ;
break;
case "3":
permission = pond.Permission.PERMISSION_WRITE;
break;
case "4":
permission = pond.Permission.PERMISSION_SHARE;
break;
case "5":
permission = pond.Permission.PERMISSION_BILLING;
break;
case "6":
permission = pond.Permission.PERMISSION_FILE_MANAGEMENT;
break;
default:
permission = pond.Permission.PERMISSION_INVALID;
break;
}
if (updatedSharedPermissions.includes(permission)) {
updatedSharedPermissions = updatedSharedPermissions.filter(
(curPermission: pond.Permission) => curPermission !== permission
);
} else {
updatedSharedPermissions.push(permission);
}
setSharedPermissions(updatedSharedPermissions);
};
const changeTab = (event: any, newTab: number) => {
setTab(newTab);
};
const isValid = () => {
return email.trim() !== "" && sharedPermissions.length > 0;
};
// UI BEGINS
const emailForm = () => {
return (
<TextField
id="email"
name="email"
label="Email"
value={email}
onChange={changeEmail}
margin="normal"
fullWidth
variant="outlined"
className={classes.emailForm}
/>
);
};
const permissionsForm = () => {
return (
<FormControl component="fieldset" fullWidth variant="outlined">
<FormLabel component="legend">Permissions</FormLabel>
<FormGroup>
{permissions.includes(pond.Permission.PERMISSION_READ) && (
<FormControlLabel
control={
<Checkbox
disabled={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
checked={sharedPermissions.includes(pond.Permission.PERMISSION_READ)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_READ as pond.Permission}
/>
}
label="View"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_WRITE)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
/>
}
label="Edit"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_SHARE)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
/>
}
label="Share"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_USERS)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_USERS as pond.Permission}
/>
}
label="Manage Users"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
/>
}
label="Files"
labelPlacement="end"
/>
)}
{permissions.includes(pond.Permission.PERMISSION_BILLING) && (
<FormControlLabel
control={
<Checkbox
checked={sharedPermissions.includes(pond.Permission.PERMISSION_BILLING)}
onChange={changePermissions}
value={pond.Permission.PERMISSION_BILLING as pond.Permission}
/>
}
label="Billing"
labelPlacement="end"
/>
)}
</FormGroup>
</FormControl>
);
};
const emailSharing = () => {
return (
<React.Fragment>
{emailForm()}
{permissionsForm()}
</React.Fragment>
);
};
const existingUserDialog = () => {
return (
<ResponsiveDialog
open={openExistingUser}
onClose={() => {
setOpenExistingUser(false);
close();
}}>
<DialogTitle>User Already exists</DialogTitle>
<DialogContent>This user is already part of {label}.</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenExistingUser(false);
close();
}}>
Close
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const shareableLinksList = () => {
const now = moment();
let items: any[] = [];
shareableLinks.forEach((link: ShareableLink) => {
const expiration = moment(link.settings.expiration);
const neverExpires = link.settings.expiration === getNeverExpires();
const expired = !neverExpires && expiration < now;
items.push(
<ListItem key={link.key()}>
{expired ? (
<IconButton disabled>
<LinkOff />
</IconButton>
) : (
<Tooltip title="Click to copy link">
<IconButton onClick={() => copyShareableLinkToClipboard(link.key())}>
<LinkIcon />
</IconButton>
</Tooltip>
)}
<ListItemText
primary={
expired ? (
<span className={classes.grey}>{"/devices/" + link.key()}</span>
) : (
<Link href={getShareableLinkURL(link.key())} target="_blank">
{"/devices/" + link.key()}
</Link>
)
}
secondary={
neverExpires ? (
"Never expires"
) : (
<Tooltip title={expiration.calendar()}>
<span>{(expired ? "Expired " : "Expires ") + expiration.fromNow()}</span>
</Tooltip>
)
}
/>
<ListItemSecondaryAction>
<Tooltip title="Revoke shareable link">
<IconButton onClick={() => revokeShareableLink(link.key())}>
<RemoveCircleIcon className={classes.removeIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
});
return (
<List>
{isLoadingLinks ? <CircularProgress /> : <React.Fragment>{items}</React.Fragment>}
{shareableLinks.length > 0 && <Divider variant="middle" />}
<ListItem>
<FormGroup row>
<MobileDateTimePicker
disabled={isNewLinkInfinite}
// inputRef={props => <TextField {...props} helperText="" />}
label="Expiration Date"
value={newLinkExpiration}
onChange={date => setNewLinkExpiration(date as Moment)}
disablePast
/>
<FormControlLabel
control={
<Checkbox
checked={isNewLinkInfinite}
onChange={(_, checked) => setIsNewLinkInfinite(checked)}
value="isNewLinkInfinite"
color="secondary"
/>
}
label="Never expires"
/>
</FormGroup>
<ListItemSecondaryAction>
<Tooltip title="Create new shareable link">
<IconButton
className={classes.addIcon}
color="default"
onClick={() => createShareableLink()}>
<AddIcon className={classes.addIcon} />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
</List>
);
};
const content = () => {
switch (tab) {
case 1:
return <React.Fragment>{shareableLinksList()}</React.Fragment>;
default:
return <React.Fragment>{emailSharing()}</React.Fragment>;
}
};
return (
<React.Fragment>
{existingUserDialog()}
<ResponsiveDialog
maxWidth="sm"
fullWidth
open={isDialogOpen}
onClose={close}
aria-labelledby="share-dialog-title">
<DialogTitle id="share-dialog-title">
Share
<Typography variant="body2" color="textSecondary">
{label}
</Typography>
</DialogTitle>
{scope.kind === "device" && (
<Tabs value={tab} onChange={changeTab}>
<Tab label="Share by Email" />
<Tab label="Shareable Links" />
</Tabs>
)}
<Divider />
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
<DialogActions>
<Button onClick={close} color="primary">
Cancel
</Button>
{tab === 0 && (
<Button
onClick={() => {
//check here if the email they entered is already in the list of shared users if it was passed in
let hasUser = false;
sharedUsers?.forEach(user => {
if (user.settings.email === email) hasUser = true;
});
if (hasUser) {
setOpenExistingUser(true);
} else {
share();
}
}}
color="primary"
disabled={!isValid()}>
Share
</Button>
)}
</DialogActions>
</ResponsiveDialog>
</React.Fragment>
);
}

32
src/utils/environment.ts Normal file
View file

@ -0,0 +1,32 @@
function getEnvironment() {
switch (process.env.NODE_ENV) {
case "development":
return "development";
case "production":
if (window.location.hostname.includes("staging")) {
return "staging";
} else {
return "production";
}
case "test":
return "test";
default:
return "";
}
}
export function isOffline() {
return process.env.REACT_APP_IS_OFFLINE === "true";
}
export function isDevelopment() {
return getEnvironment() === "development";
}
export function isProduction() {
return getEnvironment() === "production";
}
export function isStaging() {
return getEnvironment() === "staging";
}