added tons of files for component functionality; manual component adding added temporarily
This commit is contained in:
parent
58830d480e
commit
45ff49bcea
121 changed files with 25883 additions and 65 deletions
390
src/component/ComponentCard.tsx
Normal file
390
src/component/ComponentCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue