added tons of files for component functionality; manual component adding added temporarily

This commit is contained in:
Carter 2025-02-11 16:31:03 -06:00
parent 58830d480e
commit 45ff49bcea
121 changed files with 25883 additions and 65 deletions

View file

@ -0,0 +1,390 @@
import {
Avatar,
Box,
Card,
CardActionArea,
CardContent,
CardHeader,
Grid2 as Grid,
Switch,
Theme,
Tooltip,
Typography
} from "@mui/material";
import EventBlocker from "common/EventBlocker";
import MeasurementSummary from "component/MeasurementSummary";
import { useComponentAPI, useSnackbar, useThemeType } from "hooks";
import InteractionsOverview from "interactions/InteractionsOverview";
import { cloneDeep } from "lodash";
import { Component, Device, Interaction } from "models";
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 }] = useGlobalState();
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)
.then(() => {
success(component.name() + "'s mode was set to " + describe);
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 {
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={
// !newStructure ? (
// <MeasurementSummary
// component={component}
// reading={component.status.lastMeasurement}
// dense
// />
// ) : (
<UnitMeasurementSummary
component={component}
excludedNodes={deviceComponentPreferences?.excludedNodes}
reading={UnitMeasurement.convertLastMeasurement(measurements)}
//dense
/>
// )
}
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 (
<Grid
size={{
xs: 12,
sm: showMobile ? 12 : 6,
md: showMobile ? 12 : 6,
lg: showMobile ? 12 : 4,
xl: showMobile ? 12 : 3
}}
>
<Card raised className={classes.card}>
<CardActionArea
onClick={(event: any) => openComponentPage(event)}
component="div"
style={{ height: "100%" }}>
{header()}
{content()}
{sensorCard()}
</CardActionArea>
</Card>
</Grid>
);
}