opening interaction websocket to detect accepted pending changes
This commit is contained in:
parent
6b07859d00
commit
895ed8e50b
2 changed files with 81 additions and 7 deletions
|
|
@ -4,6 +4,7 @@ import {
|
||||||
CardActions,
|
CardActions,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
CircularProgress,
|
||||||
darken,
|
darken,
|
||||||
Grid2 as Grid,
|
Grid2 as Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
|
@ -14,7 +15,7 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Settings } from "@mui/icons-material";
|
import { CheckCircleOutline, Settings } from "@mui/icons-material";
|
||||||
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
|
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { Component, Device, Interaction } from "models";
|
import { Component, Device, Interaction } from "models";
|
||||||
|
|
@ -80,6 +81,7 @@ export default function InteractionsOverview(props: Props) {
|
||||||
const [interactions, setInteractions] = useState<Interaction[]>(props.interactions);
|
const [interactions, setInteractions] = useState<Interaction[]>(props.interactions);
|
||||||
const [dirtyInteractions, setDirtyInteractions] = useState<Map<number, boolean>>(new Map());
|
const [dirtyInteractions, setDirtyInteractions] = useState<Map<number, boolean>>(new Map());
|
||||||
const [mappedComponents, setMappedComponents] = useState<Map<string, Component>>(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 [renderToggle, setRenderToggle] = useState(false);
|
||||||
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | undefined>(
|
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | undefined>(
|
||||||
undefined
|
undefined
|
||||||
|
|
@ -95,6 +97,20 @@ export default function InteractionsOverview(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.interactions !== prevInteractions) {
|
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));
|
setInteractions(cloneDeep(props.interactions));
|
||||||
}
|
}
|
||||||
}, [components, prevComponents, props.interactions, prevInteractions]);
|
}, [components, prevComponents, props.interactions, prevInteractions]);
|
||||||
|
|
@ -226,10 +242,16 @@ export default function InteractionsOverview(props: Props) {
|
||||||
let key = interaction.key();
|
let key = interaction.key();
|
||||||
let source = mappedComponents.get(componentIDToString(interaction.settings.source));
|
let source = mappedComponents.get(componentIDToString(interaction.settings.source));
|
||||||
let sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
|
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 =
|
let statusText =
|
||||||
!interaction.status.synced && interaction.status.lastUpdate
|
!interaction.status.synced
|
||||||
? "Pending " +
|
? interaction.status.lastUpdate
|
||||||
moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow()
|
? "Pending " +
|
||||||
|
moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow()
|
||||||
|
: ""
|
||||||
|
: acceptedInteractions.has(interaction.key())
|
||||||
|
? "Pending change accepted"
|
||||||
: "";
|
: "";
|
||||||
let schedule = pond.InteractionSchedule.create(
|
let schedule = pond.InteractionSchedule.create(
|
||||||
interaction.settings.schedule !== null ? interaction.settings.schedule : undefined
|
interaction.settings.schedule !== null ? interaction.settings.schedule : undefined
|
||||||
|
|
@ -272,7 +294,20 @@ export default function InteractionsOverview(props: Props) {
|
||||||
className={classes.header}
|
className={classes.header}
|
||||||
titleTypographyProps={{ variant: "subtitle2" }}
|
titleTypographyProps={{ variant: "subtitle2" }}
|
||||||
title={interactionResultText(interaction, sink)}
|
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" }}
|
subheaderTypographyProps={{ variant: "caption" }}
|
||||||
action={action}
|
action={action}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,45 @@ export default function DevicePage() {
|
||||||
enabled: !!deviceID,
|
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 = () => {
|
const loadPortScan = () => {
|
||||||
deviceAPI.listFoundComponents(parseInt(deviceID))
|
deviceAPI.listFoundComponents(parseInt(deviceID))
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
|
|
@ -395,7 +434,7 @@ export default function DevicePage() {
|
||||||
interactions={filteredInteractions}
|
interactions={filteredInteractions}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
deviceComponentPreferences={prefsMap.get(c.key())}
|
deviceComponentPreferences={prefsMap.get(c.key())}
|
||||||
key={i}
|
key={c.key()}
|
||||||
refreshCallback={(updatedComponent?: Component) =>
|
refreshCallback={(updatedComponent?: Component) =>
|
||||||
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
||||||
}
|
}
|
||||||
|
|
@ -412,7 +451,7 @@ export default function DevicePage() {
|
||||||
interactions={filteredInteractions}
|
interactions={filteredInteractions}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
deviceComponentPreferences={prefsMap.get(c.key())}
|
deviceComponentPreferences={prefsMap.get(c.key())}
|
||||||
key={i}
|
key={c.key()}
|
||||||
refreshCallback={(updatedComponent?: Component) =>
|
refreshCallback={(updatedComponent?: Component) =>
|
||||||
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue