@procaaso/alphinity-ui-components 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/index.cjs +173 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +215 -102
- package/dist/index.d.ts +215 -102
- package/dist/index.js +174 -174
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -72,6 +72,17 @@ function HMIPanel() {
|
|
|
72
72
|
- `npm test` - Run tests with Vitest
|
|
73
73
|
- `npm run lint` - Lint code with ESLint
|
|
74
74
|
- `npm run typecheck` - Type check with TypeScript
|
|
75
|
+
- `npm run dev` - Run the demo playground on http://localhost:3010
|
|
76
|
+
|
|
77
|
+
### Demo views
|
|
78
|
+
|
|
79
|
+
The demo (`npm run dev`) mounts one of three views, selected by a `?demo=` query param:
|
|
80
|
+
|
|
81
|
+
- `http://localhost:3010/` *(default)* - **Runtime**: full P&ID canvas with live telemetry, simulation, and device control/config panels
|
|
82
|
+
- `http://localhost:3010/?demo=single-node` - **Single-node HMI mockup** over the full plant SVG layout
|
|
83
|
+
- `http://localhost:3010/?demo=gallery` - **Component gallery** of individual widgets
|
|
84
|
+
|
|
85
|
+
> The demo is served, not opened as a file — `demo-dist/index.html` references assets by absolute path and will be blank if opened via `file://`. Use `npm run dev`, or `npx vite preview --config vite.demo.config.ts` for the built version.
|
|
75
86
|
|
|
76
87
|
### Components
|
|
77
88
|
|
package/dist/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
CardValue: () => CardValue,
|
|
37
37
|
CircularGauge: () => CircularGauge,
|
|
38
38
|
ControlPanel: () => ControlPanel,
|
|
39
|
+
DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
|
|
39
40
|
DEFAULT_THRESHOLDS: () => DEFAULT_THRESHOLDS,
|
|
40
41
|
DashboardCard: () => DashboardCard,
|
|
41
42
|
DeviceConfigPanel: () => DeviceConfigPanel,
|
|
@@ -85,6 +86,7 @@ __export(index_exports, {
|
|
|
85
86
|
overlayStyles: () => overlayStyles,
|
|
86
87
|
registerSymbol: () => registerSymbol,
|
|
87
88
|
registerSymbols: () => registerSymbols,
|
|
89
|
+
resolveStatusColor: () => resolveStatusColor,
|
|
88
90
|
useDeviceConfigs: () => useDeviceConfigs,
|
|
89
91
|
useDeviceControls: () => useDeviceControls,
|
|
90
92
|
useDrag: () => useDrag,
|
|
@@ -578,11 +580,12 @@ var ControlPanel = ({
|
|
|
578
580
|
secondSetpointValue,
|
|
579
581
|
onSecondSetpointChange,
|
|
580
582
|
modeConfigs = {},
|
|
581
|
-
defaultUnit
|
|
583
|
+
defaultUnit,
|
|
582
584
|
defaultLabel = "Setpoint",
|
|
583
|
-
defaultMin
|
|
584
|
-
defaultMax
|
|
585
|
-
defaultPrecision
|
|
585
|
+
defaultMin,
|
|
586
|
+
defaultMax,
|
|
587
|
+
defaultPrecision,
|
|
588
|
+
scaling,
|
|
586
589
|
status = "normal",
|
|
587
590
|
collapsed = false,
|
|
588
591
|
onCollapseChange,
|
|
@@ -604,11 +607,11 @@ var ControlPanel = ({
|
|
|
604
607
|
onCollapseChange?.(!newExpanded);
|
|
605
608
|
};
|
|
606
609
|
const currentModeConfig = modeConfigs[selectedMode] || {};
|
|
607
|
-
const currentUnit = currentModeConfig.unit || defaultUnit;
|
|
610
|
+
const currentUnit = currentModeConfig.unit || defaultUnit || scaling?.units || "Units";
|
|
608
611
|
const currentLabel = currentModeConfig.label || defaultLabel;
|
|
609
|
-
const currentMin = currentModeConfig.min ?? defaultMin;
|
|
610
|
-
const currentMax = currentModeConfig.max ?? defaultMax;
|
|
611
|
-
const currentPrecision = currentModeConfig.precision ?? defaultPrecision;
|
|
612
|
+
const currentMin = currentModeConfig.min ?? defaultMin ?? scaling?.euLow ?? 0;
|
|
613
|
+
const currentMax = currentModeConfig.max ?? defaultMax ?? scaling?.euHigh ?? 100;
|
|
614
|
+
const currentPrecision = currentModeConfig.precision ?? defaultPrecision ?? scaling?.decimals ?? 2;
|
|
612
615
|
const hasSecondSetpoint = currentModeConfig.hasSecondSetpoint || false;
|
|
613
616
|
const baseClass = "ui-control-panel";
|
|
614
617
|
const statusClass = status ? `ui-control-panel--${status}` : "";
|
|
@@ -996,6 +999,31 @@ var NodeControlsPanel = ({
|
|
|
996
999
|
|
|
997
1000
|
// src/components/DeviceControlPanel/DeviceControlPanel.tsx
|
|
998
1001
|
var import_react5 = require("react");
|
|
1002
|
+
|
|
1003
|
+
// src/theme/statusColors.ts
|
|
1004
|
+
var DEFAULT_STATUS_COLORS = {
|
|
1005
|
+
normal: { fill: "#888888", stroke: "#6b6b6b", bg: "#888888", text: "#ffffff", border: "#6b6b6b" },
|
|
1006
|
+
warning: { fill: "#f59e0b", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" },
|
|
1007
|
+
alarm: { fill: "#ef4444", stroke: "#dc2626", bg: "#FF0000", text: "#ffffff", border: "#dc2626" },
|
|
1008
|
+
fault: { fill: "#dc2626", stroke: "#b91c1c", bg: "#FF0000", text: "#ffffff", border: "#b91c1c" },
|
|
1009
|
+
off: { fill: "#edeeef", stroke: "#000000", bg: "#666666", text: "#ffffff", border: "#4b5563" },
|
|
1010
|
+
starting: { fill: "#90EE90", stroke: "#22c55e", bg: "#90EE90", text: "#000000", border: "#22c55e" },
|
|
1011
|
+
stopping: { fill: "#FFD700", stroke: "#d97706", bg: "#FFD700", text: "#000000", border: "#d97706" }
|
|
1012
|
+
};
|
|
1013
|
+
var STATUS_ALIASES = {
|
|
1014
|
+
running: "normal",
|
|
1015
|
+
stopped: "off"
|
|
1016
|
+
};
|
|
1017
|
+
function resolveStatusColor(status, customMap) {
|
|
1018
|
+
const raw = (status || "off").toLowerCase();
|
|
1019
|
+
const key = STATUS_ALIASES[raw] ?? raw;
|
|
1020
|
+
const defaults = DEFAULT_STATUS_COLORS[key] ?? DEFAULT_STATUS_COLORS.off;
|
|
1021
|
+
const overrides = customMap?.[key];
|
|
1022
|
+
if (!overrides) return defaults;
|
|
1023
|
+
return { ...defaults, ...overrides };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/components/DeviceControlPanel/DeviceControlPanel.tsx
|
|
999
1027
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1000
1028
|
function DeviceControlPanel({
|
|
1001
1029
|
binding,
|
|
@@ -1012,6 +1040,8 @@ function DeviceControlPanel({
|
|
|
1012
1040
|
onParameterChange,
|
|
1013
1041
|
onStart,
|
|
1014
1042
|
onStop,
|
|
1043
|
+
startPending = false,
|
|
1044
|
+
stopPending = false,
|
|
1015
1045
|
onCustomAction,
|
|
1016
1046
|
onOpenConfig,
|
|
1017
1047
|
showConfigButton = true,
|
|
@@ -1070,23 +1100,8 @@ function DeviceControlPanel({
|
|
|
1070
1100
|
const textColor = binding.theme?.textColor || "#000000";
|
|
1071
1101
|
const mutedTextColor = "#666666";
|
|
1072
1102
|
const selectedColor = "#b4d0fe";
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
// Gray (no alarm)
|
|
1076
|
-
warning: binding.theme?.statusColors?.warning || "#FFD700",
|
|
1077
|
-
// Yellow
|
|
1078
|
-
alarm: binding.theme?.statusColors?.alarm || "#FF0000",
|
|
1079
|
-
// Red
|
|
1080
|
-
fault: binding.theme?.statusColors?.fault || "#FF0000",
|
|
1081
|
-
// Red
|
|
1082
|
-
off: binding.theme?.statusColors?.off || "#666666",
|
|
1083
|
-
// Dark gray
|
|
1084
|
-
starting: binding.theme?.statusColors?.starting || "#90EE90",
|
|
1085
|
-
// Light green
|
|
1086
|
-
stopping: binding.theme?.statusColors?.stopping || "#FFD700"
|
|
1087
|
-
// Yellow
|
|
1088
|
-
};
|
|
1089
|
-
const statusColor = statusColors[binding.state.status || "normal"];
|
|
1103
|
+
const statusEntry = resolveStatusColor(binding.state.status, binding.theme?.statusColors);
|
|
1104
|
+
const statusColor = statusEntry.bg;
|
|
1090
1105
|
const handleModeChange = (mode) => {
|
|
1091
1106
|
onModeChange?.(mode);
|
|
1092
1107
|
};
|
|
@@ -1373,7 +1388,10 @@ function DeviceControlPanel({
|
|
|
1373
1388
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1374
1389
|
"div",
|
|
1375
1390
|
{
|
|
1376
|
-
onClick: () =>
|
|
1391
|
+
onClick: () => {
|
|
1392
|
+
setPendingMode(currentMode);
|
|
1393
|
+
setShowModeModal(true);
|
|
1394
|
+
},
|
|
1377
1395
|
style: {
|
|
1378
1396
|
padding: "12px",
|
|
1379
1397
|
backgroundColor: surfaceColor,
|
|
@@ -1770,11 +1788,10 @@ function DeviceControlPanel({
|
|
|
1770
1788
|
"button",
|
|
1771
1789
|
{
|
|
1772
1790
|
onClick: onStart,
|
|
1773
|
-
disabled: binding.state.isRunning,
|
|
1774
1791
|
style: {
|
|
1775
1792
|
flex: 1,
|
|
1776
1793
|
height: touchSize,
|
|
1777
|
-
backgroundColor:
|
|
1794
|
+
backgroundColor: "#10b981",
|
|
1778
1795
|
color: "#ffffff",
|
|
1779
1796
|
border: "none",
|
|
1780
1797
|
borderRadius: "6px",
|
|
@@ -1782,21 +1799,20 @@ function DeviceControlPanel({
|
|
|
1782
1799
|
fontWeight: 700,
|
|
1783
1800
|
fontFamily: "Arial, sans-serif",
|
|
1784
1801
|
letterSpacing: "0.5px",
|
|
1785
|
-
cursor:
|
|
1786
|
-
opacity:
|
|
1802
|
+
cursor: "pointer",
|
|
1803
|
+
opacity: 1
|
|
1787
1804
|
},
|
|
1788
|
-
children: "START"
|
|
1805
|
+
children: startPending ? "STARTING\u2026" : "START"
|
|
1789
1806
|
}
|
|
1790
1807
|
),
|
|
1791
1808
|
activeCapabilities?.canStop && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1792
1809
|
"button",
|
|
1793
1810
|
{
|
|
1794
1811
|
onClick: onStop,
|
|
1795
|
-
disabled: !binding.state.isRunning,
|
|
1796
1812
|
style: {
|
|
1797
1813
|
flex: 1,
|
|
1798
1814
|
height: touchSize,
|
|
1799
|
-
backgroundColor:
|
|
1815
|
+
backgroundColor: "#ef4444",
|
|
1800
1816
|
color: "#ffffff",
|
|
1801
1817
|
border: "none",
|
|
1802
1818
|
borderRadius: "6px",
|
|
@@ -1804,10 +1820,10 @@ function DeviceControlPanel({
|
|
|
1804
1820
|
fontWeight: 700,
|
|
1805
1821
|
fontFamily: "Arial, sans-serif",
|
|
1806
1822
|
letterSpacing: "0.5px",
|
|
1807
|
-
cursor:
|
|
1808
|
-
opacity:
|
|
1823
|
+
cursor: "pointer",
|
|
1824
|
+
opacity: 1
|
|
1809
1825
|
},
|
|
1810
|
-
children: "STOP"
|
|
1826
|
+
children: stopPending ? "STOPPING\u2026" : "STOP"
|
|
1811
1827
|
}
|
|
1812
1828
|
)
|
|
1813
1829
|
] }),
|
|
@@ -4113,7 +4129,8 @@ function NodeRenderer({
|
|
|
4113
4129
|
onNodeDragStart,
|
|
4114
4130
|
draggingNodeId,
|
|
4115
4131
|
dragOffset = { x: 0, y: 0 },
|
|
4116
|
-
enableDrag = false
|
|
4132
|
+
enableDrag = false,
|
|
4133
|
+
statusColorMap
|
|
4117
4134
|
}) {
|
|
4118
4135
|
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("g", { id: "layer-nodes", children: nodes2.filter((node) => node.visible !== false).map((node) => {
|
|
4119
4136
|
const symbol = getSymbolDefinition(node.symbolId);
|
|
@@ -4172,24 +4189,9 @@ function NodeRenderer({
|
|
|
4172
4189
|
`translate(${centerX}, ${centerY}) scale(${scaleX}, ${scaleY}) translate(${-centerX}, ${-centerY})`
|
|
4173
4190
|
);
|
|
4174
4191
|
}
|
|
4175
|
-
const
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
return "#22c55e";
|
|
4179
|
-
// Green
|
|
4180
|
-
case "alarm":
|
|
4181
|
-
return "#ef4444";
|
|
4182
|
-
// Red
|
|
4183
|
-
case "warning":
|
|
4184
|
-
return "#f59e0b";
|
|
4185
|
-
// Orange
|
|
4186
|
-
case "stopped":
|
|
4187
|
-
default:
|
|
4188
|
-
return "#edeeef";
|
|
4189
|
-
}
|
|
4190
|
-
};
|
|
4191
|
-
const fillColor = node.customColors?.fill ?? getStatusColor(node.status);
|
|
4192
|
-
const strokeColor = node.customColors?.stroke ?? "rgb(0, 0, 0)";
|
|
4192
|
+
const statusEntry = resolveStatusColor(node.status, statusColorMap);
|
|
4193
|
+
const fillColor = node.customColors?.fill ?? statusEntry.fill;
|
|
4194
|
+
const strokeColor = node.customColors?.stroke ?? statusEntry.stroke;
|
|
4193
4195
|
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
|
|
4194
4196
|
"g",
|
|
4195
4197
|
{
|
|
@@ -4453,14 +4455,16 @@ function DataOverlay({
|
|
|
4453
4455
|
x,
|
|
4454
4456
|
y,
|
|
4455
4457
|
telemetry,
|
|
4456
|
-
scale = 1
|
|
4458
|
+
scale = 1,
|
|
4459
|
+
statusColorMap
|
|
4457
4460
|
}) {
|
|
4458
|
-
const { value, display, history } = telemetry;
|
|
4461
|
+
const { value, display, history, scaling } = telemetry;
|
|
4459
4462
|
const showValue = display?.showValue ?? true;
|
|
4460
4463
|
const showStatus = display?.showStatus ?? true;
|
|
4461
4464
|
const showSparkline = display?.showSparkline ?? false;
|
|
4462
4465
|
const label = display?.label;
|
|
4463
|
-
const precision = display?.precision;
|
|
4466
|
+
const precision = scaling?.decimals ?? display?.precision;
|
|
4467
|
+
const unit = scaling?.units ?? value.unit;
|
|
4464
4468
|
const sparklineColor = display?.sparklineColor;
|
|
4465
4469
|
const textColor = display?.textColor;
|
|
4466
4470
|
const backgroundColor = display?.backgroundColor;
|
|
@@ -4478,25 +4482,13 @@ function DataOverlay({
|
|
|
4478
4482
|
}
|
|
4479
4483
|
return val.toString();
|
|
4480
4484
|
};
|
|
4481
|
-
const
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
alarm: { bg: "#ef4444", text: "#ffffff", border: "#dc2626" },
|
|
4485
|
-
fault: { bg: "#dc2626", text: "#ffffff", border: "#b91c1c" },
|
|
4486
|
-
off: { bg: "#6b7280", text: "#ffffff", border: "#4b5563" }
|
|
4487
|
-
};
|
|
4488
|
-
const getStatusColors = () => {
|
|
4489
|
-
const customColor = telemetry.statusColors?.[value.status];
|
|
4490
|
-
if (customColor) {
|
|
4491
|
-
return { bg: customColor, text: "#ffffff", border: customColor };
|
|
4492
|
-
}
|
|
4493
|
-
return defaultStatusColors[value.status] || defaultStatusColors.normal;
|
|
4494
|
-
};
|
|
4495
|
-
const statusColors = getStatusColors();
|
|
4485
|
+
const resolvedEntry = resolveStatusColor(value.status, statusColorMap);
|
|
4486
|
+
const customColor = telemetry.statusColors?.[value.status];
|
|
4487
|
+
const statusColors = customColor ? { bg: customColor, text: "#ffffff", border: customColor } : { bg: resolvedEntry.bg, text: resolvedEntry.text, border: resolvedEntry.border };
|
|
4496
4488
|
const colors = backgroundColor && backgroundColor !== "status" ? { bg: backgroundColor, text: textColor || "#ffffff", border: backgroundColor } : statusColors;
|
|
4497
4489
|
const finalTextColor = textColor || colors.text;
|
|
4498
4490
|
const formattedValue = formatValue(value.value);
|
|
4499
|
-
const displayText =
|
|
4491
|
+
const displayText = unit ? `${formattedValue} ${unit}` : formattedValue;
|
|
4500
4492
|
const sparklinePath = (() => {
|
|
4501
4493
|
if (!showSparkline || !history || history.length < 2) return "";
|
|
4502
4494
|
const width = 70 * scale;
|
|
@@ -4504,8 +4496,9 @@ function DataOverlay({
|
|
|
4504
4496
|
const padding = 5 * scale;
|
|
4505
4497
|
const effectiveWidth = width - padding * 2;
|
|
4506
4498
|
const effectiveHeight = height - padding * 2;
|
|
4507
|
-
const
|
|
4508
|
-
const
|
|
4499
|
+
const hasEuRange = scaling?.euLow !== void 0 && scaling?.euHigh !== void 0 && scaling.euHigh !== scaling.euLow;
|
|
4500
|
+
const min = hasEuRange ? scaling.euLow : Math.min(...history);
|
|
4501
|
+
const max = hasEuRange ? scaling.euHigh : Math.max(...history);
|
|
4509
4502
|
const range = max - min || 1;
|
|
4510
4503
|
const points = history.map((val, i) => {
|
|
4511
4504
|
const xPos = i / (history.length - 1) * effectiveWidth + padding;
|
|
@@ -5763,6 +5756,7 @@ function PIDCanvas({
|
|
|
5763
5756
|
onPipeCreated,
|
|
5764
5757
|
onMounted,
|
|
5765
5758
|
telemetry,
|
|
5759
|
+
statusColorMap,
|
|
5766
5760
|
...props
|
|
5767
5761
|
}) {
|
|
5768
5762
|
const [localDiagram, setLocalDiagram] = (0, import_react16.useState)(diagram);
|
|
@@ -6416,7 +6410,8 @@ function PIDCanvas({
|
|
|
6416
6410
|
enableDrag: dragEnabled && !isConnecting,
|
|
6417
6411
|
onNodeDragStart: dragEnabled && !isConnecting ? handleNodeDragStart : void 0,
|
|
6418
6412
|
draggingNodeId: dragState.draggedId,
|
|
6419
|
-
dragOffset: dragState.offset
|
|
6413
|
+
dragOffset: dragState.offset,
|
|
6414
|
+
statusColorMap
|
|
6420
6415
|
}
|
|
6421
6416
|
),
|
|
6422
6417
|
(features.editPipeRoutes ?? false) && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
@@ -6450,7 +6445,8 @@ function PIDCanvas({
|
|
|
6450
6445
|
x: overlayX,
|
|
6451
6446
|
y: overlayY,
|
|
6452
6447
|
telemetry: binding,
|
|
6453
|
-
scale: features.telemetryScale ?? 1
|
|
6448
|
+
scale: features.telemetryScale ?? 1,
|
|
6449
|
+
statusColorMap
|
|
6454
6450
|
},
|
|
6455
6451
|
`telemetry-${node.id}`
|
|
6456
6452
|
);
|
|
@@ -7664,7 +7660,11 @@ function createControlBinding(nodeId, deviceConfig, actions) {
|
|
|
7664
7660
|
|
|
7665
7661
|
// src/hooks/useDeviceControls.ts
|
|
7666
7662
|
var import_react18 = require("react");
|
|
7667
|
-
function
|
|
7663
|
+
function commandKey(nodeId, kind, suffix) {
|
|
7664
|
+
return suffix ? `${nodeId}:${kind}:${suffix}` : `${nodeId}:${kind}`;
|
|
7665
|
+
}
|
|
7666
|
+
function useDeviceControls(initialBindings, options) {
|
|
7667
|
+
const { commandTimeoutMs } = options ?? {};
|
|
7668
7668
|
const [controls, setControls] = (0, import_react18.useState)(() => {
|
|
7669
7669
|
const map = /* @__PURE__ */ new Map();
|
|
7670
7670
|
if (initialBindings) {
|
|
@@ -7674,6 +7674,11 @@ function useDeviceControls(initialBindings) {
|
|
|
7674
7674
|
}
|
|
7675
7675
|
return map;
|
|
7676
7676
|
});
|
|
7677
|
+
const pendingRef = (0, import_react18.useRef)(/* @__PURE__ */ new Map());
|
|
7678
|
+
const [pendingCommands, setPendingCommands] = (0, import_react18.useState)(() => /* @__PURE__ */ new Set());
|
|
7679
|
+
const syncPending = (0, import_react18.useCallback)(() => {
|
|
7680
|
+
setPendingCommands(new Set(pendingRef.current.keys()));
|
|
7681
|
+
}, []);
|
|
7677
7682
|
const updateControl = (0, import_react18.useCallback)((nodeId, updates) => {
|
|
7678
7683
|
setControls((prev) => {
|
|
7679
7684
|
const newMap = new Map(prev);
|
|
@@ -7763,121 +7768,112 @@ function useDeviceControls(initialBindings) {
|
|
|
7763
7768
|
},
|
|
7764
7769
|
[controls]
|
|
7765
7770
|
);
|
|
7766
|
-
const
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
lastCommandTime: Date.now(),
|
|
7775
|
-
lastCommandResult: { success: true, message: `Mode changed to ${mode}` }
|
|
7776
|
-
});
|
|
7777
|
-
} catch (error) {
|
|
7778
|
-
updateDeviceState(nodeId, {
|
|
7779
|
-
currentMode: binding.state.currentMode,
|
|
7780
|
-
lastCommandResult: {
|
|
7781
|
-
success: false,
|
|
7782
|
-
message: error instanceof Error ? error.message : "Mode change failed"
|
|
7783
|
-
}
|
|
7784
|
-
});
|
|
7771
|
+
const isCommandPending = (0, import_react18.useCallback)(
|
|
7772
|
+
(nodeId, kind) => {
|
|
7773
|
+
if (kind) {
|
|
7774
|
+
const prefix2 = `${nodeId}:${kind}`;
|
|
7775
|
+
for (const key of pendingCommands) {
|
|
7776
|
+
if (key === prefix2 || key.startsWith(`${prefix2}:`)) return true;
|
|
7777
|
+
}
|
|
7778
|
+
return false;
|
|
7785
7779
|
}
|
|
7780
|
+
const prefix = `${nodeId}:`;
|
|
7781
|
+
for (const key of pendingCommands) {
|
|
7782
|
+
if (key.startsWith(prefix)) return true;
|
|
7783
|
+
}
|
|
7784
|
+
return false;
|
|
7786
7785
|
},
|
|
7787
|
-
[
|
|
7786
|
+
[pendingCommands]
|
|
7788
7787
|
);
|
|
7789
|
-
const
|
|
7790
|
-
|
|
7791
|
-
const
|
|
7792
|
-
if (
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
await binding.actions.onParameterChange?.(parameterId, value);
|
|
7796
|
-
updateDeviceState(nodeId, {
|
|
7797
|
-
lastCommandTime: Date.now(),
|
|
7798
|
-
lastCommandResult: { success: true, message: `Parameter ${parameterId} updated` }
|
|
7799
|
-
});
|
|
7800
|
-
} catch (error) {
|
|
7801
|
-
updateDeviceState(nodeId, {
|
|
7802
|
-
lastCommandResult: {
|
|
7803
|
-
success: false,
|
|
7804
|
-
message: error instanceof Error ? error.message : "Parameter change failed"
|
|
7805
|
-
}
|
|
7806
|
-
});
|
|
7807
|
-
}
|
|
7788
|
+
const markPending = (0, import_react18.useCallback)(
|
|
7789
|
+
(key, delta) => {
|
|
7790
|
+
const next = (pendingRef.current.get(key) ?? 0) + delta;
|
|
7791
|
+
if (next > 0) pendingRef.current.set(key, next);
|
|
7792
|
+
else pendingRef.current.delete(key);
|
|
7793
|
+
syncPending();
|
|
7808
7794
|
},
|
|
7809
|
-
[
|
|
7795
|
+
[syncPending]
|
|
7810
7796
|
);
|
|
7811
|
-
const
|
|
7812
|
-
async (nodeId) => {
|
|
7797
|
+
const runCommand = (0, import_react18.useCallback)(
|
|
7798
|
+
async (nodeId, key, invoke, messages) => {
|
|
7813
7799
|
const binding = controls.get(nodeId);
|
|
7814
7800
|
if (!binding) return;
|
|
7815
|
-
|
|
7801
|
+
markPending(key, 1);
|
|
7802
|
+
let timer;
|
|
7816
7803
|
try {
|
|
7817
|
-
|
|
7804
|
+
const action = Promise.resolve(invoke(binding));
|
|
7805
|
+
if (commandTimeoutMs && commandTimeoutMs > 0) {
|
|
7806
|
+
await Promise.race([
|
|
7807
|
+
action,
|
|
7808
|
+
new Promise((_, reject) => {
|
|
7809
|
+
timer = setTimeout(
|
|
7810
|
+
() => reject(new Error(`${messages.success.replace(/ sent$/, "")} timed out (unconfirmed)`)),
|
|
7811
|
+
commandTimeoutMs
|
|
7812
|
+
);
|
|
7813
|
+
})
|
|
7814
|
+
]);
|
|
7815
|
+
} else {
|
|
7816
|
+
await action;
|
|
7817
|
+
}
|
|
7818
7818
|
updateDeviceState(nodeId, {
|
|
7819
|
-
status: "normal",
|
|
7820
7819
|
lastCommandTime: Date.now(),
|
|
7821
|
-
lastCommandResult: { success: true, message:
|
|
7820
|
+
lastCommandResult: { success: true, message: messages.success }
|
|
7822
7821
|
});
|
|
7823
7822
|
} catch (error) {
|
|
7824
7823
|
updateDeviceState(nodeId, {
|
|
7825
|
-
|
|
7826
|
-
status: "fault",
|
|
7824
|
+
lastCommandTime: Date.now(),
|
|
7827
7825
|
lastCommandResult: {
|
|
7828
7826
|
success: false,
|
|
7829
|
-
message: error instanceof Error ? error.message :
|
|
7827
|
+
message: error instanceof Error ? error.message : messages.failure
|
|
7830
7828
|
}
|
|
7831
7829
|
});
|
|
7830
|
+
} finally {
|
|
7831
|
+
if (timer) clearTimeout(timer);
|
|
7832
|
+
markPending(key, -1);
|
|
7832
7833
|
}
|
|
7833
7834
|
},
|
|
7834
|
-
[controls, updateDeviceState]
|
|
7835
|
+
[controls, updateDeviceState, markPending, commandTimeoutMs]
|
|
7836
|
+
);
|
|
7837
|
+
const sendModeChange = (0, import_react18.useCallback)(
|
|
7838
|
+
(nodeId, mode) => runCommand(
|
|
7839
|
+
nodeId,
|
|
7840
|
+
commandKey(nodeId, "mode"),
|
|
7841
|
+
(binding) => binding.actions.onModeChange?.(mode),
|
|
7842
|
+
{ success: `Mode ${mode} command sent`, failure: "Mode change failed" }
|
|
7843
|
+
),
|
|
7844
|
+
[runCommand]
|
|
7845
|
+
);
|
|
7846
|
+
const sendParameterChange = (0, import_react18.useCallback)(
|
|
7847
|
+
(nodeId, parameterId, value) => runCommand(
|
|
7848
|
+
nodeId,
|
|
7849
|
+
commandKey(nodeId, "parameter", parameterId),
|
|
7850
|
+
(binding) => binding.actions.onParameterChange?.(parameterId, value),
|
|
7851
|
+
{ success: `Parameter ${parameterId} command sent`, failure: "Parameter change failed" }
|
|
7852
|
+
),
|
|
7853
|
+
[runCommand]
|
|
7854
|
+
);
|
|
7855
|
+
const sendStartCommand = (0, import_react18.useCallback)(
|
|
7856
|
+
(nodeId) => runCommand(nodeId, commandKey(nodeId, "start"), (binding) => binding.actions.onStart?.(), {
|
|
7857
|
+
success: "Start command sent",
|
|
7858
|
+
failure: "Start command failed"
|
|
7859
|
+
}),
|
|
7860
|
+
[runCommand]
|
|
7835
7861
|
);
|
|
7836
7862
|
const sendStopCommand = (0, import_react18.useCallback)(
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
await binding.actions.onStop?.();
|
|
7843
|
-
updateDeviceState(nodeId, {
|
|
7844
|
-
status: "off",
|
|
7845
|
-
lastCommandTime: Date.now(),
|
|
7846
|
-
lastCommandResult: { success: true, message: "Device stopped" }
|
|
7847
|
-
});
|
|
7848
|
-
} catch (error) {
|
|
7849
|
-
updateDeviceState(nodeId, {
|
|
7850
|
-
isRunning: true,
|
|
7851
|
-
status: "fault",
|
|
7852
|
-
lastCommandResult: {
|
|
7853
|
-
success: false,
|
|
7854
|
-
message: error instanceof Error ? error.message : "Stop failed"
|
|
7855
|
-
}
|
|
7856
|
-
});
|
|
7857
|
-
}
|
|
7858
|
-
},
|
|
7859
|
-
[controls, updateDeviceState]
|
|
7863
|
+
(nodeId) => runCommand(nodeId, commandKey(nodeId, "stop"), (binding) => binding.actions.onStop?.(), {
|
|
7864
|
+
success: "Stop command sent",
|
|
7865
|
+
failure: "Stop command failed"
|
|
7866
|
+
}),
|
|
7867
|
+
[runCommand]
|
|
7860
7868
|
);
|
|
7861
7869
|
const sendCustomAction = (0, import_react18.useCallback)(
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
lastCommandResult: { success: true, message: `Action ${actionId} executed` }
|
|
7870
|
-
});
|
|
7871
|
-
} catch (error) {
|
|
7872
|
-
updateDeviceState(nodeId, {
|
|
7873
|
-
lastCommandResult: {
|
|
7874
|
-
success: false,
|
|
7875
|
-
message: error instanceof Error ? error.message : "Action failed"
|
|
7876
|
-
}
|
|
7877
|
-
});
|
|
7878
|
-
}
|
|
7879
|
-
},
|
|
7880
|
-
[controls, updateDeviceState]
|
|
7870
|
+
(nodeId, actionId) => runCommand(
|
|
7871
|
+
nodeId,
|
|
7872
|
+
commandKey(nodeId, "action", actionId),
|
|
7873
|
+
(binding) => binding.actions.onCustomAction?.(actionId),
|
|
7874
|
+
{ success: `Action ${actionId} command sent`, failure: "Action failed" }
|
|
7875
|
+
),
|
|
7876
|
+
[runCommand]
|
|
7881
7877
|
);
|
|
7882
7878
|
return {
|
|
7883
7879
|
controls,
|
|
@@ -7888,6 +7884,10 @@ function useDeviceControls(initialBindings) {
|
|
|
7888
7884
|
setControlBinding,
|
|
7889
7885
|
removeControlBinding,
|
|
7890
7886
|
getControlBinding,
|
|
7887
|
+
// In-flight command visibility (interaction state, not device state).
|
|
7888
|
+
// `isCommandPending` is the stable query surface; the raw key set is
|
|
7889
|
+
// intentionally not exported (its key format is an implementation detail).
|
|
7890
|
+
isCommandPending,
|
|
7891
7891
|
// Command helpers
|
|
7892
7892
|
sendModeChange,
|
|
7893
7893
|
sendParameterChange,
|
|
@@ -8946,6 +8946,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
|
|
|
8946
8946
|
CardValue,
|
|
8947
8947
|
CircularGauge,
|
|
8948
8948
|
ControlPanel,
|
|
8949
|
+
DEFAULT_STATUS_COLORS,
|
|
8949
8950
|
DEFAULT_THRESHOLDS,
|
|
8950
8951
|
DashboardCard,
|
|
8951
8952
|
DeviceConfigPanel,
|
|
@@ -8995,6 +8996,7 @@ function useSimulation(telemetry, updateTelemetryBatch, options = {}) {
|
|
|
8995
8996
|
overlayStyles,
|
|
8996
8997
|
registerSymbol,
|
|
8997
8998
|
registerSymbols,
|
|
8999
|
+
resolveStatusColor,
|
|
8998
9000
|
useDeviceConfigs,
|
|
8999
9001
|
useDeviceControls,
|
|
9000
9002
|
useDrag,
|