device settings and actions made functional
This commit is contained in:
parent
3ce00facd1
commit
4e7c68401b
19 changed files with 3042 additions and 41 deletions
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,
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
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 { 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}
|
||||
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 {
|
||||
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 => {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
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