frontend/src/interactions/InteractionsOverview.tsx

345 lines
12 KiB
TypeScript

import {
Button,
Card,
CardActions,
CardContent,
CardHeader,
darken,
Grid2 as Grid,
IconButton,
Slider,
Theme,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography
} from "@mui/material";
import { Settings } from "@mui/icons-material";
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { Component, Device, Interaction } from "models";
import moment from "moment";
import { componentIDToString } from "pbHelpers/Component";
import {
abbreviatedWeekdays,
friendlyTimeOfDayDescriptor,
interactionConditionText,
interactionResultText,
isSource,
timeOfDayDescriptor
} from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { canWrite } from "pbHelpers/Permission";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
import InteractionSettings from "./InteractionSettings";
import { makeStyles } from "@mui/styles";
import { useGlobalState } from "providers";
const useStyles = makeStyles((theme: Theme) => {
return ({
card: {
backgroundColor: darken(theme.palette.background.paper, 0.05)
},
header: {
padding: theme.spacing(1),
paddingBottom: 0
},
content: {
padding: `${theme.spacing(1)}px !important`
},
actions: {
marginTop: "auto",
display: "flex",
flexDirection: "row",
justifyContent: "flex-end",
padding: theme.spacing(1),
paddingTop: 0
}
})
});
interface Props {
device: Device;
component: Component;
components: Component[];
interactions: Interaction[];
permissions: pond.Permission[];
refreshCallback: () => void;
}
export default function InteractionsOverview(props: Props) {
const classes = useStyles();
const [{as, user}] = useGlobalState();
const interactionsAPI = useInteractionsAPI();
const { success, error } = useSnackbar();
const { device, component, components, permissions, refreshCallback } = props;
const prevComponents = usePrevious(components);
const prevInteractions = usePrevious(props.interactions);
const [interactions, setInteractions] = useState<Interaction[]>(props.interactions);
const [dirtyInteractions, setDirtyInteractions] = useState<Map<number, boolean>>(new Map());
const [mappedComponents, setMappedComponents] = useState<Map<string, Component>>(new Map());
const [renderToggle, setRenderToggle] = useState(false);
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | undefined>(
undefined
);
useEffect(() => {
if (components !== prevComponents) {
let initMappedComponents: Map<string, Component> = new Map();
components.forEach((component: Component) => {
initMappedComponents.set(component.locationString(), component);
});
setMappedComponents(initMappedComponents);
}
if (props.interactions !== prevInteractions) {
setInteractions(cloneDeep(props.interactions));
}
}, [components, prevComponents, props.interactions, prevInteractions]);
const openInteractionSettings = (interaction: Interaction) => {
setSelectedInteraction(interaction);
};
const closeInteractionSettings = () => {
setSelectedInteraction(undefined);
};
const interactionConditions = (
source: Component | undefined,
conditions: pond.IInteractionCondition[] | null | undefined,
interactionIndex: number
) => {
let sourceType = source && source.settings.type;
let sourceSubtype = source && source.settings.subtype;
let items: any[] = [];
if (!conditions) return items;
conditions.forEach((condition: pond.IInteractionCondition, conditionIndex: number) => {
let measurementType = condition.measurementType;
let measurement = describeMeasurement(condition.measurementType, sourceType, sourceSubtype, undefined, user);
items.push(
<Grid container size={{ sm: 12 }} key={conditionIndex} alignItems="center">
<Grid size={{ xs: 12, sm: 6 }}>
<Typography variant="caption" gutterBottom>
{interactionConditionText(source, condition, conditionIndex > 0, user)}
</Typography>
</Grid>
<Grid size={{ xs: 12, sm: 6 }}>
{measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN ? (
<ToggleButtonGroup
value={measurement.toDisplay(condition.value ? condition.value : 0)}
exclusive
size="small"
onChange={(_, value) =>
updateConditionValue(
source,
interactionIndex,
conditionIndex,
parseInt(value) === 1 ? 1 : 0
)
}>
<ToggleButton value={0} size="small">
{measurement.convertWithUnits(0)}
</ToggleButton>
<ToggleButton value={1} size="small">
{measurement.convertWithUnits(1)}
</ToggleButton>
</ToggleButtonGroup>
) : (
<Slider
disabled={!canWrite(permissions)}
valueLabelDisplay="auto"
color="secondary"
min={measurement.min()}
max={measurement.max()}
step={measurement.step()}
track={false}
value={measurement.toDisplay(condition.value ? condition.value : 0)}
onChange={(_event, value) =>
updateConditionValue(source, interactionIndex, conditionIndex, value)
}
/>
)}
</Grid>
</Grid>
);
});
return items;
};
const updateConditionValue = (
source: Component | undefined,
interactionIndex: number,
conditionIndex: number,
value: number | number[]
) => {
if (interactions.length > interactionIndex) {
let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
let updatedInteractions = cloneDeep(interactions);
let interaction = updatedInteractions[interactionIndex];
let conditions = interaction.settings.conditions;
if (conditions.length > conditionIndex) {
if (typeof value === "number") {
let condition = conditions[conditionIndex];
let sourceType = source && source.settings.type;
let sourceSubtype = source && source.settings.subtype;
let describer = describeMeasurement(condition.measurementType, sourceType, sourceSubtype, undefined, user);
condition.value = describer.toStored(value);
updatedDirtyInteractions.set(interactionIndex, true);
}
setInteractions(updatedInteractions);
setDirtyInteractions(updatedDirtyInteractions);
setRenderToggle(!renderToggle);
}
}
};
const submitInteraction = (
deviceID: number,
index: number,
settings: pond.IInteractionSettings
) => {
interactionsAPI
.updateInteraction(Number(deviceID), settings, as)
.then((_response: any) => {
let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
updatedDirtyInteractions.set(index, false);
setDirtyInteractions(updatedDirtyInteractions);
success("Successfully updated the interaction for " + component.name());
refreshCallback();
})
.catch((_err: any) => {
error("Error occurred while updating the interaction for " + component.name());
});
};
const cancel = (index: number) => {
let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
updatedDirtyInteractions.set(index, false);
setDirtyInteractions(updatedDirtyInteractions);
setInteractions(cloneDeep(props.interactions));
};
const interactionItem = (interaction: Interaction, index: number) => {
let key = interaction.key();
let source = mappedComponents.get(componentIDToString(interaction.settings.source));
let sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
let statusText =
!interaction.status.synced && interaction.status.lastUpdate
? "Pending " +
moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow()
: "";
let schedule = pond.InteractionSchedule.create(
interaction.settings.schedule !== null ? interaction.settings.schedule : undefined
);
let timezone = schedule && schedule.timezone ? schedule.timezone : moment.tz.guess();
let todStart = (schedule.timeOfDayStart ? schedule.timeOfDayStart : "00:00").split(":");
let todEnd = (schedule.timeOfDayEnd ? schedule.timeOfDayEnd : "23:59").split(":");
let start = moment
.tz(timezone)
.startOf("day")
.add(todStart[0], "hours")
.add(todStart[1], "minutes")
.local();
let end = moment
.tz(timezone)
.startOf("day")
.add(todEnd[0], "hours")
.add(todEnd[1], "minutes")
.local();
let descriptor: "allDay" | "before" | "after" | "from" = timeOfDayDescriptor(schedule);
let showStart = descriptor === "after" || descriptor === "from";
let showEnd = descriptor === "before" || descriptor === "from";
let showBoth = showStart && showEnd;
let everyDay = schedule.weekdays && schedule.weekdays.length === 7;
let action = <React.Fragment />;
if (canWrite(permissions)) {
action = (
<Tooltip title="Settings">
<IconButton onClick={() => openInteractionSettings(interaction)}>
<Settings />
</IconButton>
</Tooltip>
);
}
return (
<Grid size={{ xs: 12 }} key={key}>
<Card elevation={0} className={classes.card}>
<CardHeader
className={classes.header}
titleTypographyProps={{ variant: "subtitle2" }}
title={interactionResultText(interaction, sink)}
subheader={statusText}
subheaderTypographyProps={{ variant: "caption" }}
action={action}
/>
<CardContent className={classes.content}>
{interaction.settings.conditions && (
<Grid container>
{interactionConditions(source, interaction.settings.conditions, index)}
</Grid>
)}
{(descriptor !== "allDay" || !everyDay) && (
<Grid container justifyContent="center">
<Typography variant="caption" color="textSecondary">
{!everyDay ? abbreviatedWeekdays(schedule) : "Operational"}{" "}
{descriptor !== "allDay" && friendlyTimeOfDayDescriptor(descriptor)}{" "}
{showStart && start.format("h:mma")} {showBoth && "-"}{" "}
{showEnd && end.format("h:mma")}
</Typography>
</Grid>
)}
</CardContent>
{dirtyInteractions.get(index) && (
<CardActions className={classes.actions}>
<Button size="small" color="primary" onClick={() => cancel(index)}>
Cancel
</Button>
<Button
size="small"
color="primary"
onClick={() => submitInteraction(device.id(), index, interaction.settings)}>
Submit
</Button>
</CardActions>
)}
</Card>
</Grid>
);
};
const interactionList = (component: Component, interactions: Interaction[]) => {
let items: any = [];
interactions.forEach((interaction: Interaction, index: number) => {
if (isSource(component.location(), interaction)) {
items.push(interactionItem(interaction, index));
}
});
if (items.length <= 0) {
return null;
}
return items;
};
return (
<Grid container spacing={1}>
{interactionList(component, interactions)}
<InteractionSettings
device={device}
components={components}
initialComponent={component}
initialInteraction={selectedInteraction}
mode="update"
isDialogOpen={selectedInteraction !== undefined}
closeDialogCallback={() => closeInteractionSettings()}
refreshCallback={refreshCallback}
canEdit={canWrite(permissions)}
/>
</Grid>
);
}