device settings and actions made functional

This commit is contained in:
Carter 2025-01-10 14:42:30 -06:00
parent 3ce00facd1
commit 4e7c68401b
19 changed files with 3042 additions and 41 deletions

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

View file

@ -9,12 +9,10 @@ import {
TextField,
Theme
} from "@mui/material";
// import { red } from "@mui/icons-materia";
// import { createStyles, makeStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Delete as DeleteIcon } from "@mui/icons-material";
import ColourPicker from "common/ColourPicker";
import { Tag as TagModel } from "models";
import { useGlobalState, useTagAPI, useSnackbar } from "providers";
import { useTagAPI, useSnackbar } from "providers";
import { useState } from "react";
import { Tag as TagUI } from "./Tag";
import { red } from "@mui/material/colors";
@ -92,7 +90,7 @@ interface Props {
mode: "add" | "update";
}
const DeleteButton = withStyles((theme: Theme) => ({
const DeleteButton = withStyles((_theme: Theme) => ({
root: {
color: "#FFF",
backgroundColor: red[500],
@ -133,7 +131,7 @@ export default function TagSettings(props: Props) {
default:
tagAPI
.updateTag(tag.settings.key, tag.settings)
.then((response: any) => {
.then((_response: any) => {
// dispatch({
// key: "tags",
// 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") {
setTag(new TagModel());
}

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

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

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

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

View file

@ -61,6 +61,8 @@ import ShareObject from "user/ShareObject";
import { amber, blue, green } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import DeviceSettings from "device/DeviceSettings";
import ObjectTeams from "teams/ObjectTeams";
const useStyles = makeStyles((theme: Theme) => {
// const isMobile = useMobile()
@ -371,16 +373,18 @@ export default function DeviceActions(props: Props) {
isJsonDataDialogOpen
} = dialogState;
console.log(canWrite)
return (
<React.Fragment>
{/* <DeviceSettings
<DeviceSettings
device={device}
isDialogOpen={or(isDeviceSettingsDialogOpen, false)}
closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")}
refreshCallback={refreshCallback}
canEdit={canWrite}
components={components}
/> */}
// components={components}
components={[]}
/>
{/* <ComponentSettings
mode="add"
device={device}
@ -421,14 +425,14 @@ export default function DeviceActions(props: Props) {
closeDialogCallback={() => closeDialog("isObjectUsersDialogOpen")}
refreshCallback={refreshCallback}
/>
{/* <ObjectTeams
<ObjectTeams
scope={deviceScope(device.id().toString())}
label={deviceName}
permissions={permissions}
isDialogOpen={or(isObjectTeamsDialogOpen, false)}
closeDialogCallback={() => closeDialog("isObjectTeamsDialogOpen")}
refreshCallback={refreshCallback}
/> */}
/>
<RemoveSelfFromObject
scope={deviceScope(device.id().toString())}
label={deviceName}

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

View file

@ -1,4 +1,5 @@
import {
CircularProgress,
Dialog,
DialogContent,
DialogTitle,
@ -18,10 +19,11 @@ import { Tag as TagUI } from "common/Tag";
import TagSettings from "common/TagSettings";
import { Device, Tag } from "models";
import { filterByTag } from "pbHelpers/Tag";
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
import { useDeviceAPI, useSnackbar, useTagAPI } from "providers";
import React, { useEffect, useRef, useState } from "react";
import { pond } from "protobuf-ts/pond";
const useStyles = makeStyles((theme: Theme) => {
const useStyles = makeStyles((_theme: Theme) => {
return ({
addIcon: {
color: "var(--status-ok)"
@ -37,13 +39,27 @@ interface AddDeviceTagProps {
function AddDeviceTag(props: AddDeviceTagProps) {
const classes = useStyles();
const tagAPI = useTagAPI();
const { device, deviceTags, addTagToDevice } = props;
const [open, setOpen] = useState(false);
const [createNewOpen, setCreateNewOpen] = useState(false);
const [searchValue, setSearchValue] = useState("");
// const [{ tags }] = useGlobalState();
const [tags, setTags] = useState<Tag[]>([])
const 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
.filter(tag => !deviceTags.some(key => key === tag.settings.key))
.filter(tag => filterByTag(searchValue, tag))
@ -117,12 +133,35 @@ interface DeviceTagsProps {
export default function DeviceTags(props: DeviceTagsProps) {
const { device, disableAdd } = props;
// const [{ tags }] = useGlobalState();
const tagAPI = useTagAPI()
const [tags, setTags] = useState<Tag[]>([])
const [loading, setLoading] = useState(false)
const deviceAPI = useDeviceAPI();
const { error } = useSnackbar();
const [deviceTags, setDeviceTags] = useState<string[]>(device.status.tagKeys);
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(() => {
if (previousDeviceRef.current !== device) {
setDeviceTags(device.status.tagKeys);
@ -156,6 +195,12 @@ export default function DeviceTags(props: DeviceTagsProps) {
return null;
}
if (loading) {
return (
<CircularProgress />
)
}
return (
<React.Fragment>
{deviceTags.map(key => {

View file

@ -8,18 +8,11 @@ import PageContainer from "./PageContainer";
import { useMobile } from "hooks";
import LoadingScreen from "app/LoadingScreen";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import DeviceActions from "common/DeviceActions";
import DeviceActions from "device/DeviceActions";
import { pond } from "protobuf-ts/pond";
import { cloneDeep } from "lodash";
import DeviceOverview from "device/DeviceOverview";
// const useStyles = makeStyles((theme: Theme) => {
// // const isMobile = useMobile()
// return ({
// });
// });
export default function DevicePage() {
const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar()
@ -32,8 +25,8 @@ export default function DevicePage() {
const [permissions, setPermissions] = useState<pond.Permission[]>([])
const [preferences, setPreferences] = useState<pond.DevicePreferences>(pond.DevicePreferences.create())
const [cellularUsage, setCellularUsage] = useState<number>(0);
const [cellularStatus, setCellularStatus] = useState<string>("");
const [cellularUsage, _setCellularUsage] = useState<number>(0);
const [cellularStatus, _setCellularStatus] = useState<string>("");
const loadDevice = () => {
if (loading) return
@ -50,7 +43,16 @@ export default function DevicePage() {
setLoading(true)
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => {
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);
setPreferences(u.preferences)
}).catch(err => {
@ -77,6 +79,10 @@ export default function DevicePage() {
.finally(() => setLoading(false));
}
useEffect(() => {
console.log(permissions)
}, [permissions])
useEffect(() => {
loadDevice()
}, [deviceID])
@ -110,9 +116,6 @@ export default function DevicePage() {
return (
<PageContainer padding={isMobile ? 0 : 2}>
{/* <Typography>
{device.name()}
</Typography> */}
<Grid2 container justifyContent={"space-between"}>
<Grid2>
<SmartBreadcrumb />

View file

@ -125,6 +125,8 @@ export default function Devices() {
let keys = getContextKeys()
keys.splice(keys.length - 1, 0, tab);
return keys
} else {
return getContextKeys()
}
}
@ -133,6 +135,8 @@ export default function Devices() {
let types = getContextTypes()
types.splice(types.length - 1, 0, "group");
return types
} else {
return getContextTypes()
}
}
@ -160,6 +164,7 @@ export default function Devices() {
const loadDevices = () => {
setDevicesLoading(true)
console.log(getKeys())
deviceAPI.list(
limit,
page*limit,

View file

@ -2,7 +2,6 @@ import { cyan } from "@mui/material/colors";
import AcceptedIcon from "@mui/icons-material/CloudDone";
import PendingIcon from "@mui/icons-material/CloudQueueTwoTone";
import RejectedIcon from "@mui/icons-material/SmsFailed";
import React from "react";
export interface StatusHelper {
description: string;

617
src/teams/ObjectTeams.tsx Normal file
View 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
View 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
View 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>
);
}