merged pending changes branch

This commit is contained in:
Carter 2026-06-24 13:14:42 -06:00
commit e1a4443a38
3 changed files with 82 additions and 9 deletions

View file

@ -4,6 +4,7 @@ import {
CardActions,
CardContent,
CardHeader,
CircularProgress,
darken,
Grid2 as Grid,
IconButton,
@ -14,7 +15,7 @@ import {
Tooltip,
Typography
} from "@mui/material";
import { Settings } from "@mui/icons-material";
import { CheckCircleOutline, Settings } from "@mui/icons-material";
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { Component, Device, Interaction } from "models";
@ -80,6 +81,7 @@ export default function InteractionsOverview(props: Props) {
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 [acceptedInteractions, setAcceptedInteractions] = useState<Set<string>>(new Set());
const [renderToggle, setRenderToggle] = useState(false);
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | undefined>(
undefined
@ -95,6 +97,20 @@ export default function InteractionsOverview(props: Props) {
}
if (props.interactions !== prevInteractions) {
if (prevInteractions) {
setAcceptedInteractions((prevAccepted) => {
const updatedAccepted = new Set(prevAccepted);
props.interactions.forEach((interaction: Interaction) => {
const previous = prevInteractions.find(prevInteraction => prevInteraction.key() === interaction.key());
if (!interaction.status.synced) {
updatedAccepted.delete(interaction.key());
} else if (previous && !previous.status.synced) {
updatedAccepted.add(interaction.key());
}
});
return updatedAccepted;
});
}
setInteractions(cloneDeep(props.interactions));
}
}, [components, prevComponents, props.interactions, prevInteractions]);
@ -226,10 +242,16 @@ export default function InteractionsOverview(props: Props) {
let key = interaction.key();
let source = mappedComponents.get(componentIDToString(interaction.settings.source));
let sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
const isPending = !interaction.status.synced && Boolean(interaction.status.lastUpdate);
const isAccepted = interaction.status.synced && acceptedInteractions.has(interaction.key());
let statusText =
!interaction.status.synced && interaction.status.lastUpdate
!interaction.status.synced
? interaction.status.lastUpdate
? "Pending " +
moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow()
: ""
: acceptedInteractions.has(interaction.key())
? "Pending change accepted"
: "";
let schedule = pond.InteractionSchedule.create(
interaction.settings.schedule !== null ? interaction.settings.schedule : undefined
@ -272,7 +294,20 @@ export default function InteractionsOverview(props: Props) {
className={classes.header}
titleTypographyProps={{ variant: "subtitle2" }}
title={interactionResultText(interaction, sink)}
subheader={statusText}
subheader={
statusText ? (
<Grid container spacing={0.5} alignItems="center">
{isPending && <CircularProgress size={10} thickness={5} color="inherit" />}
{isAccepted && (
<CheckCircleOutline
sx={{ fontSize: 13, color: "var(--status-ok)" }}
/>
)}
<Grid>
{statusText}
</Grid>
</Grid>
) : ""}
subheaderTypographyProps={{ variant: "caption" }}
action={action}
/>

View file

@ -282,6 +282,45 @@ export default function DevicePage() {
enabled: !!deviceID,
});
// --- Real-time interaction updates ---
// Streams interaction changes for this device, including status changes
// when the device accepts pending interaction updates.
useWebSocket<{ key: string; interaction: Interaction } | null>({
path: `/v1/live/devices/${deviceID}/interactions`,
parse: (e) => {
try {
const raw = JSON.parse(e.data);
const interaction = Interaction.any(raw);
if (!interaction?.settings) {
console.warn("[ws] received interaction without settings:", raw);
return null;
}
return { key: interaction.key(), interaction };
} catch (err) {
console.warn("[ws] failed to parse interaction:", err);
return null;
}
},
onMessage: (data) => {
if (!data) return;
const { key, interaction } = data;
setInteractions((prev) => {
const index = prev.findIndex((existing) => existing.key() === key);
if (index < 0) {
return [...prev, interaction];
}
const updated = [...prev];
updated[index] = interaction;
return updated;
});
},
keys: liveContextKeys,
types: liveContextTypes,
as: liveAs,
token,
enabled: !!deviceID,
});
const loadPortScan = () => {
deviceAPI.listFoundComponents(parseInt(deviceID))
.then(resp => {
@ -395,7 +434,7 @@ export default function DevicePage() {
interactions={filteredInteractions}
permissions={permissions}
deviceComponentPreferences={prefsMap.get(c.key())}
key={i}
key={c.key()}
refreshCallback={(updatedComponent?: Component) =>
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
}
@ -412,7 +451,7 @@ export default function DevicePage() {
interactions={filteredInteractions}
permissions={permissions}
deviceComponentPreferences={prefsMap.get(c.key())}
key={i}
key={c.key()}
refreshCallback={(updatedComponent?: Component) =>
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
}

View file

@ -174,10 +174,9 @@ export class MeasurementDescriber {
this.details.path = "power.inputVoltageTimes10";
this.details.decimals = 1;
}
if (componentType === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) {
if (componentType === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE || componentType === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE) {
this.details.unit = "mV";
this.details.graph = GraphType.MULTILINE;
this.details.unit = "mV";
this.details.decimals = 0
// this.details.nodeDetails = {
// colours: ["white", "black", "red", "blue"],