device settings and actions made functional
This commit is contained in:
parent
3ce00facd1
commit
4e7c68401b
19 changed files with 3042 additions and 41 deletions
25
package-lock.json
generated
25
package-lock.json
generated
|
|
@ -14,7 +14,7 @@
|
||||||
"@mui/icons-material": "^6.1.6",
|
"@mui/icons-material": "^6.1.6",
|
||||||
"@mui/material": "^6.1.6",
|
"@mui/material": "^6.1.6",
|
||||||
"@mui/styles": "^6.1.6",
|
"@mui/styles": "^6.1.6",
|
||||||
"@mui/x-date-pickers": "^7.22.3",
|
"@mui/x-date-pickers": "^7.23.6",
|
||||||
"@sentry/react": "^8.38.0",
|
"@sentry/react": "^8.38.0",
|
||||||
"@types/classnames": "^2.3.0",
|
"@types/classnames": "^2.3.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
|
@ -1526,12 +1526,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-date-pickers": {
|
"node_modules/@mui/x-date-pickers": {
|
||||||
"version": "7.22.3",
|
"version": "7.23.6",
|
||||||
"integrity": "sha512-shNp92IrST5BiVy2f4jbrmRaD32QhyUthjh1Oexvpcn0v6INyuWgxfodoTi5ZCnE5Ue5UVFSs4R9Xre0UbJ5DQ==",
|
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.23.6.tgz",
|
||||||
|
"integrity": "sha512-jt6rEAYLju3NZe3y2S+I5KcTiSHV79FW0jeNUEUTceg1qsPzseHbND66k3zVF0hO3N2oZtLtPywof6vN5Doe+Q==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.25.7",
|
"@babel/runtime": "^7.25.7",
|
||||||
"@mui/utils": "^5.16.6 || ^6.0.0",
|
"@mui/utils": "^5.16.6 || ^6.0.0",
|
||||||
"@mui/x-internals": "7.21.0",
|
"@mui/x-internals": "7.23.6",
|
||||||
"@types/react-transition-group": "^4.4.11",
|
"@types/react-transition-group": "^4.4.11",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
|
@ -1550,14 +1552,14 @@
|
||||||
"@mui/material": "^5.15.14 || ^6.0.0",
|
"@mui/material": "^5.15.14 || ^6.0.0",
|
||||||
"@mui/system": "^5.15.14 || ^6.0.0",
|
"@mui/system": "^5.15.14 || ^6.0.0",
|
||||||
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
|
"date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
|
||||||
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0",
|
"date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
"luxon": "^3.0.2",
|
"luxon": "^3.0.2",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"moment-hijri": "^2.1.2 || ^3.0.0",
|
"moment-hijri": "^2.1.2 || ^3.0.0",
|
||||||
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@emotion/react": {
|
"@emotion/react": {
|
||||||
|
|
@ -1590,9 +1592,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-internals": {
|
"node_modules/@mui/x-internals": {
|
||||||
"version": "7.21.0",
|
"version": "7.23.6",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.23.6.tgz",
|
||||||
"integrity": "sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ==",
|
"integrity": "sha512-hT1Pa4PNCnxwiauPbYMC3p4DiEF1x05Iu4C1MtC/jMJ1LtthymLmTuQ6ZQ53/R9FeqK6sYd6A6noR+vNMjp5DA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.25.7",
|
"@babel/runtime": "^7.25.7",
|
||||||
"@mui/utils": "^5.16.6 || ^6.0.0"
|
"@mui/utils": "^5.16.6 || ^6.0.0"
|
||||||
|
|
@ -1605,7 +1608,7 @@
|
||||||
"url": "https://opencollective.com/mui-org"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^17.0.0 || ^18.0.0"
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"@mui/icons-material": "^6.1.6",
|
"@mui/icons-material": "^6.1.6",
|
||||||
"@mui/material": "^6.1.6",
|
"@mui/material": "^6.1.6",
|
||||||
"@mui/styles": "^6.1.6",
|
"@mui/styles": "^6.1.6",
|
||||||
"@mui/x-date-pickers": "^7.22.3",
|
"@mui/x-date-pickers": "^7.23.6",
|
||||||
"@sentry/react": "^8.38.0",
|
"@sentry/react": "^8.38.0",
|
||||||
"@types/classnames": "^2.3.0",
|
"@types/classnames": "^2.3.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
|
|
|
||||||
308
src/common/LinearMutationBuilder.tsx
Normal file
308
src/common/LinearMutationBuilder.tsx
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Button,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid2 as Grid,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
MenuItem,
|
||||||
|
TextField
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ExpandMore } from "@mui/icons-material";
|
||||||
|
import { Component } from "models";
|
||||||
|
import { pond, quack } from "protobuf-ts/pond";
|
||||||
|
import React, { useCallback, useState, useEffect } from "react";
|
||||||
|
import ResponsiveDialog from "./ResponsiveDialog";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
existingMutation?: pond.LinearMutation;
|
||||||
|
componentsByDevice: Map<string, Component[]>; //string being device id and component array of that devices components
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (mutation: pond.LinearMutation) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LinearMutationBuilder(props: Props) {
|
||||||
|
const { open, onClose, componentsByDevice, onSubmit, existingMutation } = props;
|
||||||
|
const [coefficientSets, setCoefficientSets] = useState<pond.ComponentCoefficients[]>([]);
|
||||||
|
const [devOptions, setDevOptions] = useState<string[]>([]);
|
||||||
|
const [compOptions, setCompOptions] = useState<Component[][]>([]);
|
||||||
|
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||||
|
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
|
||||||
|
const [selectedMutation, setSelectedMutation] = useState(pond.Mutator.MUTATOR_NONE);
|
||||||
|
const [mutationConstant, setMutationConstant] = useState(0);
|
||||||
|
const [mutationName, setMutationName] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDevOptions(Array.from(componentsByDevice.keys()));
|
||||||
|
setMutationName("");
|
||||||
|
setSelectedMutation(pond.Mutator.MUTATOR_NONE);
|
||||||
|
setCoefficientSets([]);
|
||||||
|
setMutationConstant(0);
|
||||||
|
setSelectedComponents([]);
|
||||||
|
setSelectedDevices([]);
|
||||||
|
setCompOptions([]);
|
||||||
|
if (existingMutation) {
|
||||||
|
setMutationName(existingMutation.mutationName);
|
||||||
|
setSelectedMutation(existingMutation.mutation);
|
||||||
|
setCoefficientSets(existingMutation.componentCoefficients);
|
||||||
|
setMutationConstant(existingMutation.constant);
|
||||||
|
let co: Component[][] = [];
|
||||||
|
let sc: string[] = [];
|
||||||
|
let sd: string[] = [];
|
||||||
|
existingMutation.componentCoefficients.forEach(compCo => {
|
||||||
|
let splitString = compCo.componentKey.split(":", 2);
|
||||||
|
if (splitString.length > 1) {
|
||||||
|
sd.push(splitString[0]);
|
||||||
|
sc.push(splitString[1]);
|
||||||
|
let c = componentsByDevice.get(splitString[0]);
|
||||||
|
if (c) {
|
||||||
|
co.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setSelectedComponents(sc);
|
||||||
|
setSelectedDevices(sd);
|
||||||
|
setCompOptions(co);
|
||||||
|
}
|
||||||
|
}, [componentsByDevice, existingMutation]);
|
||||||
|
|
||||||
|
const addNewSet = () => {
|
||||||
|
let cs = coefficientSets;
|
||||||
|
let sd = selectedDevices;
|
||||||
|
let co = compOptions;
|
||||||
|
let sc = selectedComponents;
|
||||||
|
|
||||||
|
cs.push(pond.ComponentCoefficients.create());
|
||||||
|
sd.push("");
|
||||||
|
co.push([]);
|
||||||
|
sc.push("");
|
||||||
|
|
||||||
|
setCoefficientSets([...cs]);
|
||||||
|
setSelectedDevices([...sd]);
|
||||||
|
setCompOptions([...co]);
|
||||||
|
setSelectedComponents([...sc]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOptionsFor = (pos: number, dev: string) => {
|
||||||
|
let co = compOptions;
|
||||||
|
let components = componentsByDevice.get(dev);
|
||||||
|
if (components) {
|
||||||
|
co[pos] = components;
|
||||||
|
}
|
||||||
|
setCompOptions([...co]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeCoefficientDisplay = (
|
||||||
|
deviceID: string,
|
||||||
|
componentKey: string,
|
||||||
|
set: pond.ComponentCoefficients
|
||||||
|
) => {
|
||||||
|
set.componentKey = deviceID + ":" + componentKey; //note: changing the set that is passed in is changing the state variable
|
||||||
|
let componentList = componentsByDevice.get(deviceID);
|
||||||
|
let component: Component = Component.create();
|
||||||
|
if (componentList) {
|
||||||
|
componentList.forEach(comp => {
|
||||||
|
if (comp.key() === componentKey) {
|
||||||
|
component = comp;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let nodeList: JSX.Element[] = [];
|
||||||
|
component.lastMeasurement.forEach((um, k) => {
|
||||||
|
if (um.values[0]) {
|
||||||
|
if (set.measurementCoefficients[k] === undefined) {
|
||||||
|
set.measurementCoefficients[k] = pond.MeasurementCoefficients.create({
|
||||||
|
coefficients: [],
|
||||||
|
measurementType: um.type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (let i = 0; i < um.values[0].values.length; i++) {
|
||||||
|
//default to 0 if it isn't set
|
||||||
|
if (!set.measurementCoefficients[k].coefficients[i]) {
|
||||||
|
set.measurementCoefficients[k].coefficients[i] = 0;
|
||||||
|
}
|
||||||
|
nodeList.push(
|
||||||
|
<ListItem key={componentKey + "node" + i + um.type}>
|
||||||
|
<Grid container direction="row" alignContent="center" alignItems="center">
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
Node {i + 1} ({quack.MeasurementType[um.type]})
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
value={set.measurementCoefficients[k].coefficients[i]}
|
||||||
|
type="number"
|
||||||
|
onChange={e => {
|
||||||
|
set.measurementCoefficients[k].coefficients[i] = +e.target.value;
|
||||||
|
setCoefficientSets([...coefficientSets]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <List>{nodeList}</List>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const displaySets = useCallback(() => {
|
||||||
|
let setDisplay: JSX.Element[] = [];
|
||||||
|
coefficientSets.forEach((set, i) => {
|
||||||
|
setDisplay.push(
|
||||||
|
<ListItem key={i} style={{ width: 450 }}>
|
||||||
|
<Grid container direction="column">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
alignContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between">
|
||||||
|
<Grid size={{ xs: 5 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={"Device"}
|
||||||
|
select
|
||||||
|
value={selectedDevices[i]}
|
||||||
|
onChange={event => {
|
||||||
|
let sd = selectedDevices;
|
||||||
|
sd[i] = event.target.value as string;
|
||||||
|
setSelectedDevices([...sd]);
|
||||||
|
setOptionsFor(i, event.target.value as string);
|
||||||
|
}}>
|
||||||
|
{devOptions.map(dev => (
|
||||||
|
<MenuItem key={dev} value={dev}>
|
||||||
|
{dev}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 5 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={"Component"}
|
||||||
|
select
|
||||||
|
value={selectedComponents[i]}
|
||||||
|
onChange={event => {
|
||||||
|
let sc = selectedComponents;
|
||||||
|
sc[i] = event.target.value as string;
|
||||||
|
setSelectedComponents([...sc]);
|
||||||
|
set.measurementCoefficients = [];
|
||||||
|
}}>
|
||||||
|
{compOptions[i] &&
|
||||||
|
compOptions[i].map(comp => (
|
||||||
|
<MenuItem key={comp.key()} value={comp.key()}>
|
||||||
|
{comp.name()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{selectedComponents[i] !== "" && (
|
||||||
|
<Grid>
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMore />}>Node Coefficients</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{nodeCoefficientDisplay(selectedDevices[i], selectedComponents[i], set)}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return <List>{setDisplay}</List>;
|
||||||
|
}, [coefficientSets, selectedDevices, compOptions, devOptions, selectedComponents]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog open={open} onClose={() => onClose()}>
|
||||||
|
<DialogTitle>Build New Mutation</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
style={{ marginBottom: 10 }}
|
||||||
|
value={mutationName}
|
||||||
|
fullWidth
|
||||||
|
label={"Mutation Name"}
|
||||||
|
onChange={e => {
|
||||||
|
setMutationName(e.target.value as string);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
style={{ marginBottom: 10 }}
|
||||||
|
value={selectedMutation}
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
label={"Mutation Type"}
|
||||||
|
onChange={e => {
|
||||||
|
setSelectedMutation(+e.target.value);
|
||||||
|
}}>
|
||||||
|
<MenuItem key={pond.Mutator.MUTATOR_NONE} value={pond.Mutator.MUTATOR_NONE}>
|
||||||
|
Select Type of Calculated Measurement
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key={pond.Mutator.MUTATOR_FUEL_LEVEL} value={pond.Mutator.MUTATOR_FUEL_LEVEL}>
|
||||||
|
Fuel Level
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
style={{ marginBottom: 10 }}
|
||||||
|
value={mutationConstant}
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
label={"Mutation Constant"}
|
||||||
|
onChange={e => {
|
||||||
|
setMutationConstant(+e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{displaySets()}
|
||||||
|
<Button variant="contained" color="primary" onClick={addNewSet}>
|
||||||
|
Add Component
|
||||||
|
</Button>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{existingMutation ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
existingMutation.mutationName = mutationName;
|
||||||
|
existingMutation.constant = mutationConstant;
|
||||||
|
existingMutation.mutation = selectedMutation;
|
||||||
|
existingMutation.componentCoefficients = coefficientSets;
|
||||||
|
onClose();
|
||||||
|
}}>
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button color="primary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
disabled={selectedMutation === pond.Mutator.MUTATOR_NONE}
|
||||||
|
onClick={() => {
|
||||||
|
let newMutation = pond.LinearMutation.create();
|
||||||
|
newMutation.mutation = selectedMutation;
|
||||||
|
newMutation.componentCoefficients = coefficientSets;
|
||||||
|
newMutation.constant = mutationConstant;
|
||||||
|
newMutation.mutationName = mutationName;
|
||||||
|
onSubmit(newMutation);
|
||||||
|
onClose();
|
||||||
|
}}>
|
||||||
|
Add Mutation
|
||||||
|
</Button>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,10 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
Theme
|
Theme
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
// import { red } from "@mui/icons-materia";
|
|
||||||
// import { createStyles, makeStyles, Theme, withStyles } from "@material-ui/core/styles";
|
|
||||||
import { Delete as DeleteIcon } from "@mui/icons-material";
|
import { Delete as DeleteIcon } from "@mui/icons-material";
|
||||||
import ColourPicker from "common/ColourPicker";
|
import ColourPicker from "common/ColourPicker";
|
||||||
import { Tag as TagModel } from "models";
|
import { Tag as TagModel } from "models";
|
||||||
import { useGlobalState, useTagAPI, useSnackbar } from "providers";
|
import { useTagAPI, useSnackbar } from "providers";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Tag as TagUI } from "./Tag";
|
import { Tag as TagUI } from "./Tag";
|
||||||
import { red } from "@mui/material/colors";
|
import { red } from "@mui/material/colors";
|
||||||
|
|
@ -92,7 +90,7 @@ interface Props {
|
||||||
mode: "add" | "update";
|
mode: "add" | "update";
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteButton = withStyles((theme: Theme) => ({
|
const DeleteButton = withStyles((_theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
color: "#FFF",
|
color: "#FFF",
|
||||||
backgroundColor: red[500],
|
backgroundColor: red[500],
|
||||||
|
|
@ -133,7 +131,7 @@ export default function TagSettings(props: Props) {
|
||||||
default:
|
default:
|
||||||
tagAPI
|
tagAPI
|
||||||
.updateTag(tag.settings.key, tag.settings)
|
.updateTag(tag.settings.key, tag.settings)
|
||||||
.then((response: any) => {
|
.then((_response: any) => {
|
||||||
// dispatch({
|
// dispatch({
|
||||||
// key: "tags",
|
// key: "tags",
|
||||||
// value: tags.map(t => {
|
// value: tags.map(t => {
|
||||||
|
|
@ -162,7 +160,7 @@ export default function TagSettings(props: Props) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (refresh?: boolean) => {
|
const handleClose = (_refresh?: boolean) => {
|
||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
setTag(new TagModel());
|
setTag(new TagModel());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
src/common/time/DateRange.ts
Normal file
60
src/common/time/DateRange.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import moment, { Moment } from "moment";
|
||||||
|
|
||||||
|
export type DateRangePreset =
|
||||||
|
| "live"
|
||||||
|
| "pastHour"
|
||||||
|
| "pastTwelveHours"
|
||||||
|
| "pastDay"
|
||||||
|
| "pastWeek"
|
||||||
|
| "pastMonth"
|
||||||
|
| "selectRange";
|
||||||
|
|
||||||
|
export interface DateRange {
|
||||||
|
start: Moment;
|
||||||
|
end: Moment;
|
||||||
|
live?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetDefaultDateRange = (): DateRange => {
|
||||||
|
let preset: DateRangePreset = getDefaultPreset();
|
||||||
|
const now = moment();
|
||||||
|
let start = moment().subtract(1, "days");
|
||||||
|
let end = now;
|
||||||
|
let live = false;
|
||||||
|
switch (preset) {
|
||||||
|
case "live":
|
||||||
|
live = true;
|
||||||
|
break;
|
||||||
|
case "pastTwelveHours":
|
||||||
|
start = moment().subtract(12, "hours");
|
||||||
|
end = now;
|
||||||
|
break;
|
||||||
|
case "pastDay":
|
||||||
|
start = moment().subtract(1, "days");
|
||||||
|
end = now;
|
||||||
|
break;
|
||||||
|
case "pastWeek":
|
||||||
|
start = moment().subtract(7, "days");
|
||||||
|
end = now;
|
||||||
|
break;
|
||||||
|
case "pastMonth":
|
||||||
|
start = moment().subtract(1, "months");
|
||||||
|
end = now;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
//default to past hour
|
||||||
|
start = moment().subtract(1, "hours");
|
||||||
|
end = now;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return { start, end, live } as DateRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDefaultPreset(): DateRangePreset {
|
||||||
|
let preset: string | null = localStorage.getItem("dateRangePreset");
|
||||||
|
return preset ? (preset as DateRangePreset) : "pastHour";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetDefaultPreset(preset: DateRangePreset) {
|
||||||
|
localStorage.setItem("dateRangePreset", preset);
|
||||||
|
}
|
||||||
280
src/common/time/DateSelect.tsx
Normal file
280
src/common/time/DateSelect.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
createStyles,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
OutlinedInput,
|
||||||
|
Select,
|
||||||
|
Theme
|
||||||
|
} from "@mui/material";
|
||||||
|
// import { DateRange, StaticDateRangePicker, DateRangeDelimiter } from "@material-ui/pickers";
|
||||||
|
import moment, { Moment } from "moment";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { DateRangePreset, SetDefaultPreset } from "./DateRange";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { withStyles, WithStyles } from "@mui/styles";
|
||||||
|
|
||||||
|
const styles = (_theme: Theme) => createStyles({});
|
||||||
|
|
||||||
|
interface Props extends WithStyles<typeof styles> {
|
||||||
|
startDate: Moment;
|
||||||
|
endDate: Moment;
|
||||||
|
live?: boolean;
|
||||||
|
updateDateRange: (start: Moment, end: Moment, live: boolean) => void;
|
||||||
|
label?: string;
|
||||||
|
allowLive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DateRange<TDate> = [TDate | null, TDate | null];
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
selectMenuOpen: boolean;
|
||||||
|
dateRangeSelect: DateRangePreset;
|
||||||
|
dateRangeDialogOpen: boolean;
|
||||||
|
labelWidth: number;
|
||||||
|
dateRange: DateRange<Moment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateSelect extends React.Component<Props, State> {
|
||||||
|
InputLabelRef: any;
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
const { startDate, endDate, live } = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
selectMenuOpen: false,
|
||||||
|
dateRangeSelect: this.determineSelectOption(startDate, endDate, live),
|
||||||
|
dateRangeDialogOpen: false,
|
||||||
|
labelWidth: 0,
|
||||||
|
dateRange: [startDate, endDate]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
determineSelectOption = (
|
||||||
|
startDate?: Moment,
|
||||||
|
endDate?: Moment,
|
||||||
|
live?: boolean
|
||||||
|
): DateRangePreset => {
|
||||||
|
if (live) return "live";
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
return "pastDay";
|
||||||
|
}
|
||||||
|
|
||||||
|
let hourDiff = moment(endDate)
|
||||||
|
.diff(moment(startDate), "hours", true)
|
||||||
|
.toFixed(4);
|
||||||
|
if (hourDiff === "1.0000") {
|
||||||
|
return "pastHour";
|
||||||
|
}
|
||||||
|
if (hourDiff === "12.0000") {
|
||||||
|
return "pastTwelveHours";
|
||||||
|
}
|
||||||
|
|
||||||
|
let dayDiff = moment(endDate)
|
||||||
|
.diff(moment(startDate), "days", true)
|
||||||
|
.toFixed(4);
|
||||||
|
if (dayDiff === "1.0000") {
|
||||||
|
return "pastDay";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayDiff === "7.0000") {
|
||||||
|
return "pastWeek";
|
||||||
|
}
|
||||||
|
|
||||||
|
let monthDiff = moment(endDate)
|
||||||
|
.diff(moment(startDate), "months", true)
|
||||||
|
.toFixed(4);
|
||||||
|
if (monthDiff === "1.0000") {
|
||||||
|
return "pastMonth";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "selectRange";
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let labelRef: any = ReactDOM.findDOMNode(this.InputLabelRef);
|
||||||
|
if (typeof labelRef !== "undefined" && labelRef !== null) {
|
||||||
|
this.setState({
|
||||||
|
labelWidth: labelRef.offsetWidth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate = (prevProps: Props) => {
|
||||||
|
const { startDate, endDate, live } = this.props;
|
||||||
|
if (
|
||||||
|
prevProps.startDate !== startDate ||
|
||||||
|
prevProps.endDate !== endDate ||
|
||||||
|
prevProps.live !== live
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
dateRange: [startDate, endDate],
|
||||||
|
dateRangeSelect: this.determineSelectOption(startDate, endDate, live)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
changeDateRangeSelect = (selection: DateRangePreset) => {
|
||||||
|
let dateRangeDialogOpen = false;
|
||||||
|
|
||||||
|
let start, end;
|
||||||
|
switch (selection) {
|
||||||
|
case "live":
|
||||||
|
SetDefaultPreset(selection);
|
||||||
|
this.props.updateDateRange(moment(), moment(), true);
|
||||||
|
break;
|
||||||
|
case "pastHour":
|
||||||
|
SetDefaultPreset(selection);
|
||||||
|
start = moment().subtract(1, "hours");
|
||||||
|
end = moment();
|
||||||
|
this.props.updateDateRange(start, end, false);
|
||||||
|
break;
|
||||||
|
case "pastTwelveHours":
|
||||||
|
SetDefaultPreset(selection);
|
||||||
|
start = moment().subtract(12, "hours");
|
||||||
|
end = moment();
|
||||||
|
this.props.updateDateRange(start, end, false);
|
||||||
|
break;
|
||||||
|
case "pastDay":
|
||||||
|
SetDefaultPreset(selection);
|
||||||
|
start = moment().subtract(1, "days");
|
||||||
|
end = moment();
|
||||||
|
this.props.updateDateRange(start, end, false);
|
||||||
|
break;
|
||||||
|
case "pastWeek":
|
||||||
|
SetDefaultPreset(selection);
|
||||||
|
start = moment().subtract(7, "days");
|
||||||
|
end = moment();
|
||||||
|
this.props.updateDateRange(start, end, false);
|
||||||
|
break;
|
||||||
|
case "pastMonth":
|
||||||
|
SetDefaultPreset(selection);
|
||||||
|
start = moment().subtract(1, "months");
|
||||||
|
end = moment();
|
||||||
|
this.props.updateDateRange(start, end, false);
|
||||||
|
break;
|
||||||
|
case "selectRange":
|
||||||
|
dateRangeDialogOpen = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
dateRangeSelect: selection,
|
||||||
|
dateRangeDialogOpen: dateRangeDialogOpen
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDateRange = (dateRange: DateRange<Moment>) => {
|
||||||
|
this.setState({ dateRange });
|
||||||
|
};
|
||||||
|
|
||||||
|
submitCustomRange = () => {
|
||||||
|
const { dateRange } = this.state;
|
||||||
|
const startDate = dateRange[0] ? dateRange[0].clone() : moment();
|
||||||
|
const endDate = dateRange[1] ? dateRange[1].clone() : moment();
|
||||||
|
this.props.updateDateRange(startDate, endDate, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmitDateRange = () => {
|
||||||
|
this.submitCustomRange();
|
||||||
|
this.closeDateRangeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
closeDateRangeDialog = () => {
|
||||||
|
this.setState({ dateRangeDialogOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
customRangeLabel = (dateRange: DateRange<Moment>) => {
|
||||||
|
const startDate = dateRange[0] ? dateRange[0].clone() : moment();
|
||||||
|
const endDate = dateRange[1] ? dateRange[1].clone() : moment();
|
||||||
|
const sameYear = startDate.get("year") === endDate.get("year");
|
||||||
|
|
||||||
|
return sameYear
|
||||||
|
? startDate.format("MMM Do") + " to " + endDate.format("MMM Do")
|
||||||
|
: startDate.format("MMM Do, Y") + " to " + endDate.format("MMM Do, Y");
|
||||||
|
};
|
||||||
|
|
||||||
|
openSelectMenu = () => {
|
||||||
|
this.setState({ selectMenuOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
closeSelectMenu = () => {
|
||||||
|
this.setState({ selectMenuOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, allowLive } = this.props;
|
||||||
|
const {
|
||||||
|
selectMenuOpen,
|
||||||
|
dateRangeSelect,
|
||||||
|
dateRangeDialogOpen,
|
||||||
|
dateRange,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<FormControl margin="normal" variant="outlined" fullWidth>
|
||||||
|
<InputLabel
|
||||||
|
ref={ref => {
|
||||||
|
this.InputLabelRef = ref;
|
||||||
|
}}
|
||||||
|
htmlFor="select-date-range">
|
||||||
|
{label !== undefined ? label : "Show"}
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
open={selectMenuOpen}
|
||||||
|
onClose={this.closeSelectMenu}
|
||||||
|
onOpen={this.openSelectMenu}
|
||||||
|
value={dateRangeSelect}
|
||||||
|
onChange={event => this.changeDateRangeSelect(event.target.value as DateRangePreset)}
|
||||||
|
input={<OutlinedInput /*labelWidth={labelWidth}*/ />}>
|
||||||
|
{allowLive && <MenuItem value="live">Live</MenuItem>}
|
||||||
|
<MenuItem value="pastHour">Past Hour</MenuItem>
|
||||||
|
<MenuItem value="pastTwelveHours">Past 12 Hours</MenuItem>
|
||||||
|
<MenuItem value="pastDay">Past Day</MenuItem>
|
||||||
|
<MenuItem value="pastWeek">Past Week</MenuItem>
|
||||||
|
<MenuItem value="pastMonth">Past Month</MenuItem>
|
||||||
|
<MenuItem value="selectRange">
|
||||||
|
{selectMenuOpen ? "Select Range" : this.customRangeLabel(dateRange)}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={dateRangeDialogOpen}
|
||||||
|
onClose={this.closeDateRangeDialog}
|
||||||
|
aria-labelledby="date-range-dialog">
|
||||||
|
<DialogContent>
|
||||||
|
{/* <StaticDateRangePicker
|
||||||
|
disableFuture
|
||||||
|
value={dateRange}
|
||||||
|
onChange={date => this.updateDateRange(date)}
|
||||||
|
renderInput={(startProps, endProps) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<TextField {...startProps} />
|
||||||
|
<DateRangeDelimiter> to </DateRangeDelimiter>
|
||||||
|
<TextField {...endProps} />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={this.closeDateRangeDialog} color="primary">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button onClick={this.handleSubmitDateRange} color="primary">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(DateSelect);
|
||||||
79
src/common/time/PeriodSelect.tsx
Normal file
79
src/common/time/PeriodSelect.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { InputAdornment, MenuItem, TextField } from "@mui/material";
|
||||||
|
import { usePrevious } from "hooks";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { TimeUnit, milliToX, bestUnit, xToMilli } from "./duration";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onChange: (ms: number) => void;
|
||||||
|
units: TimeUnit[];
|
||||||
|
initialMs?: number;
|
||||||
|
id?: string;
|
||||||
|
label?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PeriodSelect(props: Props) {
|
||||||
|
const { onChange, id, label, isDisabled, isError, initialMs, helperText, units } = props;
|
||||||
|
let initialUnit: TimeUnit = bestUnit(initialMs);
|
||||||
|
let initialValue = milliToX(initialMs, initialUnit).toString();
|
||||||
|
const [value, setValue] = useState<string>(initialValue);
|
||||||
|
const [unit, setUnit] = useState<TimeUnit>(initialUnit);
|
||||||
|
const prevUnit = usePrevious(unit);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevUnit !== unit) {
|
||||||
|
onChange(xToMilli(value, unit));
|
||||||
|
}
|
||||||
|
}, [unit, prevUnit, onChange, value]);
|
||||||
|
|
||||||
|
const changeValue = (event: any) => {
|
||||||
|
let value = event.target.value;
|
||||||
|
setValue(value);
|
||||||
|
onChange(isNaN(value) ? 0 : xToMilli(value, unit));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectText = (event: any) => {
|
||||||
|
event.target.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
id={id}
|
||||||
|
label={label}
|
||||||
|
disabled={isDisabled === true}
|
||||||
|
error={isError === true}
|
||||||
|
value={value}
|
||||||
|
onChange={changeValue}
|
||||||
|
onFocus={selectText}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
helperText={helperText}
|
||||||
|
type="text"
|
||||||
|
variant="outlined"
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<TextField
|
||||||
|
id="unitSelect"
|
||||||
|
disabled={isDisabled === true}
|
||||||
|
error={isError === true}
|
||||||
|
select
|
||||||
|
value={unit}
|
||||||
|
onChange={event => setUnit(event.target.value as TimeUnit)}
|
||||||
|
margin="none"
|
||||||
|
variant="standard">
|
||||||
|
{units.includes("ms") && <MenuItem value="milliseconds">ms</MenuItem>}
|
||||||
|
{units.includes("seconds") && <MenuItem value="seconds">sec</MenuItem>}
|
||||||
|
{units.includes("minutes") && <MenuItem value="minutes">min</MenuItem>}
|
||||||
|
{units.includes("hours") && <MenuItem value="hours">hrs</MenuItem>}
|
||||||
|
{units.includes("days") && <MenuItem value="days">days</MenuItem>}
|
||||||
|
</TextField>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
202
src/common/time/TimeBar.tsx
Normal file
202
src/common/time/TimeBar.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
Grid,
|
||||||
|
TextField,
|
||||||
|
Typography
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
|
||||||
|
import moment, { Moment } from "moment";
|
||||||
|
import { DateRange as DateIcon } from "@material-ui/icons";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { DateRange, DateRangeDelimiter, StaticDateRangePicker } from "@material-ui/pickers";
|
||||||
|
import { DateRangePreset, GetDefaultDateRange, SetDefaultPreset } from "./DateRange";
|
||||||
|
import { useThemeType } from "hooks";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
startDate: Moment;
|
||||||
|
endDate: Moment;
|
||||||
|
updateDateRange: (start: Moment, end: Moment, live: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
activeButtonLight: {
|
||||||
|
border: "1px solid black"
|
||||||
|
},
|
||||||
|
acitveButtonDark: {
|
||||||
|
border: "1px solid white"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function TimeBar(props: Props) {
|
||||||
|
const { updateDateRange, startDate, endDate } = props;
|
||||||
|
const defaultDateRange = GetDefaultDateRange();
|
||||||
|
const [dateRangeDialog, setDateRangeDialog] = useState(false);
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange<Moment>>([
|
||||||
|
defaultDateRange.start,
|
||||||
|
defaultDateRange.end
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [activeView, setActiveView] = useState<DateRangePreset>();
|
||||||
|
const classes = useStyles();
|
||||||
|
const themeType = useThemeType();
|
||||||
|
|
||||||
|
const submitDateRange = () => {
|
||||||
|
const startDate = dateRange[0] ? dateRange[0].clone() : moment();
|
||||||
|
const endDate = dateRange[1] ? dateRange[1].clone() : moment();
|
||||||
|
updateDateRange(startDate, endDate, false);
|
||||||
|
setDateRangeDialog(false);
|
||||||
|
setActiveView("selectRange");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let hourDiff = moment(endDate)
|
||||||
|
.diff(moment(startDate), "hours", true)
|
||||||
|
.toFixed(4);
|
||||||
|
let dayDiff = moment(endDate)
|
||||||
|
.diff(moment(startDate), "days", true)
|
||||||
|
.toFixed(4);
|
||||||
|
let monthDiff = moment(endDate)
|
||||||
|
.diff(moment(startDate), "months", true)
|
||||||
|
.toFixed(4);
|
||||||
|
if (hourDiff === "1.0000") {
|
||||||
|
setActiveView("pastHour");
|
||||||
|
} else if (dayDiff === "1.0000") {
|
||||||
|
setActiveView("pastDay");
|
||||||
|
} else if (dayDiff === "7.0000") {
|
||||||
|
setActiveView("pastWeek");
|
||||||
|
} else if (monthDiff === "1.0000") {
|
||||||
|
setActiveView("pastMonth");
|
||||||
|
} else {
|
||||||
|
setActiveView("selectRange");
|
||||||
|
}
|
||||||
|
}, [startDate, endDate]);
|
||||||
|
|
||||||
|
const datePickerDialog = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveDialog
|
||||||
|
open={dateRangeDialog}
|
||||||
|
onClose={() => setDateRangeDialog(false)}
|
||||||
|
aria-labelledby="date-range-dialog">
|
||||||
|
<DialogContent>
|
||||||
|
<StaticDateRangePicker
|
||||||
|
disableFuture
|
||||||
|
value={dateRange}
|
||||||
|
onChange={date => setDateRange(date)}
|
||||||
|
renderInput={(startProps, endProps) => (
|
||||||
|
<React.Fragment>
|
||||||
|
<TextField {...startProps} />
|
||||||
|
<DateRangeDelimiter> to </DateRangeDelimiter>
|
||||||
|
<TextField {...endProps} />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setDateRangeDialog(false)} color="primary">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button onClick={submitDateRange} color="primary">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{datePickerDialog()}
|
||||||
|
<Grid container direction="row" spacing={1} wrap="nowrap">
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
style={{ borderRadius: "25px" }}
|
||||||
|
className={
|
||||||
|
activeView === "pastHour"
|
||||||
|
? themeType === "light"
|
||||||
|
? classes.activeButtonLight
|
||||||
|
: classes.acitveButtonDark
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
updateDateRange(moment().subtract(1, "hour"), moment(), false);
|
||||||
|
SetDefaultPreset("pastHour");
|
||||||
|
}}>
|
||||||
|
<Typography style={{ fontWeight: 650 }}>1H</Typography>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
style={{ borderRadius: "25px" }}
|
||||||
|
className={
|
||||||
|
activeView === "pastDay"
|
||||||
|
? themeType === "light"
|
||||||
|
? classes.activeButtonLight
|
||||||
|
: classes.acitveButtonDark
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
updateDateRange(moment().subtract(1, "day"), moment(), false);
|
||||||
|
SetDefaultPreset("pastDay");
|
||||||
|
}}>
|
||||||
|
<Typography style={{ fontWeight: 650 }}>1D</Typography>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
style={{ borderRadius: "25px" }}
|
||||||
|
className={
|
||||||
|
activeView === "pastWeek"
|
||||||
|
? themeType === "light"
|
||||||
|
? classes.activeButtonLight
|
||||||
|
: classes.acitveButtonDark
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
updateDateRange(moment().subtract(1, "week"), moment(), false);
|
||||||
|
SetDefaultPreset("pastWeek");
|
||||||
|
}}>
|
||||||
|
<Typography style={{ fontWeight: 650 }}>1W</Typography>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
style={{ borderRadius: "25px" }}
|
||||||
|
className={
|
||||||
|
activeView === "pastMonth"
|
||||||
|
? themeType === "light"
|
||||||
|
? classes.activeButtonLight
|
||||||
|
: classes.acitveButtonDark
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
updateDateRange(moment().subtract(1, "month"), moment(), false);
|
||||||
|
SetDefaultPreset("pastMonth");
|
||||||
|
}}>
|
||||||
|
<Typography style={{ fontWeight: 650 }}>1M</Typography>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
style={{ borderRadius: "25px" }}
|
||||||
|
className={
|
||||||
|
activeView === "selectRange"
|
||||||
|
? themeType === "light"
|
||||||
|
? classes.activeButtonLight
|
||||||
|
: classes.acitveButtonDark
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setDateRangeDialog(true);
|
||||||
|
}}>
|
||||||
|
<DateIcon />
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/common/time/duration.ts
Normal file
53
src/common/time/duration.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
export type TimeUnit = "ms" | "seconds" | "minutes" | "hours" | "days";
|
||||||
|
|
||||||
|
export function milliToX(ms: number | undefined, unit: TimeUnit): number {
|
||||||
|
if (!ms || isNaN(ms)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
switch (unit) {
|
||||||
|
case "days":
|
||||||
|
return parseFloat(ms.toString()) / 86400000;
|
||||||
|
case "hours":
|
||||||
|
return parseFloat(ms.toString()) / 3600000;
|
||||||
|
case "minutes":
|
||||||
|
return parseFloat(ms.toString()) / 60000;
|
||||||
|
case "seconds":
|
||||||
|
return parseFloat(ms.toString()) / 1000;
|
||||||
|
default:
|
||||||
|
return parseFloat(ms.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xToMilli(value: string, unit: TimeUnit): number {
|
||||||
|
if (isNaN(parseInt(value)) || value === "") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
switch (unit) {
|
||||||
|
case "days":
|
||||||
|
return parseFloat(value.toString()) * 86400000;
|
||||||
|
case "hours":
|
||||||
|
return parseFloat(value.toString()) * 3600000;
|
||||||
|
case "minutes":
|
||||||
|
return parseFloat(value.toString()) * 60000;
|
||||||
|
case "seconds":
|
||||||
|
return parseFloat(value.toString()) * 1000;
|
||||||
|
default:
|
||||||
|
return parseInt(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bestUnit(ms?: number): TimeUnit {
|
||||||
|
if (!ms || ms === 0) {
|
||||||
|
return "minutes";
|
||||||
|
}
|
||||||
|
|
||||||
|
return milliToX(ms, "days") >= 1
|
||||||
|
? "days"
|
||||||
|
: milliToX(ms, "hours") >= 1
|
||||||
|
? "hours"
|
||||||
|
: milliToX(ms, "minutes") >= 1
|
||||||
|
? "minutes"
|
||||||
|
: milliToX(ms, "seconds") >= 1
|
||||||
|
? "seconds"
|
||||||
|
: "ms";
|
||||||
|
}
|
||||||
27
src/common/time/locale.ts
Normal file
27
src/common/time/locale.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
type Language = "en";
|
||||||
|
|
||||||
|
export const LoadLocale = (language: Language) => {
|
||||||
|
switch (language) {
|
||||||
|
default:
|
||||||
|
moment.updateLocale("en", {
|
||||||
|
relativeTime: {
|
||||||
|
future: "in %s",
|
||||||
|
past: "%s ago",
|
||||||
|
s: "seconds",
|
||||||
|
ss: "%ss",
|
||||||
|
m: "a minute",
|
||||||
|
mm: "%dmin",
|
||||||
|
h: "an hour",
|
||||||
|
hh: "%dh",
|
||||||
|
d: "a day",
|
||||||
|
dd: "%dd",
|
||||||
|
M: "a month",
|
||||||
|
MM: "%dmo",
|
||||||
|
y: "a year",
|
||||||
|
yy: "%d years"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -61,6 +61,8 @@ import ShareObject from "user/ShareObject";
|
||||||
import { amber, blue, green } from "@mui/material/colors";
|
import { amber, blue, green } from "@mui/material/colors";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import DeviceSettings from "device/DeviceSettings";
|
||||||
|
import ObjectTeams from "teams/ObjectTeams";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
// const isMobile = useMobile()
|
// const isMobile = useMobile()
|
||||||
|
|
@ -371,16 +373,18 @@ export default function DeviceActions(props: Props) {
|
||||||
isJsonDataDialogOpen
|
isJsonDataDialogOpen
|
||||||
} = dialogState;
|
} = dialogState;
|
||||||
|
|
||||||
|
console.log(canWrite)
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{/* <DeviceSettings
|
<DeviceSettings
|
||||||
device={device}
|
device={device}
|
||||||
isDialogOpen={or(isDeviceSettingsDialogOpen, false)}
|
isDialogOpen={or(isDeviceSettingsDialogOpen, false)}
|
||||||
closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")}
|
closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
canEdit={canWrite}
|
canEdit={canWrite}
|
||||||
components={components}
|
// components={components}
|
||||||
/> */}
|
components={[]}
|
||||||
|
/>
|
||||||
{/* <ComponentSettings
|
{/* <ComponentSettings
|
||||||
mode="add"
|
mode="add"
|
||||||
device={device}
|
device={device}
|
||||||
|
|
@ -421,14 +425,14 @@ export default function DeviceActions(props: Props) {
|
||||||
closeDialogCallback={() => closeDialog("isObjectUsersDialogOpen")}
|
closeDialogCallback={() => closeDialog("isObjectUsersDialogOpen")}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
/>
|
/>
|
||||||
{/* <ObjectTeams
|
<ObjectTeams
|
||||||
scope={deviceScope(device.id().toString())}
|
scope={deviceScope(device.id().toString())}
|
||||||
label={deviceName}
|
label={deviceName}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
isDialogOpen={or(isObjectTeamsDialogOpen, false)}
|
isDialogOpen={or(isObjectTeamsDialogOpen, false)}
|
||||||
closeDialogCallback={() => closeDialog("isObjectTeamsDialogOpen")}
|
closeDialogCallback={() => closeDialog("isObjectTeamsDialogOpen")}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
/> */}
|
/>
|
||||||
<RemoveSelfFromObject
|
<RemoveSelfFromObject
|
||||||
scope={deviceScope(device.id().toString())}
|
scope={deviceScope(device.id().toString())}
|
||||||
label={deviceName}
|
label={deviceName}
|
||||||
680
src/device/DeviceSettings.tsx
Normal file
680
src/device/DeviceSettings.tsx
Normal file
|
|
@ -0,0 +1,680 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
Grid2 as Grid,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
MenuItem,
|
||||||
|
Switch,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
TextField,
|
||||||
|
Theme,
|
||||||
|
Typography
|
||||||
|
} from "@mui/material";
|
||||||
|
// import { Theme } from "@material-ui/core/styles/createMuiTheme";
|
||||||
|
import DeleteButton from "common/DeleteButton";
|
||||||
|
import PeriodSelect from "common/time/PeriodSelect";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { useDeviceAPI, usePrevious, useSnackbar } from "hooks";
|
||||||
|
import { Component, Device } from "models";
|
||||||
|
import { IsExtended, ListDeviceProductDescribers } from "products/DeviceProduct";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
import { useGlobalState } from "providers";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
// import { useHistory } from "react-router";
|
||||||
|
import { quack } from "protobuf-ts/quack";
|
||||||
|
import LinearMutationBuilder from "common/LinearMutationBuilder";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
index: any;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanelMine(props: TabPanelProps) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
aria-labelledby={`simple-tab-${index}`}
|
||||||
|
{...other}>
|
||||||
|
{value === index && <React.Fragment>{children}</React.Fragment>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return ({
|
||||||
|
dialogContent: {
|
||||||
|
minHeight: "25vh"
|
||||||
|
},
|
||||||
|
removeDeviceDialog: {
|
||||||
|
zIndex: theme.zIndex.modal + 1
|
||||||
|
},
|
||||||
|
tabSmall: {
|
||||||
|
width: "100%",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
flexShrink: 1,
|
||||||
|
minWidth: "48px",
|
||||||
|
marginTop: 0,
|
||||||
|
paddingTop: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
device: Device;
|
||||||
|
isDialogOpen: boolean;
|
||||||
|
closeDialogCallback: Function;
|
||||||
|
canEdit: boolean;
|
||||||
|
refreshCallback: Function;
|
||||||
|
components?: Component[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceFromForm = (device: Device): Device => {
|
||||||
|
if (device.settings.upgradeChannel === pond.UpgradeChannel.UPGRADE_CHANNEL_INVALID) {
|
||||||
|
device.settings.upgradeChannel = pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE;
|
||||||
|
}
|
||||||
|
return device;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DeviceSettings(props: Props) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const [{ user }] = useGlobalState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { success, error } = useSnackbar();
|
||||||
|
const deviceAPI = useDeviceAPI();
|
||||||
|
const { device, isDialogOpen, closeDialogCallback, canEdit, refreshCallback, components } = props;
|
||||||
|
const prevDevice = usePrevious(device);
|
||||||
|
const [deviceForm, setDeviceForm] = useState<Device>(deviceFromForm(Device.clone(device)));
|
||||||
|
const [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState<boolean>(false);
|
||||||
|
const [sleeps, setSleeps] = useState<boolean>(device.settings.sleepDurationS > 0);
|
||||||
|
const [compExtOne, setCompExtOne] = useState("");
|
||||||
|
const [compExtTwo, setCompExtTwo] = useState("");
|
||||||
|
const [compExtThree, setCompExtThree] = useState("");
|
||||||
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
|
const [linearMutations, setLinearMutations] = useState<pond.LinearMutation[]>([]);
|
||||||
|
const [componentsByDevice, setComponentsByDevice] = useState<Map<string, Component[]>>(
|
||||||
|
new Map<string, Component[]>()
|
||||||
|
);
|
||||||
|
const [newMutationDialog, setNewMutationDialog] = useState(false);
|
||||||
|
const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevDevice !== device) {
|
||||||
|
setDeviceForm(deviceFromForm(Device.clone(props.device)));
|
||||||
|
if (device.settings.extensionComponents[0]) {
|
||||||
|
setCompExtOne(device.settings.extensionComponents[0]);
|
||||||
|
}
|
||||||
|
if (device.settings.extensionComponents[1]) {
|
||||||
|
setCompExtTwo(device.settings.extensionComponents[1]);
|
||||||
|
}
|
||||||
|
if (device.settings.extensionComponents[2]) {
|
||||||
|
setCompExtThree(device.settings.extensionComponents[2]);
|
||||||
|
}
|
||||||
|
if (device.settings.mutations) {
|
||||||
|
setLinearMutations(device.settings.mutations);
|
||||||
|
}
|
||||||
|
if (components) {
|
||||||
|
let cbd = new Map<string, Component[]>();
|
||||||
|
cbd.set(device.id().toString(), components);
|
||||||
|
setComponentsByDevice(cbd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [device, prevDevice, props.device, components]);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
closeDialogCallback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSleepDurationValid = (device: Device): boolean => {
|
||||||
|
return (
|
||||||
|
!sleeps ||
|
||||||
|
(sleeps && device.settings.sleepDurationS >= 10 && device.settings.sleepDelayMs >= 1000)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const minCheckPeriodS = (): number => {
|
||||||
|
let defaultPeriod = 60;
|
||||||
|
switch (device.settings.platform) {
|
||||||
|
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
|
||||||
|
return user.hasFeature("admin") ? defaultPeriod : 300;
|
||||||
|
default:
|
||||||
|
return defaultPeriod;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const minCheckPeriodDescription = (): string => {
|
||||||
|
let min = minCheckPeriodS() / 60;
|
||||||
|
return min.toString() + " minute" + (min !== 1 ? "s" : "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCheckPeriodValid = (device: Device): boolean => {
|
||||||
|
return (
|
||||||
|
device.settings.pondCheckPeriodS >= minCheckPeriodS() &&
|
||||||
|
device.settings.pondCheckPeriodS <= 3600
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = (): boolean => {
|
||||||
|
return isSleepDurationValid(deviceForm) && isCheckPeriodValid(deviceForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeName = (event: any) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.name = String(event.target.value);
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeDescription = (event: any) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.description = String(event.target.value);
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeSleepDuration = (ms: number) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.sleepDurationS = ms / 1000;
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeSleepDelay = (ms: number) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.sleepDelayMs = ms;
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePondCheckPeriod = (ms: number) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.pondCheckPeriodS = ms / 1000;
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeUpgradeChannel = (event: any) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.upgradeChannel = event.target.value;
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeProduct = (event: any) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.product = event.target.value;
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const upgradeChannelHelper = (): string => {
|
||||||
|
switch (deviceForm.settings.upgradeChannel) {
|
||||||
|
case pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE: {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
case pond.UpgradeChannel.UPGRADE_CHANNEL_BETA: {
|
||||||
|
return "The newest field tested features as they become available";
|
||||||
|
}
|
||||||
|
case pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA: {
|
||||||
|
return "The newest lab tested features as they become available";
|
||||||
|
}
|
||||||
|
case pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT: {
|
||||||
|
return "Features as they're written, expect things to break";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAutomaticallyUpgrade = (event: any) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
updatedForm.settings.automaticallyUpgrade = event.target.checked;
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSleeps = (event: any) => {
|
||||||
|
let updatedForm = Device.clone(deviceForm);
|
||||||
|
let updatedSleeps = event.target.checked;
|
||||||
|
updatedSleeps
|
||||||
|
? (updatedForm.settings.sleepType = quack.SleepType.SLEEP_TYPE_NAP)
|
||||||
|
: (updatedForm.settings.sleepType = quack.SleepType.SLEEP_TYPE_NONE);
|
||||||
|
setSleeps(updatedSleeps);
|
||||||
|
setDeviceForm(updatedForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitSettings = () => {
|
||||||
|
const deviceName = deviceForm.name();
|
||||||
|
const deviceID = device.id();
|
||||||
|
if (!sleeps) {
|
||||||
|
deviceForm.settings.sleepDurationS = 0;
|
||||||
|
}
|
||||||
|
deviceForm.settings.extensionComponents = [compExtOne, compExtTwo, compExtThree];
|
||||||
|
deviceForm.settings.mutations = linearMutations;
|
||||||
|
deviceAPI
|
||||||
|
.update(deviceID, deviceForm.settings)
|
||||||
|
.then((_response: any) => {
|
||||||
|
success(deviceName + " was successfully updated!");
|
||||||
|
close();
|
||||||
|
refreshCallback();
|
||||||
|
})
|
||||||
|
.catch((_err: any) => {
|
||||||
|
error("Error occured while configuring " + deviceName);
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmRemoveDevice = () => {
|
||||||
|
setIsRemoveDeviceDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeConfirmRemoveDeviceDialog = () => {
|
||||||
|
setIsRemoveDeviceDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDevice = () => {
|
||||||
|
const deviceName = deviceForm.name();
|
||||||
|
deviceAPI
|
||||||
|
.remove(device.id())
|
||||||
|
.then((_response: any) => {
|
||||||
|
success(deviceName + " was successfully deleted!");
|
||||||
|
navigate("/");
|
||||||
|
})
|
||||||
|
.catch((_err: any) => {
|
||||||
|
error("Error occured while deleting " + deviceName + ".");
|
||||||
|
closeConfirmRemoveDeviceDialog();
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const canRemove = (): boolean => {
|
||||||
|
return user.allowedTo("remove-devices");
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
Device Settings
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{deviceForm.name()} - ID: {deviceForm.id()}
|
||||||
|
</Typography>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<TextField
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
defaultValue={deviceForm.settings.name}
|
||||||
|
onChange={changeName}
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
defaultValue={deviceForm.settings.description}
|
||||||
|
onChange={changeDescription}
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
// rowsMax={4}
|
||||||
|
type="text"
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
<Grid container direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
|
||||||
|
<Grid size={{ xs: 12, sm: 12 }}>
|
||||||
|
<PeriodSelect
|
||||||
|
id="pondCheckPeriod"
|
||||||
|
label="Check-in Period"
|
||||||
|
isError={!isCheckPeriodValid(deviceForm)}
|
||||||
|
helperText={
|
||||||
|
isCheckPeriodValid(deviceForm)
|
||||||
|
? "How often the device checks for updates"
|
||||||
|
: "Value must be between " + minCheckPeriodDescription() + " and 1 hour"
|
||||||
|
}
|
||||||
|
initialMs={deviceForm.settings.pondCheckPeriodS * 1000}
|
||||||
|
isDisabled={!canEdit}
|
||||||
|
onChange={changePondCheckPeriod}
|
||||||
|
units={["seconds", "minutes", "hours"]}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
{user.hasFeature("sleep") && (
|
||||||
|
<Grid container direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
|
||||||
|
<Grid container size={{ xs: 4, sm: 3 }} justifyContent="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={sleeps}
|
||||||
|
onChange={toggleSleeps}
|
||||||
|
name="sleeps"
|
||||||
|
aria-label="sleeps"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography variant="caption">Sleeps</Typography>}
|
||||||
|
labelPlacement="top"
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 8, sm: 9 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<PeriodSelect
|
||||||
|
id="sleepDuration"
|
||||||
|
label="Sleep Duration"
|
||||||
|
isError={!isSleepDurationValid(deviceForm)}
|
||||||
|
helperText={
|
||||||
|
!isSleepDurationValid(deviceForm) ? "Must be at least 10 seconds" : undefined
|
||||||
|
}
|
||||||
|
isDisabled={!canEdit || !sleeps}
|
||||||
|
initialMs={deviceForm.settings.sleepDurationS * 1000}
|
||||||
|
onChange={changeSleepDuration}
|
||||||
|
units={["seconds", "minutes", "hours"]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<PeriodSelect
|
||||||
|
id="awakeDuration"
|
||||||
|
label="Awake Duration"
|
||||||
|
isError={!isSleepDurationValid(deviceForm)}
|
||||||
|
helperText={
|
||||||
|
!isSleepDurationValid(deviceForm) ? "Must be at least 1 second" : undefined
|
||||||
|
}
|
||||||
|
isDisabled={!canEdit || !sleeps}
|
||||||
|
initialMs={deviceForm.settings.sleepDelayMs}
|
||||||
|
onChange={changeSleepDelay}
|
||||||
|
units={["seconds", "minutes", "hours"]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Grid size={{ xs: 12, sm: 12 }}>
|
||||||
|
<TextField
|
||||||
|
id="upgradeChannel"
|
||||||
|
label="Upgrade Channel"
|
||||||
|
select
|
||||||
|
value={deviceForm.settings.upgradeChannel}
|
||||||
|
onChange={changeUpgradeChannel}
|
||||||
|
helperText={upgradeChannelHelper()}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canEdit}>
|
||||||
|
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE}>Stable</MenuItem>
|
||||||
|
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_BETA}>Beta</MenuItem>
|
||||||
|
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA}>Alpha</MenuItem>
|
||||||
|
{user.hasFeature("dev-channel") && (
|
||||||
|
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT}>
|
||||||
|
Development
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{user.hasFeature("dev-channel") && (
|
||||||
|
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_RECOVERY}>Recovery</MenuItem>
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
{user.hasFeature("admin") && (
|
||||||
|
<TextField
|
||||||
|
id="deviceProduct"
|
||||||
|
label="Device Product"
|
||||||
|
select
|
||||||
|
value={deviceForm.settings.product}
|
||||||
|
onChange={event => changeProduct(event)}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canEdit}>
|
||||||
|
{ListDeviceProductDescribers().map(describer => (
|
||||||
|
<MenuItem key={describer.product} value={describer.product}>
|
||||||
|
{describer.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
{IsExtended(deviceForm.settings.product) && components && (
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
Select components to display on card
|
||||||
|
<TextField
|
||||||
|
id="component1"
|
||||||
|
label="Component 1"
|
||||||
|
select
|
||||||
|
value={compExtOne}
|
||||||
|
onChange={event => setCompExtOne(event.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canEdit}>
|
||||||
|
{components.map(comp => (
|
||||||
|
<MenuItem key={comp.key()} value={comp.locationString()}>
|
||||||
|
{comp.name()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
id="component1"
|
||||||
|
label="Component 2"
|
||||||
|
select
|
||||||
|
value={compExtTwo}
|
||||||
|
onChange={event => setCompExtTwo(event.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canEdit}>
|
||||||
|
{components.map(comp => (
|
||||||
|
<MenuItem key={comp.key()} value={comp.locationString()}>
|
||||||
|
{comp.name()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
id="component1"
|
||||||
|
label="Component 3"
|
||||||
|
select
|
||||||
|
value={compExtThree}
|
||||||
|
onChange={event => setCompExtThree(event.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canEdit}>
|
||||||
|
{components.map(comp => (
|
||||||
|
<MenuItem key={comp.key()} value={comp.locationString()}>
|
||||||
|
{comp.name()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
{user.hasAdmin() && (
|
||||||
|
<Grid size={{ xs: 12, sm: 12 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={deviceForm.settings.automaticallyUpgrade}
|
||||||
|
onChange={toggleAutomaticallyUpgrade}
|
||||||
|
name="automaticallyUpgrade"
|
||||||
|
aria-label="automaticallyUpgrade"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
disabled={!canEdit}
|
||||||
|
label="Automatically Upgrade"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutationsContent = () => {
|
||||||
|
let mutations: JSX.Element[] = [];
|
||||||
|
if (device.settings.mutations) {
|
||||||
|
device.settings.mutations.forEach((mut, i) => {
|
||||||
|
mutations.push(
|
||||||
|
<ListItem key={"mutation" + i}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
alignContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between">
|
||||||
|
<Grid size={{ xs: 5 }}>
|
||||||
|
<Typography>{mut.mutationName}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
style={{ backgroundColor: "red" }}
|
||||||
|
onClick={() => {
|
||||||
|
let lm = linearMutations;
|
||||||
|
lm.splice(i, 1);
|
||||||
|
setLinearMutations([...lm]);
|
||||||
|
}}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
style={{ marginLeft: 10 }}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setExistingMutation(mut);
|
||||||
|
setNewMutationDialog(true);
|
||||||
|
}}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mutationList: JSX.Element = <List>{mutations}</List>;
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setExistingMutation(undefined);
|
||||||
|
setNewMutationDialog(true);
|
||||||
|
}}>
|
||||||
|
Add New Mutation
|
||||||
|
</Button>
|
||||||
|
<List>{mutationList}</List>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = () => {
|
||||||
|
return (
|
||||||
|
<Grid container justifyContent="space-between" direction="row">
|
||||||
|
<Grid size={{ xs: 5 }}>
|
||||||
|
{canRemove() && <DeleteButton onClick={confirmRemoveDevice}>Delete</DeleteButton>}
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
||||||
|
<Button onClick={close} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={submitSettings} color="primary" disabled={!isFormValid() || !canEdit}>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<ResponsiveDialog
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
open={isDialogOpen}
|
||||||
|
onClose={close}
|
||||||
|
aria-labelledby="device-config-title">
|
||||||
|
<DialogTitle id="device-config-title">{title()}</DialogTitle>
|
||||||
|
<DialogContent className={classes.dialogContent}>
|
||||||
|
{components ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tabs
|
||||||
|
value={currentTab}
|
||||||
|
onChange={(_, value) => setCurrentTab(value)}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
variant="fullWidth"
|
||||||
|
aria-label="device tabs"
|
||||||
|
classes={{ root: classes.tabSmall }}>
|
||||||
|
<Tab label={"General"} className={classes.tabSmall} />
|
||||||
|
{user.hasFeature("beta") && (
|
||||||
|
<Tab label={"Mutations"} className={classes.tabSmall} />
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
<TabPanelMine value={currentTab} index={0}>
|
||||||
|
{content()}
|
||||||
|
</TabPanelMine>
|
||||||
|
<TabPanelMine value={currentTab} index={1}>
|
||||||
|
{mutationsContent()}
|
||||||
|
</TabPanelMine>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
content()
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>{actions()}</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
<Dialog
|
||||||
|
open={isRemoveDeviceDialogOpen}
|
||||||
|
onClose={closeConfirmRemoveDeviceDialog}
|
||||||
|
aria-labelledby="confirm-remove-device-label"
|
||||||
|
aria-describedby="confirm-remove-device-description"
|
||||||
|
className={classes.removeDeviceDialog}>
|
||||||
|
<DialogTitle id="confirm-remove-device-title">Delete {deviceForm.name()}?</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="subtitle1" color="error" align="left">
|
||||||
|
WARNING:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="subtitle1" color="textSecondary" align="left">
|
||||||
|
Clicking 'Accept' will remove the device from all users and groups associated with it.
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={closeConfirmRemoveDeviceDialog} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={removeDevice} color="primary">
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<LinearMutationBuilder
|
||||||
|
existingMutation={existingMutation}
|
||||||
|
componentsByDevice={componentsByDevice}
|
||||||
|
open={newMutationDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setNewMutationDialog(false);
|
||||||
|
}}
|
||||||
|
onSubmit={newMutation => {
|
||||||
|
let lm = linearMutations;
|
||||||
|
lm.push(newMutation);
|
||||||
|
setLinearMutations(lm);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
CircularProgress,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
|
@ -18,10 +19,11 @@ import { Tag as TagUI } from "common/Tag";
|
||||||
import TagSettings from "common/TagSettings";
|
import TagSettings from "common/TagSettings";
|
||||||
import { Device, Tag } from "models";
|
import { Device, Tag } from "models";
|
||||||
import { filterByTag } from "pbHelpers/Tag";
|
import { filterByTag } from "pbHelpers/Tag";
|
||||||
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
|
import { useDeviceAPI, useSnackbar, useTagAPI } from "providers";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((_theme: Theme) => {
|
||||||
return ({
|
return ({
|
||||||
addIcon: {
|
addIcon: {
|
||||||
color: "var(--status-ok)"
|
color: "var(--status-ok)"
|
||||||
|
|
@ -37,13 +39,27 @@ interface AddDeviceTagProps {
|
||||||
|
|
||||||
function AddDeviceTag(props: AddDeviceTagProps) {
|
function AddDeviceTag(props: AddDeviceTagProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const tagAPI = useTagAPI();
|
||||||
const { device, deviceTags, addTagToDevice } = props;
|
const { device, deviceTags, addTagToDevice } = props;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [createNewOpen, setCreateNewOpen] = useState(false);
|
const [createNewOpen, setCreateNewOpen] = useState(false);
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
// const [{ tags }] = useGlobalState();
|
|
||||||
const [tags, setTags] = useState<Tag[]>([])
|
const [tags, setTags] = useState<Tag[]>([])
|
||||||
|
|
||||||
|
const loadTags = () => {
|
||||||
|
tagAPI.listTags().then(resp => {
|
||||||
|
let newTags: Tag[] = [];
|
||||||
|
resp.data.tags.forEach((tag: pond.Tag) => {
|
||||||
|
newTags.push(Tag.create(tag))
|
||||||
|
})
|
||||||
|
setTags(newTags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTags()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const tagItems = tags
|
const tagItems = tags
|
||||||
.filter(tag => !deviceTags.some(key => key === tag.settings.key))
|
.filter(tag => !deviceTags.some(key => key === tag.settings.key))
|
||||||
.filter(tag => filterByTag(searchValue, tag))
|
.filter(tag => filterByTag(searchValue, tag))
|
||||||
|
|
@ -117,12 +133,35 @@ interface DeviceTagsProps {
|
||||||
export default function DeviceTags(props: DeviceTagsProps) {
|
export default function DeviceTags(props: DeviceTagsProps) {
|
||||||
const { device, disableAdd } = props;
|
const { device, disableAdd } = props;
|
||||||
// const [{ tags }] = useGlobalState();
|
// const [{ tags }] = useGlobalState();
|
||||||
|
const tagAPI = useTagAPI()
|
||||||
const [tags, setTags] = useState<Tag[]>([])
|
const [tags, setTags] = useState<Tag[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
const deviceAPI = useDeviceAPI();
|
const deviceAPI = useDeviceAPI();
|
||||||
const { error } = useSnackbar();
|
const { error } = useSnackbar();
|
||||||
const [deviceTags, setDeviceTags] = useState<string[]>(device.status.tagKeys);
|
const [deviceTags, setDeviceTags] = useState<string[]>(device.status.tagKeys);
|
||||||
const previousDeviceRef = useRef(device);
|
const previousDeviceRef = useRef(device);
|
||||||
|
|
||||||
|
const loadTags = () => {
|
||||||
|
setLoading(true)
|
||||||
|
tagAPI.listTags().then(resp => {
|
||||||
|
let newTags: Tag[] = [];
|
||||||
|
resp.data.tags.forEach((tag: pond.Tag) => {
|
||||||
|
newTags.push(Tag.create(tag))
|
||||||
|
})
|
||||||
|
setTags(newTags)
|
||||||
|
}).finally(() => {
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(tags)
|
||||||
|
}, [tags])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTags()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousDeviceRef.current !== device) {
|
if (previousDeviceRef.current !== device) {
|
||||||
setDeviceTags(device.status.tagKeys);
|
setDeviceTags(device.status.tagKeys);
|
||||||
|
|
@ -156,6 +195,12 @@ export default function DeviceTags(props: DeviceTagsProps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<CircularProgress />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{deviceTags.map(key => {
|
{deviceTags.map(key => {
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,11 @@ import PageContainer from "./PageContainer";
|
||||||
import { useMobile } from "hooks";
|
import { useMobile } from "hooks";
|
||||||
import LoadingScreen from "app/LoadingScreen";
|
import LoadingScreen from "app/LoadingScreen";
|
||||||
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
||||||
import DeviceActions from "common/DeviceActions";
|
import DeviceActions from "device/DeviceActions";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import DeviceOverview from "device/DeviceOverview";
|
import DeviceOverview from "device/DeviceOverview";
|
||||||
|
|
||||||
// const useStyles = makeStyles((theme: Theme) => {
|
|
||||||
// // const isMobile = useMobile()
|
|
||||||
// return ({
|
|
||||||
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
export default function DevicePage() {
|
export default function DevicePage() {
|
||||||
const deviceAPI = useDeviceAPI()
|
const deviceAPI = useDeviceAPI()
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
|
|
@ -32,8 +25,8 @@ export default function DevicePage() {
|
||||||
const [permissions, setPermissions] = useState<pond.Permission[]>([])
|
const [permissions, setPermissions] = useState<pond.Permission[]>([])
|
||||||
const [preferences, setPreferences] = useState<pond.DevicePreferences>(pond.DevicePreferences.create())
|
const [preferences, setPreferences] = useState<pond.DevicePreferences>(pond.DevicePreferences.create())
|
||||||
|
|
||||||
const [cellularUsage, setCellularUsage] = useState<number>(0);
|
const [cellularUsage, _setCellularUsage] = useState<number>(0);
|
||||||
const [cellularStatus, setCellularStatus] = useState<string>("");
|
const [cellularStatus, _setCellularStatus] = useState<string>("");
|
||||||
|
|
||||||
const loadDevice = () => {
|
const loadDevice = () => {
|
||||||
if (loading) return
|
if (loading) return
|
||||||
|
|
@ -50,7 +43,16 @@ export default function DevicePage() {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => {
|
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => {
|
||||||
setDevice(Device.any(resp.data.device))
|
setDevice(Device.any(resp.data.device))
|
||||||
setPermissions(resp.data.permissions)
|
let newPermissions: pond.Permission[] = []
|
||||||
|
resp.data.permissions.forEach(perm => {
|
||||||
|
if (typeof(perm) === "string" ) {
|
||||||
|
const permNumber = pond.Permission[perm as keyof typeof pond.Permission]
|
||||||
|
newPermissions.push(permNumber)
|
||||||
|
} else {
|
||||||
|
newPermissions.push(perm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setPermissions(newPermissions)
|
||||||
let u = User.any(resp.data.user);
|
let u = User.any(resp.data.user);
|
||||||
setPreferences(u.preferences)
|
setPreferences(u.preferences)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
|
|
@ -77,6 +79,10 @@ export default function DevicePage() {
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(permissions)
|
||||||
|
}, [permissions])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDevice()
|
loadDevice()
|
||||||
}, [deviceID])
|
}, [deviceID])
|
||||||
|
|
@ -110,9 +116,6 @@ export default function DevicePage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer padding={isMobile ? 0 : 2}>
|
<PageContainer padding={isMobile ? 0 : 2}>
|
||||||
{/* <Typography>
|
|
||||||
{device.name()}
|
|
||||||
</Typography> */}
|
|
||||||
<Grid2 container justifyContent={"space-between"}>
|
<Grid2 container justifyContent={"space-between"}>
|
||||||
<Grid2>
|
<Grid2>
|
||||||
<SmartBreadcrumb />
|
<SmartBreadcrumb />
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,8 @@ export default function Devices() {
|
||||||
let keys = getContextKeys()
|
let keys = getContextKeys()
|
||||||
keys.splice(keys.length - 1, 0, tab);
|
keys.splice(keys.length - 1, 0, tab);
|
||||||
return keys
|
return keys
|
||||||
|
} else {
|
||||||
|
return getContextKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,6 +135,8 @@ export default function Devices() {
|
||||||
let types = getContextTypes()
|
let types = getContextTypes()
|
||||||
types.splice(types.length - 1, 0, "group");
|
types.splice(types.length - 1, 0, "group");
|
||||||
return types
|
return types
|
||||||
|
} else {
|
||||||
|
return getContextTypes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,6 +164,7 @@ export default function Devices() {
|
||||||
|
|
||||||
const loadDevices = () => {
|
const loadDevices = () => {
|
||||||
setDevicesLoading(true)
|
setDevicesLoading(true)
|
||||||
|
console.log(getKeys())
|
||||||
deviceAPI.list(
|
deviceAPI.list(
|
||||||
limit,
|
limit,
|
||||||
page*limit,
|
page*limit,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { cyan } from "@mui/material/colors";
|
||||||
import AcceptedIcon from "@mui/icons-material/CloudDone";
|
import AcceptedIcon from "@mui/icons-material/CloudDone";
|
||||||
import PendingIcon from "@mui/icons-material/CloudQueueTwoTone";
|
import PendingIcon from "@mui/icons-material/CloudQueueTwoTone";
|
||||||
import RejectedIcon from "@mui/icons-material/SmsFailed";
|
import RejectedIcon from "@mui/icons-material/SmsFailed";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface StatusHelper {
|
export interface StatusHelper {
|
||||||
description: string;
|
description: string;
|
||||||
|
|
|
||||||
617
src/teams/ObjectTeams.tsx
Normal file
617
src/teams/ObjectTeams.tsx
Normal file
|
|
@ -0,0 +1,617 @@
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CircularProgress,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormGroup,
|
||||||
|
FormLabel,
|
||||||
|
Grid2 as Grid,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
PaletteColor,
|
||||||
|
Theme,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from "@mui/material";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { usePermissionAPI, usePrevious, useSnackbar } from "hooks";
|
||||||
|
import { cloneDeep, isEqual } from "lodash";
|
||||||
|
import { Scope, Team, User } from "models";
|
||||||
|
import { 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 { useGlobalState, useTeamAPI } from "providers";
|
||||||
|
import ShareWithTeam from "./ShareWithTeam";
|
||||||
|
import { blue, red } from "@mui/material/colors";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { Share, RemoveCircle as RemoveUserIcon } from "@mui/icons-material";
|
||||||
|
|
||||||
|
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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
scope: Scope;
|
||||||
|
label: string;
|
||||||
|
permissions: pond.Permission[];
|
||||||
|
isDialogOpen: boolean;
|
||||||
|
closeDialogCallback: Function;
|
||||||
|
refreshCallback: Function;
|
||||||
|
userCallback?: (users?: User[] | Team[] | undefined) => void;
|
||||||
|
dialog?: string;
|
||||||
|
cardMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ObjectTeams(props: Props) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
const permissionAPI = usePermissionAPI();
|
||||||
|
const [{ user, as }] = useGlobalState();
|
||||||
|
const canProvision = user.allowedTo("provision");
|
||||||
|
const teamAPI = useTeamAPI();
|
||||||
|
const { error, success, warning } = useSnackbar();
|
||||||
|
const {
|
||||||
|
scope,
|
||||||
|
label,
|
||||||
|
permissions,
|
||||||
|
isDialogOpen,
|
||||||
|
closeDialogCallback,
|
||||||
|
refreshCallback,
|
||||||
|
userCallback,
|
||||||
|
dialog,
|
||||||
|
cardMode
|
||||||
|
} = props;
|
||||||
|
const prevPermissions = usePrevious(permissions);
|
||||||
|
const prevIsDialogOpen = usePrevious(isDialogOpen);
|
||||||
|
const [initialUsers, setInitialUsers] = useState<Team[]>([]);
|
||||||
|
const [users, setUsers] = useState<Team[]>([]);
|
||||||
|
const [canManageUsers, setCanManageUsers] = useState<boolean>(
|
||||||
|
permissions.includes(pond.Permission.PERMISSION_USERS)
|
||||||
|
);
|
||||||
|
const [canShare, setCanShare] = useState<boolean>(
|
||||||
|
permissions.includes(pond.Permission.PERMISSION_SHARE)
|
||||||
|
);
|
||||||
|
const [removedUsers, setRemovedUsers] = useState<string[]>([]);
|
||||||
|
const [removeSelfDialogIsOpen, setRemoveSelfDialogIsOpen] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [isShareObjectDialogOpen, setIsShareObjectDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const setDefaultState = () => {
|
||||||
|
setInitialUsers([]);
|
||||||
|
setUsers([]);
|
||||||
|
setRemovedUsers([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = useCallback(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
teamAPI
|
||||||
|
.listObjectTeams(scope, as)
|
||||||
|
.then((response: any) => {
|
||||||
|
let rTeams: Team[] = [];
|
||||||
|
or(response.data, { teams: [] }).teams.forEach((user: any) => {
|
||||||
|
rTeams.push(Team.any(user));
|
||||||
|
});
|
||||||
|
rTeams = rTeams.filter(u => u.permissions.length > 0);
|
||||||
|
setInitialUsers(cloneDeep(rTeams));
|
||||||
|
setUsers(rTeams);
|
||||||
|
})
|
||||||
|
.catch((_err: any) => {
|
||||||
|
setInitialUsers([]);
|
||||||
|
setUsers([]);
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
|
}, [scope, teamAPI, as]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!prevIsDialogOpen && isDialogOpen) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
if (prevPermissions !== permissions) {
|
||||||
|
setCanManageUsers(permissions.includes(pond.Permission.PERMISSION_USERS));
|
||||||
|
setCanShare(permissions.includes(pond.Permission.PERMISSION_SHARE));
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, load, permissions, prevIsDialogOpen, prevPermissions]);
|
||||||
|
|
||||||
|
const closeRemoveSelfDialog = () => {
|
||||||
|
setRemoveSelfDialogIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
closeRemoveSelfDialog();
|
||||||
|
closeDialogCallback();
|
||||||
|
setDefaultState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
permissionAPI
|
||||||
|
.updatePermissions(scope, users)
|
||||||
|
.then((_response: any) => {
|
||||||
|
success("Users were sucessfully updated for " + label);
|
||||||
|
close();
|
||||||
|
refreshCallback();
|
||||||
|
userCallback && userCallback(users);
|
||||||
|
if (cardMode) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
err.response.data.error
|
||||||
|
? warning(err.response.data.error)
|
||||||
|
: error("Error occured when updating users for " + label);
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeUserPermissions = (user: pond.ITeam) => (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],
|
||||||
|
["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.key === user.settings.key
|
||||||
|
) {
|
||||||
|
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;
|
||||||
|
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.key === user) {
|
||||||
|
updatedRemovedUsers.push(currUser.settings.key);
|
||||||
|
updatedUsers[i].permissions = [];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUsers(updatedUsers);
|
||||||
|
setRemovedUsers(updatedRemovedUsers);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIsSelf = (checkUser: pond.ITeam): boolean => {
|
||||||
|
const id = checkUser && checkUser.settings ? checkUser.settings.key : "";
|
||||||
|
return user.id() === id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const userIsRemoved = (user: pond.ITeam): boolean => {
|
||||||
|
const id = user && user.settings ? user.settings.key : "";
|
||||||
|
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" justifyContent="space-between">
|
||||||
|
<Grid container direction="row" justifyContent="space-between">
|
||||||
|
<Grid size={{ xs: 8 }}>
|
||||||
|
Teams
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 4 }} container justifyContent="flex-end">
|
||||||
|
{canShare && (
|
||||||
|
<Tooltip title={"Share " + label}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Share"
|
||||||
|
className={classes.iconButton}
|
||||||
|
onClick={openShareObjectDialog}>
|
||||||
|
<Share className={classes.shareIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{dialog && (
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ marginTop: "4px" }}>
|
||||||
|
{dialog}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const userPermissionManagement = (user: Team) => {
|
||||||
|
let permissions = user.permissions;
|
||||||
|
if (
|
||||||
|
!canManageUsers ||
|
||||||
|
userIsRemoved(user) ||
|
||||||
|
//check they have all permissions
|
||||||
|
(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 null;
|
||||||
|
}
|
||||||
|
const canRead = permissions.includes(pond.Permission.PERMISSION_READ);
|
||||||
|
return (
|
||||||
|
<FormControl component="fieldset" fullWidth className={classes.userPermissions}>
|
||||||
|
<FormLabel component="legend"></FormLabel>
|
||||||
|
<FormGroup>
|
||||||
|
<Grid container direction="row" justifyContent="flex-start">
|
||||||
|
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={permissions.includes(pond.Permission.PERMISSION_READ)}
|
||||||
|
onChange={changeUserPermissions(user)}
|
||||||
|
value={pond.Permission.PERMISSION_READ as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="View"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!canRead}
|
||||||
|
checked={permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||||
|
onChange={changeUserPermissions(user)}
|
||||||
|
value={pond.Permission.PERMISSION_WRITE as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Edit"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!canRead}
|
||||||
|
checked={permissions.includes(pond.Permission.PERMISSION_SHARE)}
|
||||||
|
onChange={changeUserPermissions(user)}
|
||||||
|
value={pond.Permission.PERMISSION_SHARE as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Share"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!canRead}
|
||||||
|
checked={permissions.includes(pond.Permission.PERMISSION_USERS)}
|
||||||
|
onChange={changeUserPermissions(user)}
|
||||||
|
value={pond.Permission.PERMISSION_USERS as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Manage Users"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 6, sm: 3 }} container justifyContent="center">
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!canRead}
|
||||||
|
checked={permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
|
||||||
|
onChange={changeUserPermissions(user)}
|
||||||
|
value={pond.Permission.PERMISSION_FILE_MANAGEMENT as pond.Permission}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="File Management"
|
||||||
|
labelPlacement="end"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToTeam = (teamKey: string) => {
|
||||||
|
navigate("/teams/" + teamKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const userListItem = (user: Team) => {
|
||||||
|
let isSelf = checkIsSelf(user);
|
||||||
|
let isRemoved = userIsRemoved(user);
|
||||||
|
let name = user.name();
|
||||||
|
let permissions = user.permissions;
|
||||||
|
let userSettings = pond.TeamSettings.create(user.settings !== null ? user.settings : undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
alignItems="flex-start"
|
||||||
|
sx={{
|
||||||
|
opacity: isRemoved ? 0.5 : 1,
|
||||||
|
pointerEvents: isRemoved ? 'none' : 'auto',
|
||||||
|
cursor: isRemoved ? 'not-allowed' : 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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={
|
||||||
|
<Link
|
||||||
|
onClick={() => {
|
||||||
|
goToTeam(user.key());
|
||||||
|
}}>
|
||||||
|
<Typography
|
||||||
|
component="div"
|
||||||
|
variant="body1"
|
||||||
|
color="textPrimary"
|
||||||
|
style={{ cursor: "pointer" }}>
|
||||||
|
{name + (isSelf ? " (you)" : "")}
|
||||||
|
</Typography>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography component="div" variant="caption" color="textSecondary">
|
||||||
|
{isRemoved ? "REMOVED " : userRoleFromPermissions(permissions)}
|
||||||
|
</Typography>
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isSelf ? (
|
||||||
|
<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 &&
|
||||||
|
!isRemoved &&
|
||||||
|
(!permissions.includes(pond.Permission.PERMISSION_USERS) || canProvision) && (
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="Revoke user's access">
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
aria-label="Remove user from device"
|
||||||
|
onClick={() => removeUser(or(user.settings, { key: "" }).key)}>
|
||||||
|
<RemoveUserIcon className={classes.removeIcon} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectUsersList = () => {
|
||||||
|
//const sortedUsers = sortUsersByRole(users);
|
||||||
|
|
||||||
|
let userListItems: any = [];
|
||||||
|
for (var i = 0; i < users.length; i++) {
|
||||||
|
let user = users[i];
|
||||||
|
if (user) {
|
||||||
|
userListItems.push(
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
{userListItem(user)}
|
||||||
|
{userPermissionManagement(user)}
|
||||||
|
<Divider />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <List>{userListItems}</List>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Grid container justifyContent="center" alignContent="center" className={classes.verticalSpacing}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (users.length < 1) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
align="center"
|
||||||
|
color="textSecondary"
|
||||||
|
className={!cardMode ? classes.verticalSpacing : ""}
|
||||||
|
style={{ padding: cardMode ? 0 : "" }}>
|
||||||
|
No teams found.
|
||||||
|
</Typography>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectUsersList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = () => {
|
||||||
|
return (
|
||||||
|
<Grid container direction="row" justifyContent="space-between">
|
||||||
|
<Grid size={{ xs: 4 }}></Grid>
|
||||||
|
<Grid size={{ xs: 8 }} container justifyContent="flex-end">
|
||||||
|
{!cardMode && (
|
||||||
|
<Button onClick={close} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{canManageUsers && (
|
||||||
|
<Button onClick={submit} color="primary" disabled={objectUsersUnchanged()}>
|
||||||
|
{cardMode ? "Update" : "Submit"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogs = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<RemoveSelfFromObject
|
||||||
|
scope={scope}
|
||||||
|
label={label}
|
||||||
|
isDialogOpen={removeSelfDialogIsOpen}
|
||||||
|
closeDialogCallback={closeRemoveSelfDialog}
|
||||||
|
/>
|
||||||
|
<ShareWithTeam
|
||||||
|
scope={scope}
|
||||||
|
label={label}
|
||||||
|
permissions={permissions}
|
||||||
|
isDialogOpen={isShareObjectDialogOpen}
|
||||||
|
closeDialogCallback={closeShareObjectDialog}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
540
src/teams/ShareWithTeam.tsx
Normal file
540
src/teams/ShareWithTeam.tsx
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
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 {
|
||||||
|
Add as AddIcon,
|
||||||
|
Link as LinkIcon,
|
||||||
|
LinkOff,
|
||||||
|
RemoveCircle as RemoveCircleIcon
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
// import { MobileDateTimePicker } from "@material-ui/pickers";
|
||||||
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||||
|
import { usePermissionAPI, usePrevious, useSnackbar } from "hooks";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { binScope, Scope, ShareableLink } 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 { useGateAPI, useBinAPI } from "providers";
|
||||||
|
import TeamSearch from "./TeamSearch";
|
||||||
|
import { green, red } from "@mui/material/colors";
|
||||||
|
import { makeStyles } from "@mui/styles";
|
||||||
|
import { MobileDateTimePicker } from "@mui/x-date-pickers";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
return ({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShareObject(props: Props) {
|
||||||
|
const binAPI = useBinAPI();
|
||||||
|
const gateAPI = useGateAPI();
|
||||||
|
const classes = useStyles();
|
||||||
|
const permissionAPI = usePermissionAPI();
|
||||||
|
const { info, success } = useSnackbar();
|
||||||
|
const { scope, label, permissions, isDialogOpen, closeDialogCallback } = props;
|
||||||
|
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 [teamKey, setTeamKey] = useState<string>("");
|
||||||
|
|
||||||
|
const share = () => {
|
||||||
|
permissionAPI.shareObjectByKey(scope, teamKey, sharedPermissions).then(resp => {
|
||||||
|
let shareBins = true;
|
||||||
|
if (resp && resp.data && resp.data.existing) {
|
||||||
|
success(label + " was shared with team");
|
||||||
|
} else {
|
||||||
|
success("Team 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.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(err => {
|
||||||
|
successBins = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!successBins) {
|
||||||
|
openSnackbar("error", "One or more bins failed to share");
|
||||||
|
} else {
|
||||||
|
openSnackbar("success", "Bins shared");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.shareObjectByKey(newScope, teamKey, sharedPermissions).catch(err => {
|
||||||
|
successGates = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!successGates) {
|
||||||
|
openSnackbar("error", "One or more Gates failed to share");
|
||||||
|
} else {
|
||||||
|
openSnackbar("success", "Gates shared");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*permissionAPI
|
||||||
|
.shareObject(scope, email, sharedPermissions)
|
||||||
|
.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 {
|
||||||
|
openSnackbar(Status.Success, "Bins shared with " + email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 close = (callback: boolean) => {
|
||||||
|
closeDialogCallback(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 "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 teamKey !== "" && sharedPermissions.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI BEGINS
|
||||||
|
|
||||||
|
const teamsList = () => {
|
||||||
|
return <TeamSearch label="Team" setTeamCallback={setTeamKey} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailSharing = () => {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{teamsList()}
|
||||||
|
<div style={{ width: "auto", height: "1rem" }} />
|
||||||
|
{permissionsForm()}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
// renderInput={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 (
|
||||||
|
<ResponsiveDialog
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
open={isDialogOpen}
|
||||||
|
onClose={() => close(false)}
|
||||||
|
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(false)} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{tab === 0 && (
|
||||||
|
<Button onClick={share} color="primary" disabled={!isValid()}>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogActions>
|
||||||
|
</ResponsiveDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/teams/TeamSearch.tsx
Normal file
98
src/teams/TeamSearch.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import SearchSelect, { Option } from "common/SearchSelect";
|
||||||
|
import { useGlobalState, useTeamAPI, useUserAPI } from "providers";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
setTeamCallback?: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
label?: string;
|
||||||
|
loadUsers?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TeamSearch(props: Props) {
|
||||||
|
const { className, setTeamCallback, style, label, loadUsers } = props;
|
||||||
|
const teamAPI = useTeamAPI();
|
||||||
|
const userAPI = useUserAPI();
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState<Option | null>(null);
|
||||||
|
const [teams, setTeams] = useState<Option[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [{ user }] = useGlobalState();
|
||||||
|
|
||||||
|
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTeams = useCallback(() => {
|
||||||
|
let options: Option[] = [];
|
||||||
|
setLoading(true);
|
||||||
|
teamAPI
|
||||||
|
.listTeams(10, 0, "desc", undefined, search)
|
||||||
|
.then(resp => {
|
||||||
|
if (loadUsers)
|
||||||
|
options.push({
|
||||||
|
value: user.id(),
|
||||||
|
label: "Yourself",
|
||||||
|
icon: "default",
|
||||||
|
group: "Users"
|
||||||
|
});
|
||||||
|
resp.data.teams.forEach(team => {
|
||||||
|
let o = {
|
||||||
|
value: team.settings?.key,
|
||||||
|
label: team.settings!.name,
|
||||||
|
icon: team.settings?.avatar,
|
||||||
|
group: "Teams"
|
||||||
|
};
|
||||||
|
options.push(o);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (loadUsers) {
|
||||||
|
userAPI
|
||||||
|
.listUsers(10, 0, "desc", undefined, search)
|
||||||
|
.then(resp => {
|
||||||
|
resp.users.forEach(user => {
|
||||||
|
options.push({
|
||||||
|
value: user.id(),
|
||||||
|
label: user.name(),
|
||||||
|
icon: user.settings.avatar,
|
||||||
|
group: "Users"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
setTeams(options);
|
||||||
|
});
|
||||||
|
}, [teamAPI, loadUsers, userAPI, user, search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTeams();
|
||||||
|
}, [loadTeams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={className} style={style}>
|
||||||
|
<SearchSelect
|
||||||
|
getOptionSelected={() => {
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
label={label ? label : "View As"}
|
||||||
|
options={teams}
|
||||||
|
loading={loading}
|
||||||
|
group
|
||||||
|
selected={selectedTeam}
|
||||||
|
onChange={onChange}
|
||||||
|
changeSelection={(option: Option | null) => {
|
||||||
|
setSelectedTeam(option);
|
||||||
|
if (setTeamCallback) setTeamCallback(option?.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue