device settings and actions made functional

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

View file

@ -0,0 +1,541 @@
import {
Divider,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Theme,
Tooltip
} from "@mui/material";
// import { amber, blue, green } from "@mui/co";
import {
AddCircle as AddIcon,
ExitToApp as RemoveSelfIcon,
History as HistoryIcon,
MoreVert as MoreIcon,
PauseCircleFilled,
PlayCircleFilled,
Reorder as ComponentOrderIcon,
Save as SaveDeviceProfileIcon,
SaveAlt as LoadDeviceProfileIcon,
Settings as DeviceSettingsIcon,
Share as ShareObjectIcon,
AccountCircle as ObjectUsersIcon,
SupervisedUserCircle as ObjectTeamsIcon,
Sync as SyncDeviceIcon,
Wifi as WifiIcon
} from "@mui/icons-material";
import { Skeleton } from "@mui/material";
import Datadog from "assets/external/datadog.png";
import { ImgIcon } from "common/ImgIcon";
import NotificationButton from "common/NotificationButton";
// import ComponentOrder from "component/ComponentOrder";
// import ComponentSettings from "component/ComponentSettings";
// import DeviceSettings from "device/DeviceSettings";
// import LoadDeviceProfile from "device/LoadDeviceProfile";
// import { PauseData } from "device/PauseData";
// import { ResumeData } from "device/ResumeData";
// import SaveDeviceProfile from "device/SaveDeviceProfile";
// import SyncDevice from "device/SyncDevice";
// import UpgradeDevice from "device/UpgradeDevice";
// import InteractionSettings from "interactions/InteractionSettings";
import { cloneDeep } from "lodash";
// import { Component, Device, deviceScope, Interaction } from "models";
import { Device, deviceScope } from "models";
// import { MatchParams } from "navigation/Routes";
import { isShareableLink } from "pbHelpers/Device";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React, { useState } from "react";
// import { useHistory, useRouteMatch } from "react-router";
import { isOffline } from "utils/environment";
import { or } from "utils/types";
import ObjectUsers from "user/ObjectUsers";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
// import Connection from "./Connection";
// import ObjectTeams from "teams/ObjectTeams";
// import ArcGISDeviceData from "./ArcGISDeviceData";
// import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
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()
return ({
greenIcon: {
color: green["500"],
"&:hover": {
color: green["600"]
}
},
amberIcon: {
color: amber["600"],
"&:hover": {
color: amber["700"]
}
},
blueIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
red: {
color: "var(--status-alert)"
}
});
});
interface Props {
device: Device;
isPaused: boolean;
// components: Component[];
// interactions: Interaction[];
refreshCallback: () => void;
// availablePositions: DeviceAvailabilityMap;
// availableOffsets: OffsetAvailabilityMap;
permissions: Array<pond.Permission>;
preferences: pond.DevicePreferences;
isLoading: boolean;
toggleNotificationPreference: Function;
}
interface DialogState {
isAddComponentDialogOpen: boolean;
isDeviceSettingsDialogOpen: boolean;
isShareObjectDialogOpen: boolean;
isSyncDeviceDialogOpen: boolean;
isPauseDialogOpen: boolean;
isResumeDialogOpen: boolean;
isInteractionSettingsOpen: boolean;
isObjectUsersDialogOpen: boolean;
isObjectTeamsDialogOpen: boolean;
isRemoveSelfDialogOpen: boolean;
isSaveDeviceProfileOpen: boolean;
isLoadDeviceProfileOpen: boolean;
isConnectionDialogOpen: boolean;
isComponentOrderDialogOpen: boolean;
isJsonDataDialogOpen: boolean;
}
export default function DeviceActions(props: Props) {
const {
device,
isPaused,
// components,
// interactions,
refreshCallback,
// availablePositions,
// availableOffsets,
permissions,
preferences,
isLoading,
toggleNotificationPreference
} = props;
const classes = useStyles();
const [{ user }] = useGlobalState();
const canWrite = permissions.includes(pond.Permission.PERMISSION_WRITE);
const navigate = useNavigate();
// const match = useRouteMatch<MatchParams>();
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const [dialogState, setDialogState] = useState<DialogState>({
isAddComponentDialogOpen: false,
isDeviceSettingsDialogOpen: false,
isShareObjectDialogOpen: false,
isSyncDeviceDialogOpen: false,
isPauseDialogOpen: false,
isResumeDialogOpen: false,
isInteractionSettingsOpen: false,
isObjectUsersDialogOpen: false,
isObjectTeamsDialogOpen: false,
isRemoveSelfDialogOpen: false,
isSaveDeviceProfileOpen: false,
isLoadDeviceProfileOpen: false,
isConnectionDialogOpen: false,
isComponentOrderDialogOpen: false,
isJsonDataDialogOpen: false
});
const openDialog = (target: keyof DialogState) => {
let updatedDialogState = cloneDeep(dialogState);
updatedDialogState[target] = true;
setDialogState(updatedDialogState);
setMenuAnchorEl(null);
};
const closeDialog = (target: keyof DialogState) => {
let updatedDialogState = cloneDeep(dialogState);
updatedDialogState[target] = false;
setDialogState(updatedDialogState);
};
const closeAndRefresh = (target: keyof DialogState) => (refresh: boolean) => {
closeDialog(target);
if (refresh) props.refreshCallback();
};
const deviceMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
const canProvision = user.allowedTo("provision");
const isCellular = device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON;
const canPause = canWrite && isCellular && user.allowedTo("pause-data");
const isWifi = device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_PHOTON;
// const isShareLink = isShareableLink(match.params.deviceID);
const isShareLink = false
return (
<Menu
id="deviceMenu"
anchorEl={menuAnchorEl ? menuAnchorEl : null}
open={menuAnchorEl !== null}
onClose={() => setMenuAnchorEl(null)}
disableAutoFocusItem>
{canWrite && (
<MenuItem
onClick={() => {
openDialog("isAddComponentDialogOpen");
}}
dense>
<ListItemIcon>
<AddIcon className={classes.greenIcon} />
</ListItemIcon>
<ListItemText secondary="Add Component" />
</MenuItem>
)}
{canWrite && (
<MenuItem onClick={() => openDialog("isInteractionSettingsOpen")} dense>
<ListItemIcon>
<AddIcon className={classes.greenIcon} />
</ListItemIcon>
<ListItemText secondary="Add Interaction" />
</MenuItem>
)}
{canWrite && <Divider />}
{!isOffline() && canShare && (
<MenuItem onClick={() => openDialog("isShareObjectDialogOpen")} dense>
<ListItemIcon>
<ShareObjectIcon className={classes.blueIcon} />
</ListItemIcon>
<ListItemText secondary="Share" />
</MenuItem>
)}
{canWrite &&
user.hasAdmin() && ( //TODO: once the resync issue has been resolved remove the admin check
<MenuItem onClick={() => openDialog("isSyncDeviceDialogOpen")} dense>
<ListItemIcon>
<SyncDeviceIcon className={classes.amberIcon} />
</ListItemIcon>
<ListItemText secondary="Resync" />
</MenuItem>
)}
{user.hasFeature("json") && (
<MenuItem onClick={() => openDialog("isJsonDataDialogOpen")} dense>
<ListItemIcon>
<LoadDeviceProfileIcon className={classes.greenIcon} />
</ListItemIcon>
<ListItemText secondary="Export Json Data" />
</MenuItem>
)}
<MenuItem onClick={() => openDialog("isComponentOrderDialogOpen")} dense>
<ListItemIcon>
<ComponentOrderIcon />
</ListItemIcon>
<ListItemText secondary="Component Order" />
</MenuItem>
<Divider />
{canPause && !isPaused && (
<MenuItem onClick={() => openDialog("isPauseDialogOpen")} dense>
<ListItemIcon>
<PauseCircleFilled />
</ListItemIcon>
<ListItemText secondary="Pause Data" />
</MenuItem>
)}
{canPause && isPaused && (
<MenuItem onClick={() => openDialog("isResumeDialogOpen")} dense>
<ListItemIcon>
<PlayCircleFilled className={classes.greenIcon} />
</ListItemIcon>
<ListItemText secondary="Resume Data" />
</MenuItem>
)}
{isWifi && canWrite && (
<MenuItem onClick={() => openDialog("isConnectionDialogOpen")} dense>
<ListItemIcon>
<WifiIcon />
</ListItemIcon>
<ListItemText secondary="Connection" />
</MenuItem>
)}
{(canPause || (isWifi && canWrite)) && <Divider />}
{canProvision && (
<MenuItem onClick={() => openDialog("isSaveDeviceProfileOpen")} dense>
<ListItemIcon>
<SaveDeviceProfileIcon className={classes.blueIcon} />
</ListItemIcon>
<ListItemText secondary="Save Profile" />
</MenuItem>
)}
{canProvision && (
<MenuItem onClick={() => openDialog("isLoadDeviceProfileOpen")} dense>
<ListItemIcon>
<LoadDeviceProfileIcon className={classes.greenIcon} />
</ListItemIcon>
<ListItemText secondary="Load Profile" />
</MenuItem>
)}
{canProvision && <Divider />}
{!isOffline() && canManageUsers && (
<MenuItem onClick={() => openDialog("isObjectUsersDialogOpen")} dense>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText secondary="Users" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem onClick={() => openDialog("isObjectTeamsDialogOpen")} dense>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText secondary="Teams" />
</MenuItem>
)}
<MenuItem
onClick={() => {
// const groupID: number = parseInt(match.params.groupID, 10);
// const groupPath: string = groupID > 0 ? "/groups/" + groupID.toString() : "";
const devicePath: string = "/devices/" + device.id().toString();
// let path = groupPath + devicePath + "/history";
let path = devicePath + "/history";
navigate(path);
}}
dense>
<ListItemIcon>
<HistoryIcon />
</ListItemIcon>
<ListItemText secondary="History" />
</MenuItem>
{/* {canProvision && (
<MenuItem
onClick={() => {
window.open(
"https://app.datadoghq.com/logs?cols=core_host%2Ccore_service&index=main&live=true&messageDisplay=inline&stream_sort=desc&query=%40device%3A" +
device.id(),
"_blank"
);
}}
dense>
<ListItemIcon>
<ImgIcon src={Datadog} />
</ListItemIcon>
<ListItemText secondary="Datadog" />
</MenuItem>
)} */}
<Divider />
{!isShareLink && (
<MenuItem onClick={() => openDialog("isRemoveSelfDialogOpen")} dense>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText secondary="Leave" />
</MenuItem>
)}
</Menu>
);
};
const dialogs = () => {
const deviceName = device.name();
const {
isAddComponentDialogOpen,
isDeviceSettingsDialogOpen,
isShareObjectDialogOpen,
isSyncDeviceDialogOpen,
isPauseDialogOpen,
isResumeDialogOpen,
isInteractionSettingsOpen,
isObjectUsersDialogOpen,
isObjectTeamsDialogOpen,
isRemoveSelfDialogOpen,
isSaveDeviceProfileOpen,
isLoadDeviceProfileOpen,
isConnectionDialogOpen,
isComponentOrderDialogOpen,
isJsonDataDialogOpen
} = dialogState;
console.log(canWrite)
return (
<React.Fragment>
<DeviceSettings
device={device}
isDialogOpen={or(isDeviceSettingsDialogOpen, false)}
closeDialogCallback={() => closeDialog("isDeviceSettingsDialogOpen")}
refreshCallback={refreshCallback}
canEdit={canWrite}
// components={components}
components={[]}
/>
{/* <ComponentSettings
mode="add"
device={device}
isDialogOpen={or(isAddComponentDialogOpen, false)}
closeDialogCallback={() => closeDialog("isAddComponentDialogOpen")}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={refreshCallback}
canEdit={canWrite}
/>
<SyncDevice
device={device}
isDialogOpen={or(isSyncDeviceDialogOpen, false)}
closeDialogCallback={() => closeDialog("isSyncDeviceDialogOpen")}
refreshCallback={refreshCallback}
/> */}
<ShareObject
scope={deviceScope(device.id().toString())}
label={deviceName}
permissions={permissions}
isDialogOpen={or(isShareObjectDialogOpen, false)}
closeDialogCallback={() => closeDialog("isShareObjectDialogOpen")}
/>
{/* <InteractionSettings
device={device}
components={cloneDeep(components)}
mode="add"
isDialogOpen={or(isInteractionSettingsOpen, false)}
closeDialogCallback={() => closeDialog("isInteractionSettingsOpen")}
refreshCallback={refreshCallback}
canEdit={canWrite}
/> */}
<ObjectUsers
scope={deviceScope(device.id().toString())}
label={deviceName}
permissions={permissions}
isDialogOpen={or(isObjectUsersDialogOpen, false)}
closeDialogCallback={() => closeDialog("isObjectUsersDialogOpen")}
refreshCallback={refreshCallback}
/>
<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}
isDialogOpen={isRemoveSelfDialogOpen}
closeDialogCallback={() => closeDialog("isRemoveSelfDialogOpen")}
/>
{/* <SaveDeviceProfile
isDialogOpen={isSaveDeviceProfileOpen}
closeDialogCallback={() => closeDialog("isSaveDeviceProfileOpen")}
device={device}
components={components}
interactions={interactions}
tagIds={device.status.tagKeys}
refreshCallback={refreshCallback}
/> */}
{/* <LoadDeviceProfile
isDialogOpen={isLoadDeviceProfileOpen}
closeDialogCallback={() => closeDialog("isLoadDeviceProfileOpen")}
device={device}
refreshCallback={refreshCallback}
/> */}
{/* <PauseData
id={device.id()}
isOpen={isPauseDialogOpen}
close={closeAndRefresh("isPauseDialogOpen")}
/>
<ResumeData
id={device.id()}
isOpen={isResumeDialogOpen}
close={closeAndRefresh("isResumeDialogOpen")}
/>
<Connection
deviceID={device.id()}
open={isConnectionDialogOpen}
close={closeAndRefresh("isConnectionDialogOpen")}
deviceName={device.name()}
/>
<ComponentOrder
device={device}
devicePreferences={preferences}
components={components}
open={isComponentOrderDialogOpen}
close={closeAndRefresh("isComponentOrderDialogOpen")}
/>
<ArcGISDeviceData
open={isJsonDataDialogOpen}
close={closeAndRefresh("isJsonDataDialogOpen")}
device={device}
components={components}
/> */}
</React.Fragment>
);
};
const buttons = () => {
return (
<React.Fragment>
{/* {canWrite && user.allowedTo("provision") && <UpgradeDevice device={device} />} */}
{/* {!isShareableLink(match.params.deviceID) && ( */}
<NotificationButton
notify={preferences.notify}
onChange={toggleNotificationPreference}
tooltip={
"Notifications for " +
device.name() +
" are " +
(preferences.notify ? "enabled" : "disabled")
}
hidden={isLoading}
/>
{/* )} */}
<Tooltip title="Settings">
<IconButton onClick={() => openDialog("isDeviceSettingsDialogOpen")}>
<DeviceSettingsIcon />
</IconButton>
</Tooltip>
<Tooltip title="More">
<IconButton
aria-owns={"deviceMenu"}
aria-haspopup="true"
onClick={event => setMenuAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
</Tooltip>
</React.Fragment>
);
};
if (isLoading)
return (
<Skeleton>
<IconButton />
<IconButton />
<IconButton />
</Skeleton>
);
return (
<React.Fragment>
{buttons()}
{deviceMenu()}
{dialogs()}
</React.Fragment>
);
}

View file

@ -0,0 +1,680 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
FormControlLabel,
Grid2 as Grid,
List,
ListItem,
MenuItem,
Switch,
Tab,
Tabs,
TextField,
Theme,
Typography
} from "@mui/material";
// import { Theme } from "@material-ui/core/styles/createMuiTheme";
import DeleteButton from "common/DeleteButton";
import PeriodSelect from "common/time/PeriodSelect";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useDeviceAPI, usePrevious, useSnackbar } from "hooks";
import { Component, Device } from "models";
import { IsExtended, ListDeviceProductDescribers } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React, { useEffect, useState } from "react";
// import { useHistory } from "react-router";
import { quack } from "protobuf-ts/quack";
import LinearMutationBuilder from "common/LinearMutationBuilder";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
const useStyles = makeStyles((theme: Theme) => {
return ({
dialogContent: {
minHeight: "25vh"
},
removeDeviceDialog: {
zIndex: theme.zIndex.modal + 1
},
tabSmall: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1,
minWidth: "48px",
marginTop: 0,
paddingTop: 0
}
})
});
interface Props {
device: Device;
isDialogOpen: boolean;
closeDialogCallback: Function;
canEdit: boolean;
refreshCallback: Function;
components?: Component[];
}
const deviceFromForm = (device: Device): Device => {
if (device.settings.upgradeChannel === pond.UpgradeChannel.UPGRADE_CHANNEL_INVALID) {
device.settings.upgradeChannel = pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE;
}
return device;
};
export default function DeviceSettings(props: Props) {
const classes = useStyles();
const [{ user }] = useGlobalState();
const navigate = useNavigate();
const { success, error } = useSnackbar();
const deviceAPI = useDeviceAPI();
const { device, isDialogOpen, closeDialogCallback, canEdit, refreshCallback, components } = props;
const prevDevice = usePrevious(device);
const [deviceForm, setDeviceForm] = useState<Device>(deviceFromForm(Device.clone(device)));
const [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState<boolean>(false);
const [sleeps, setSleeps] = useState<boolean>(device.settings.sleepDurationS > 0);
const [compExtOne, setCompExtOne] = useState("");
const [compExtTwo, setCompExtTwo] = useState("");
const [compExtThree, setCompExtThree] = useState("");
const [currentTab, setCurrentTab] = useState(0);
const [linearMutations, setLinearMutations] = useState<pond.LinearMutation[]>([]);
const [componentsByDevice, setComponentsByDevice] = useState<Map<string, Component[]>>(
new Map<string, Component[]>()
);
const [newMutationDialog, setNewMutationDialog] = useState(false);
const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>();
useEffect(() => {
if (prevDevice !== device) {
setDeviceForm(deviceFromForm(Device.clone(props.device)));
if (device.settings.extensionComponents[0]) {
setCompExtOne(device.settings.extensionComponents[0]);
}
if (device.settings.extensionComponents[1]) {
setCompExtTwo(device.settings.extensionComponents[1]);
}
if (device.settings.extensionComponents[2]) {
setCompExtThree(device.settings.extensionComponents[2]);
}
if (device.settings.mutations) {
setLinearMutations(device.settings.mutations);
}
if (components) {
let cbd = new Map<string, Component[]>();
cbd.set(device.id().toString(), components);
setComponentsByDevice(cbd);
}
}
}, [device, prevDevice, props.device, components]);
const close = () => {
closeDialogCallback();
};
const isSleepDurationValid = (device: Device): boolean => {
return (
!sleeps ||
(sleeps && device.settings.sleepDurationS >= 10 && device.settings.sleepDelayMs >= 1000)
);
};
const minCheckPeriodS = (): number => {
let defaultPeriod = 60;
switch (device.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
return user.hasFeature("admin") ? defaultPeriod : 300;
default:
return defaultPeriod;
}
};
const minCheckPeriodDescription = (): string => {
let min = minCheckPeriodS() / 60;
return min.toString() + " minute" + (min !== 1 ? "s" : "");
};
const isCheckPeriodValid = (device: Device): boolean => {
return (
device.settings.pondCheckPeriodS >= minCheckPeriodS() &&
device.settings.pondCheckPeriodS <= 3600
);
};
const isFormValid = (): boolean => {
return isSleepDurationValid(deviceForm) && isCheckPeriodValid(deviceForm);
};
const changeName = (event: any) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.name = String(event.target.value);
setDeviceForm(updatedForm);
};
const changeDescription = (event: any) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.description = String(event.target.value);
setDeviceForm(updatedForm);
};
const changeSleepDuration = (ms: number) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.sleepDurationS = ms / 1000;
setDeviceForm(updatedForm);
};
const changeSleepDelay = (ms: number) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.sleepDelayMs = ms;
setDeviceForm(updatedForm);
};
const changePondCheckPeriod = (ms: number) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.pondCheckPeriodS = ms / 1000;
setDeviceForm(updatedForm);
};
const changeUpgradeChannel = (event: any) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.upgradeChannel = event.target.value;
setDeviceForm(updatedForm);
};
const changeProduct = (event: any) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.product = event.target.value;
setDeviceForm(updatedForm);
};
const upgradeChannelHelper = (): string => {
switch (deviceForm.settings.upgradeChannel) {
case pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE: {
return "";
}
case pond.UpgradeChannel.UPGRADE_CHANNEL_BETA: {
return "The newest field tested features as they become available";
}
case pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA: {
return "The newest lab tested features as they become available";
}
case pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT: {
return "Features as they're written, expect things to break";
}
}
return "";
};
const toggleAutomaticallyUpgrade = (event: any) => {
let updatedForm = Device.clone(deviceForm);
updatedForm.settings.automaticallyUpgrade = event.target.checked;
setDeviceForm(updatedForm);
};
const toggleSleeps = (event: any) => {
let updatedForm = Device.clone(deviceForm);
let updatedSleeps = event.target.checked;
updatedSleeps
? (updatedForm.settings.sleepType = quack.SleepType.SLEEP_TYPE_NAP)
: (updatedForm.settings.sleepType = quack.SleepType.SLEEP_TYPE_NONE);
setSleeps(updatedSleeps);
setDeviceForm(updatedForm);
};
const submitSettings = () => {
const deviceName = deviceForm.name();
const deviceID = device.id();
if (!sleeps) {
deviceForm.settings.sleepDurationS = 0;
}
deviceForm.settings.extensionComponents = [compExtOne, compExtTwo, compExtThree];
deviceForm.settings.mutations = linearMutations;
deviceAPI
.update(deviceID, deviceForm.settings)
.then((_response: any) => {
success(deviceName + " was successfully updated!");
close();
refreshCallback();
})
.catch((_err: any) => {
error("Error occured while configuring " + deviceName);
close();
});
};
const confirmRemoveDevice = () => {
setIsRemoveDeviceDialogOpen(true);
};
const closeConfirmRemoveDeviceDialog = () => {
setIsRemoveDeviceDialogOpen(false);
};
const removeDevice = () => {
const deviceName = deviceForm.name();
deviceAPI
.remove(device.id())
.then((_response: any) => {
success(deviceName + " was successfully deleted!");
navigate("/");
})
.catch((_err: any) => {
error("Error occured while deleting " + deviceName + ".");
closeConfirmRemoveDeviceDialog();
close();
});
};
const canRemove = (): boolean => {
return user.allowedTo("remove-devices");
};
const title = () => {
return (
<React.Fragment>
Device Settings
<Typography variant="body2" color="textSecondary">
{deviceForm.name()} - ID: {deviceForm.id()}
</Typography>
</React.Fragment>
);
};
const content = () => {
return (
<React.Fragment>
<TextField
id="name"
name="name"
label="Name"
defaultValue={deviceForm.settings.name}
onChange={changeName}
margin="normal"
variant="outlined"
type="text"
fullWidth
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
/>
<TextField
id="description"
name="description"
label="Description"
defaultValue={deviceForm.settings.description}
onChange={changeDescription}
multiline
rows={2}
// rowsMax={4}
type="text"
margin="normal"
variant="outlined"
fullWidth
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
/>
<Grid container direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
<Grid size={{ xs: 12, sm: 12 }}>
<PeriodSelect
id="pondCheckPeriod"
label="Check-in Period"
isError={!isCheckPeriodValid(deviceForm)}
helperText={
isCheckPeriodValid(deviceForm)
? "How often the device checks for updates"
: "Value must be between " + minCheckPeriodDescription() + " and 1 hour"
}
initialMs={deviceForm.settings.pondCheckPeriodS * 1000}
isDisabled={!canEdit}
onChange={changePondCheckPeriod}
units={["seconds", "minutes", "hours"]}
/>
</Grid>
{user.hasFeature("sleep") && (
<Grid container direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
<Grid container size={{ xs: 4, sm: 3 }} justifyContent="center">
<FormControlLabel
control={
<Switch
checked={sleeps}
onChange={toggleSleeps}
name="sleeps"
aria-label="sleeps"
color="secondary"
/>
}
label={<Typography variant="caption">Sleeps</Typography>}
labelPlacement="top"
disabled={!canEdit}
/>
</Grid>
<Grid size={{ xs: 8, sm: 9 }}>
<FormControl fullWidth>
<PeriodSelect
id="sleepDuration"
label="Sleep Duration"
isError={!isSleepDurationValid(deviceForm)}
helperText={
!isSleepDurationValid(deviceForm) ? "Must be at least 10 seconds" : undefined
}
isDisabled={!canEdit || !sleeps}
initialMs={deviceForm.settings.sleepDurationS * 1000}
onChange={changeSleepDuration}
units={["seconds", "minutes", "hours"]}
/>
</FormControl>
<FormControl fullWidth>
<PeriodSelect
id="awakeDuration"
label="Awake Duration"
isError={!isSleepDurationValid(deviceForm)}
helperText={
!isSleepDurationValid(deviceForm) ? "Must be at least 1 second" : undefined
}
isDisabled={!canEdit || !sleeps}
initialMs={deviceForm.settings.sleepDelayMs}
onChange={changeSleepDelay}
units={["seconds", "minutes", "hours"]}
/>
</FormControl>
</Grid>
</Grid>
)}
<Grid size={{ xs: 12, sm: 12 }}>
<TextField
id="upgradeChannel"
label="Upgrade Channel"
select
value={deviceForm.settings.upgradeChannel}
onChange={changeUpgradeChannel}
helperText={upgradeChannelHelper()}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE}>Stable</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_BETA}>Beta</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA}>Alpha</MenuItem>
{user.hasFeature("dev-channel") && (
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT}>
Development
</MenuItem>
)}
{user.hasFeature("dev-channel") && (
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_RECOVERY}>Recovery</MenuItem>
)}
</TextField>
</Grid>
{user.hasFeature("admin") && (
<TextField
id="deviceProduct"
label="Device Product"
select
value={deviceForm.settings.product}
onChange={event => changeProduct(event)}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
{ListDeviceProductDescribers().map(describer => (
<MenuItem key={describer.product} value={describer.product}>
{describer.label}
</MenuItem>
))}
</TextField>
)}
{IsExtended(deviceForm.settings.product) && components && (
<Grid size={{ xs: 12 }}>
Select components to display on card
<TextField
id="component1"
label="Component 1"
select
value={compExtOne}
onChange={event => setCompExtOne(event.target.value)}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
{components.map(comp => (
<MenuItem key={comp.key()} value={comp.locationString()}>
{comp.name()}
</MenuItem>
))}
</TextField>
<TextField
id="component1"
label="Component 2"
select
value={compExtTwo}
onChange={event => setCompExtTwo(event.target.value)}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
{components.map(comp => (
<MenuItem key={comp.key()} value={comp.locationString()}>
{comp.name()}
</MenuItem>
))}
</TextField>
<TextField
id="component1"
label="Component 3"
select
value={compExtThree}
onChange={event => setCompExtThree(event.target.value)}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
{components.map(comp => (
<MenuItem key={comp.key()} value={comp.locationString()}>
{comp.name()}
</MenuItem>
))}
</TextField>
</Grid>
)}
{user.hasAdmin() && (
<Grid size={{ xs: 12, sm: 12 }}>
<FormControlLabel
control={
<Switch
checked={deviceForm.settings.automaticallyUpgrade}
onChange={toggleAutomaticallyUpgrade}
name="automaticallyUpgrade"
aria-label="automaticallyUpgrade"
color="secondary"
/>
}
disabled={!canEdit}
label="Automatically Upgrade"
labelPlacement="end"
/>
</Grid>
)}
</Grid>
</React.Fragment>
);
};
const mutationsContent = () => {
let mutations: JSX.Element[] = [];
if (device.settings.mutations) {
device.settings.mutations.forEach((mut, i) => {
mutations.push(
<ListItem key={"mutation" + i}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid size={{ xs: 5 }}>
<Typography>{mut.mutationName}</Typography>
</Grid>
<Grid>
<Button
variant="contained"
style={{ backgroundColor: "red" }}
onClick={() => {
let lm = linearMutations;
lm.splice(i, 1);
setLinearMutations([...lm]);
}}>
Remove
</Button>
<Button
style={{ marginLeft: 10 }}
variant="contained"
color="primary"
onClick={() => {
setExistingMutation(mut);
setNewMutationDialog(true);
}}>
Edit
</Button>
</Grid>
</Grid>
</ListItem>
);
});
}
let mutationList: JSX.Element = <List>{mutations}</List>;
return (
<React.Fragment>
<Button
variant="contained"
color="primary"
onClick={() => {
setExistingMutation(undefined);
setNewMutationDialog(true);
}}>
Add New Mutation
</Button>
<List>{mutationList}</List>
</React.Fragment>
);
};
const actions = () => {
return (
<Grid container justifyContent="space-between" direction="row">
<Grid size={{ xs: 5 }}>
{canRemove() && <DeleteButton onClick={confirmRemoveDevice}>Delete</DeleteButton>}
</Grid>
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
<Button onClick={close} color="primary">
Cancel
</Button>
<Button onClick={submitSettings} color="primary" disabled={!isFormValid() || !canEdit}>
Submit
</Button>
</Grid>
</Grid>
);
};
return (
<React.Fragment>
<ResponsiveDialog
maxWidth="sm"
fullWidth
open={isDialogOpen}
onClose={close}
aria-labelledby="device-config-title">
<DialogTitle id="device-config-title">{title()}</DialogTitle>
<DialogContent className={classes.dialogContent}>
{components ? (
<React.Fragment>
<Tabs
value={currentTab}
onChange={(_, value) => setCurrentTab(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="device tabs"
classes={{ root: classes.tabSmall }}>
<Tab label={"General"} className={classes.tabSmall} />
{user.hasFeature("beta") && (
<Tab label={"Mutations"} className={classes.tabSmall} />
)}
</Tabs>
<TabPanelMine value={currentTab} index={0}>
{content()}
</TabPanelMine>
<TabPanelMine value={currentTab} index={1}>
{mutationsContent()}
</TabPanelMine>
</React.Fragment>
) : (
content()
)}
</DialogContent>
<DialogActions>{actions()}</DialogActions>
</ResponsiveDialog>
<Dialog
open={isRemoveDeviceDialogOpen}
onClose={closeConfirmRemoveDeviceDialog}
aria-labelledby="confirm-remove-device-label"
aria-describedby="confirm-remove-device-description"
className={classes.removeDeviceDialog}>
<DialogTitle id="confirm-remove-device-title">Delete {deviceForm.name()}?</DialogTitle>
<DialogContent>
<Typography variant="subtitle1" color="error" align="left">
WARNING:
</Typography>
<Typography variant="subtitle1" color="textSecondary" align="left">
Clicking 'Accept' will remove the device from all users and groups associated with it.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={closeConfirmRemoveDeviceDialog} color="primary">
Cancel
</Button>
<Button onClick={removeDevice} color="primary">
Accept
</Button>
</DialogActions>
</Dialog>
<LinearMutationBuilder
existingMutation={existingMutation}
componentsByDevice={componentsByDevice}
open={newMutationDialog}
onClose={() => {
setNewMutationDialog(false);
}}
onSubmit={newMutation => {
let lm = linearMutations;
lm.push(newMutation);
setLinearMutations(lm);
}}
/>
</React.Fragment>
);
}

View file

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