414 lines
13 KiB
TypeScript
414 lines
13 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
Card,
|
|
CardActionArea,
|
|
CardContent,
|
|
CardHeader,
|
|
//Grid2 as Grid,
|
|
Switch,
|
|
Theme,
|
|
Tooltip,
|
|
Typography
|
|
} from "@mui/material";
|
|
import Grid from '@mui/material/Grid2';
|
|
import EventBlocker from "common/EventBlocker";
|
|
import PendingChangesIndicator from "common/PendingChangesIndicator";
|
|
import MeasurementSummary from "component/MeasurementSummary";
|
|
import { useComponentAPI, usePendingChanges, useSnackbar, useThemeType } from "hooks";
|
|
import InteractionsOverview from "interactions/InteractionsOverview";
|
|
import { cloneDeep } from "lodash";
|
|
import { Component, Device, Interaction } from "models";
|
|
import moment from "moment";
|
|
import { getFriendlyAddressTypeName, getHumanReadableAddress } from "pbHelpers/AddressType";
|
|
import { controllerModeLabel } from "pbHelpers/Component";
|
|
import {
|
|
//extension,
|
|
GetComponentIcon,
|
|
getMeasurements,
|
|
isController
|
|
} from "pbHelpers/ComponentType";
|
|
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
|
|
import { findInteractionsAsSource, HasInteraction } from "pbHelpers/Interaction";
|
|
import { canWrite } from "pbHelpers/Permission";
|
|
import { pond, quack } from "protobuf-ts/pond";
|
|
import React, { useEffect, useState } from "react";
|
|
// import { useHistory } from "react-router";
|
|
import { hasDeviceFeature } from "services/feature/service";
|
|
import ComponentActions from "./ComponentActions";
|
|
import { or } from "utils";
|
|
import { extractNodes } from "pbHelpers/ComponentTypes";
|
|
import UnitMeasurementSummary from "./UnitMeasurementSummary";
|
|
import { UnitMeasurement } from "models/UnitMeasurement";
|
|
import { useGlobalState } from "providers";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
return ({
|
|
card: {
|
|
position: "relative",
|
|
display: "flex",
|
|
height: "100%",
|
|
flexDirection: "column",
|
|
overflow: "visible"
|
|
},
|
|
cardHeader: {
|
|
padding: theme.spacing(1),
|
|
paddingLeft: theme.spacing(2)
|
|
},
|
|
cardContent: {
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
padding: theme.spacing(1),
|
|
paddingTop: 0
|
|
},
|
|
moreDetails: {
|
|
marginLeft: "auto"
|
|
},
|
|
avatarIcon: {
|
|
width: theme.spacing(3),
|
|
height: theme.spacing(3)
|
|
},
|
|
sensorDot: {
|
|
position: "absolute",
|
|
marginLeft: theme.spacing(1),
|
|
marginTop: theme.spacing(1)
|
|
}
|
|
})
|
|
});
|
|
|
|
interface Sensor {
|
|
label: string;
|
|
color: string;
|
|
value: string;
|
|
}
|
|
|
|
interface Props {
|
|
device: Device;
|
|
component: Component;
|
|
components: Component[];
|
|
availablePositions: DeviceAvailabilityMap;
|
|
availableOffsets: OffsetAvailabilityMap;
|
|
permissions: pond.Permission[];
|
|
interactions: Interaction[];
|
|
refreshCallback: (updatedComponent?: Component) => void;
|
|
showMobile?: boolean;
|
|
showSensors?: boolean;
|
|
deviceComponentPreferences?: pond.DeviceComponentPreferences;
|
|
}
|
|
|
|
export default function ComponentCard(props: Props) {
|
|
const componentAPI = useComponentAPI();
|
|
const classes = useStyles();
|
|
const themeType = useThemeType();
|
|
const { error, success } = useSnackbar();
|
|
const {
|
|
device,
|
|
component,
|
|
components,
|
|
availablePositions,
|
|
availableOffsets,
|
|
permissions,
|
|
refreshCallback,
|
|
interactions,
|
|
showMobile,
|
|
showSensors,
|
|
deviceComponentPreferences
|
|
} = props;
|
|
// const history = useHistory();
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const [sensors, setSensors] = useState<Sensor[]>([]);
|
|
const [{ user, showErrors, as }] = useGlobalState();
|
|
const { pending: pendingChanges, accepted: changesAccepted } = usePendingChanges(
|
|
component.status.synced
|
|
);
|
|
|
|
const updateControllerState = (checked: boolean) => {
|
|
let updatedComponent = cloneDeep(component);
|
|
let newMode =
|
|
checked === false ? 2 : HasInteraction(component.location(), interactions) ? 0 : 1;
|
|
updatedComponent.settings.defaultOutputState = newMode;
|
|
let describe = controllerModeLabel(newMode);
|
|
|
|
componentAPI
|
|
.update(device.id(), updatedComponent.settings, undefined, undefined, as)
|
|
.then(() => {
|
|
success(component.name() + "'s mode was set to " + describe);
|
|
updatedComponent.status.synced = false;
|
|
updatedComponent.status.lastUpdate = moment().toISOString();
|
|
refreshCallback(updatedComponent);
|
|
})
|
|
.catch(() => {
|
|
error("Failed to set " + component.name() + "'s mode");
|
|
});
|
|
};
|
|
|
|
const pathToComponent = () => {
|
|
let url = location.pathname + "/components/" + component.key();
|
|
url = url.replace("//", "/");
|
|
return url;
|
|
};
|
|
|
|
const openComponentPage = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
|
const { name, className } = event.target as HTMLButtonElement;
|
|
const blacklist = ["slider", "controllerSwitch"];
|
|
if (
|
|
blacklist.includes(name) ||
|
|
blacklist.find(
|
|
v =>
|
|
className &&
|
|
className
|
|
.toString()
|
|
.toLowerCase()
|
|
.includes(v)
|
|
) !== undefined
|
|
) {
|
|
event.preventDefault();
|
|
} else {
|
|
navigate(pathToComponent());
|
|
}
|
|
};
|
|
|
|
const getAddressDescription = () => {
|
|
if (
|
|
component.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY ||
|
|
(component.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&
|
|
component.settings.addressType <= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET100)
|
|
) {
|
|
let addressDescription = getHumanReadableAddress(
|
|
component.settings.addressType,
|
|
component.settings.type,
|
|
component.settings.address,
|
|
device.settings.product
|
|
);
|
|
if (addressDescription === "") {
|
|
addressDescription = "Internal";
|
|
}
|
|
let port = "Port: " + addressDescription;
|
|
let cableID = "";
|
|
if (
|
|
component.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&
|
|
component.settings.addressType <= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET100
|
|
) {
|
|
cableID = "Cable: " + (component.settings.addressType - 8);
|
|
}
|
|
return port + " " + cableID;
|
|
} else if (component.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C) {
|
|
let description = ""
|
|
let type = getFriendlyAddressTypeName(component.settings.addressType);
|
|
description = type
|
|
if(component.settings.expansionLine){
|
|
description = description + ": Exp Line " + component.settings.expansionLine
|
|
}
|
|
if(component.settings.muxLine){
|
|
description = description + " ,Mux Line " + component.settings.muxLine
|
|
}
|
|
return description
|
|
}else{
|
|
return getFriendlyAddressTypeName(component.settings.addressType);
|
|
}
|
|
};
|
|
|
|
const header = () => {
|
|
const canEdit = canWrite(permissions);
|
|
const componentIcon = GetComponentIcon(
|
|
component.settings.type,
|
|
component.settings.subtype,
|
|
themeType
|
|
);
|
|
const name = component.name();
|
|
|
|
let measurements: UnitMeasurement[] = [];
|
|
if (component.status.lastGoodMeasurement)
|
|
measurements = component.status.lastGoodMeasurement.map(um => UnitMeasurement.any(um, user));
|
|
|
|
if (showErrors)
|
|
measurements = component.lastMeasurement.map(um => UnitMeasurement.any(um, user));
|
|
|
|
return (
|
|
<CardHeader
|
|
avatar={
|
|
componentIcon ? (
|
|
<Avatar
|
|
variant="square"
|
|
src={componentIcon}
|
|
className={classes.avatarIcon}
|
|
alt={name}
|
|
/>
|
|
) : (
|
|
<Box className={classes.avatarIcon} />
|
|
)
|
|
}
|
|
title={
|
|
<React.Fragment>
|
|
<Tooltip
|
|
title={
|
|
<div>
|
|
Subtype: {component.subTypeName()}
|
|
<br />
|
|
ID: {component.key()}
|
|
<br />
|
|
Location: {component.locationString()}
|
|
</div>
|
|
}
|
|
placement="top">
|
|
<span>
|
|
{name} - <Typography variant="caption">{getAddressDescription()}</Typography>
|
|
</span>
|
|
</Tooltip>
|
|
{isController(component.settings.type) &&
|
|
hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && (
|
|
<Box component="span" paddingLeft={1}>
|
|
<Switch
|
|
checked={
|
|
component.settings.defaultOutputState === 0 ||
|
|
component.settings.defaultOutputState === 1
|
|
}
|
|
color="default"
|
|
size="small"
|
|
onTouchStart={event => {
|
|
event.stopPropagation();
|
|
//event.preventDefault();
|
|
}}
|
|
onMouseDown={event => {
|
|
event.stopPropagation();
|
|
//event.preventDefault();
|
|
}}
|
|
onClick={event => {
|
|
event.stopPropagation();
|
|
//event.preventDefault();
|
|
}}
|
|
disabled={!canEdit}
|
|
onChange={(_, checked) => updateControllerState(checked)}
|
|
name="controllerSwitch"
|
|
inputProps={{ "aria-label": "controller switch" }}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</React.Fragment>
|
|
}
|
|
className={classes.cardHeader}
|
|
titleTypographyProps={{ variant: "subtitle1" }}
|
|
subheader={
|
|
<React.Fragment>
|
|
{(pendingChanges || changesAccepted) && (
|
|
<Typography variant="caption" color="textSecondary" component="div">
|
|
<PendingChangesIndicator
|
|
pending={pendingChanges}
|
|
accepted={changesAccepted}
|
|
text={
|
|
changesAccepted
|
|
? "Pending change accepted"
|
|
: "Pending " + moment(component.status.lastUpdate).fromNow()
|
|
}
|
|
/>
|
|
</Typography>
|
|
)}
|
|
{/* !newStructure ? (
|
|
<MeasurementSummary
|
|
component={component}
|
|
reading={component.status.lastMeasurement}
|
|
dense
|
|
/>
|
|
) : ( */}
|
|
<UnitMeasurementSummary
|
|
component={component}
|
|
reading={UnitMeasurement.convertLastMeasurement(measurements)}
|
|
//dense
|
|
/>
|
|
{/* ) */}
|
|
</React.Fragment>
|
|
}
|
|
action={
|
|
<EventBlocker>
|
|
<ComponentActions
|
|
device={device}
|
|
component={component}
|
|
components={components}
|
|
availablePositions={availablePositions}
|
|
availableOffsets={availableOffsets}
|
|
refreshCallback={refreshCallback}
|
|
permissions={permissions}
|
|
deviceComponentPreferences={deviceComponentPreferences}
|
|
/>
|
|
</EventBlocker>
|
|
}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const content = () => {
|
|
if (findInteractionsAsSource(component.location(), interactions).length <= 0) return null;
|
|
return (
|
|
<CardContent className={classes.cardContent}>
|
|
<EventBlocker>
|
|
<InteractionsOverview
|
|
device={device}
|
|
component={component}
|
|
components={components}
|
|
interactions={interactions}
|
|
permissions={permissions}
|
|
refreshCallback={() => refreshCallback()}
|
|
/>
|
|
</EventBlocker>
|
|
</CardContent>
|
|
);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!component) return;
|
|
if (!component.status) return;
|
|
if (!component.status.lastMeasurement) return;
|
|
let measurements = getMeasurements(component.type(), or(component.settings.subtype, 0));
|
|
let nodes = extractNodes(component.status!.lastMeasurement!.measurement!);
|
|
let sensors: Sensor[] = [];
|
|
nodes.forEach(node => {
|
|
Object.values(node).forEach((value, index) => {
|
|
if (measurements[index]) {
|
|
let sensor: Sensor = {
|
|
label: measurements[index].label,
|
|
value: value,
|
|
color: measurements[index].colour
|
|
};
|
|
sensors.push(sensor);
|
|
}
|
|
});
|
|
});
|
|
setSensors(sensors);
|
|
}, [component, setSensors]);
|
|
|
|
const sensorCard = () => {
|
|
if (sensors.length <= 0 || !showSensors) return null;
|
|
return (
|
|
<CardContent className={classes.cardContent}>
|
|
<EventBlocker>
|
|
{sensors.map((sensor, index) => {
|
|
return (
|
|
<Typography variant="body2" key={index}>
|
|
{sensor.label}: {sensor.value}
|
|
</Typography>
|
|
);
|
|
})}
|
|
</EventBlocker>
|
|
</CardContent>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card raised className={classes.card}>
|
|
<CardActionArea
|
|
onClick={(event: any) => openComponentPage(event)}
|
|
component="div"
|
|
style={{ height: "100%" }}>
|
|
{header()}
|
|
{content()}
|
|
{sensorCard()}
|
|
</CardActionArea>
|
|
</Card>
|
|
|
|
);
|
|
}
|