345 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|