@vitormnm/node-red-simple-opcua 1.7.0 → 1.8.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 +17 -2
- package/client/lib/opcua-client-browser.js +19 -4
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.js +38 -1
- package/client/opcua-client-utils.js +124 -0
- package/client/opcua-client.html +1 -0
- package/client/view/opcua-client.js +106 -14
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +85 -28
- package/server/lib/opcua-address-space-builder.js +653 -45
- package/server/lib/opcua-config.js +29 -12
- package/server/lib/opcua-server-events-child.js +30 -4
- package/server/lib/opcua-server-runtime-child.js +141 -9
- package/server/lib/opcua-server-runtime.js +3 -0
- package/server/lib/opcua-server-status-child.js +32 -1
- package/server/opcua-server-io.html +21 -5
- package/server/opcua-server-io.js +23 -2
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +12 -0
- package/server/opcua-server.html +2 -0
- package/server/view/opcua-server.css +11 -0
- package/server/view/opcua-server.js +240 -23
|
@@ -737,22 +737,39 @@ class OpcUaServerConfigParser {
|
|
|
737
737
|
}
|
|
738
738
|
|
|
739
739
|
coerceValue(value, type) {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
740
|
+
const recursiveMap = (val, fn) => {
|
|
741
|
+
if (Array.isArray(val)) {
|
|
742
|
+
if ((type === "Int64" || type === "UInt64") && val.length === 2 && typeof val[0] === "number" && typeof val[1] === "number") {
|
|
743
|
+
return fn(val);
|
|
744
|
+
}
|
|
745
|
+
return val.map(item => recursiveMap(item, fn));
|
|
746
|
+
}
|
|
747
|
+
return fn(val);
|
|
748
|
+
};
|
|
743
749
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
750
|
+
const extractArray = (val) => {
|
|
751
|
+
if (Array.isArray(val)) {
|
|
752
|
+
return val;
|
|
753
|
+
}
|
|
754
|
+
if (typeof val === "string") {
|
|
755
|
+
const trimmed = val.trim();
|
|
756
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
757
|
+
try {
|
|
758
|
+
const parsed = JSON.parse(trimmed);
|
|
759
|
+
if (Array.isArray(parsed)) {
|
|
760
|
+
return parsed;
|
|
761
|
+
}
|
|
762
|
+
} catch (error) {
|
|
763
|
+
throw new Error("Invalid array value for type " + type + ": " + error.message);
|
|
751
764
|
}
|
|
752
|
-
} catch (error) {
|
|
753
|
-
throw new Error("Invalid array value for type " + type + ": " + error.message);
|
|
754
765
|
}
|
|
755
766
|
}
|
|
767
|
+
return null;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const items = extractArray(value);
|
|
771
|
+
if (items) {
|
|
772
|
+
return recursiveMap(items, (item) => this.coerceScalarValue(item, type));
|
|
756
773
|
}
|
|
757
774
|
|
|
758
775
|
return this.coerceScalarValue(value, type);
|
|
@@ -110,10 +110,36 @@ function flushQueue(node, nodeId) {
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
function upsertEvent(map, event) {
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
const nodeKey = String(event.nodeID || "").trim();
|
|
114
|
+
if (!nodeKey) return;
|
|
115
|
+
|
|
116
|
+
// Key by nodeID only so the same variable is merged into one entry
|
|
117
|
+
const key = nodeKey;
|
|
118
|
+
const existing = map.get(key);
|
|
119
|
+
|
|
120
|
+
if (!existing) {
|
|
121
|
+
// First access for this variable in this interval — store a copy
|
|
122
|
+
map.set(key, Object.assign({}, event, { users: Array.isArray(event.users) ? [...event.users] : [] }));
|
|
123
|
+
} else {
|
|
124
|
+
// Variable already seen — update value and merge any new users
|
|
125
|
+
existing.value = event.value;
|
|
126
|
+
if (event.message !== undefined) existing.message = event.message;
|
|
127
|
+
if (event.severity !== undefined) existing.severity = event.severity;
|
|
128
|
+
if (event.retain !== undefined) existing.retain = event.retain;
|
|
129
|
+
if (event.activeState !== undefined) existing.activeState = event.activeState;
|
|
130
|
+
if (event.sourceName !== undefined) existing.sourceName = event.sourceName;
|
|
131
|
+
if (event.conditionName !== undefined) existing.conditionName = event.conditionName;
|
|
132
|
+
if (event.ConfirmedState !== undefined) existing.ConfirmedState = event.ConfirmedState;
|
|
133
|
+
if (event.ackedState !== undefined) existing.ackedState = event.ackedState;
|
|
134
|
+
if (event.alarmNode !== undefined) existing.alarmNode = event.alarmNode;
|
|
135
|
+
const existingNames = new Set((existing.users || []).map(u => u.name));
|
|
136
|
+
for (const user of (event.users || [])) {
|
|
137
|
+
if (!existingNames.has(user.name)) {
|
|
138
|
+
existing.users.push(user);
|
|
139
|
+
existingNames.add(user.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
117
143
|
}
|
|
118
144
|
|
|
119
145
|
function matchesServer(serverRef, event) {
|
|
@@ -398,6 +398,14 @@ class OpcUaServerProcess {
|
|
|
398
398
|
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
399
399
|
const identifierType = this.resolveIdentifierType(target);
|
|
400
400
|
|
|
401
|
+
let isSingleTag = false;
|
|
402
|
+
try {
|
|
403
|
+
this.resolveIdentifier(target);
|
|
404
|
+
isSingleTag = true;
|
|
405
|
+
} catch (e) {
|
|
406
|
+
// not a single tag
|
|
407
|
+
}
|
|
408
|
+
|
|
401
409
|
// Buffer serializado pelo IPC
|
|
402
410
|
if (
|
|
403
411
|
payload &&
|
|
@@ -464,7 +472,7 @@ class OpcUaServerProcess {
|
|
|
464
472
|
}
|
|
465
473
|
|
|
466
474
|
// Array de objetos
|
|
467
|
-
else if (Array.isArray(payload)) {
|
|
475
|
+
else if (Array.isArray(payload) && (!isSingleTag || this.isBatchWritePayload(payload))) {
|
|
468
476
|
|
|
469
477
|
if (!payload.length) {
|
|
470
478
|
throw new Error("msg.payload array does not contain any items");
|
|
@@ -617,6 +625,18 @@ class OpcUaServerProcess {
|
|
|
617
625
|
}
|
|
618
626
|
}
|
|
619
627
|
|
|
628
|
+
isBatchWritePayload(payload) {
|
|
629
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return payload.every(item =>
|
|
633
|
+
item &&
|
|
634
|
+
typeof item === "object" &&
|
|
635
|
+
!Array.isArray(item) &&
|
|
636
|
+
(item.path !== undefined || item.nodeId !== undefined || item.identifier !== undefined)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
620
640
|
resolveIdentifierType(target) {
|
|
621
641
|
return target && target.identifierType === "nodeId" ? "nodeId" : "path";
|
|
622
642
|
}
|
|
@@ -885,10 +905,12 @@ class OpcUaServerProcess {
|
|
|
885
905
|
|
|
886
906
|
readActiveAlarms(msg, nodeId) {
|
|
887
907
|
try {
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
}
|
|
908
|
+
const result = registry.getActiveAlarms(this.node);
|
|
909
|
+
const safeResult = Array.isArray(result) ? result : [];
|
|
910
|
+
|
|
911
|
+
const msg2 = Object.assign({}, msg || {}, {
|
|
912
|
+
payload: safeResult
|
|
913
|
+
});
|
|
892
914
|
|
|
893
915
|
process.send({
|
|
894
916
|
type: "send",
|
|
@@ -901,12 +923,16 @@ class OpcUaServerProcess {
|
|
|
901
923
|
data: {
|
|
902
924
|
fill: "green",
|
|
903
925
|
shape: "dot",
|
|
904
|
-
text:
|
|
926
|
+
text: safeResult.length + " active alarm" + (safeResult.length !== 1 ? "s" : "")
|
|
905
927
|
},
|
|
906
928
|
nodeId: nodeId
|
|
907
929
|
});
|
|
908
|
-
} catch {
|
|
909
|
-
|
|
930
|
+
} catch (error) {
|
|
931
|
+
process.send({
|
|
932
|
+
type: "error",
|
|
933
|
+
data: error.message,
|
|
934
|
+
nodeId: nodeId
|
|
935
|
+
});
|
|
910
936
|
}
|
|
911
937
|
}
|
|
912
938
|
|
|
@@ -1049,6 +1075,105 @@ class OpcUaServerProcess {
|
|
|
1049
1075
|
}
|
|
1050
1076
|
}
|
|
1051
1077
|
|
|
1078
|
+
async validateLogin(msg, nodeId) {
|
|
1079
|
+
try {
|
|
1080
|
+
await this.ensureReady();
|
|
1081
|
+
const runtime = this.runtime;
|
|
1082
|
+
if (!runtime) {
|
|
1083
|
+
throw new Error("OPC UA server is not available");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const payload = msg && msg.payload ? msg.payload : {};
|
|
1087
|
+
const username = typeof payload.userName === "string" ? payload.userName : (typeof payload.username === "string" ? payload.username : "");
|
|
1088
|
+
const password = typeof payload.password === "string" ? payload.password : "";
|
|
1089
|
+
|
|
1090
|
+
const normalizedUserName = username.trim();
|
|
1091
|
+
const users = Array.isArray(runtime.users) ? runtime.users : [];
|
|
1092
|
+
const user = users.find((entry) => entry && entry.username === normalizedUserName);
|
|
1093
|
+
|
|
1094
|
+
let isValid = false;
|
|
1095
|
+
if (user) {
|
|
1096
|
+
if (user.password && user.password === password) {
|
|
1097
|
+
isValid = true;
|
|
1098
|
+
} else if (user.passwordHash) {
|
|
1099
|
+
let bcrypt = null;
|
|
1100
|
+
try {
|
|
1101
|
+
bcrypt = require("bcryptjs");
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
bcrypt = null;
|
|
1104
|
+
}
|
|
1105
|
+
if (bcrypt) {
|
|
1106
|
+
try {
|
|
1107
|
+
isValid = bcrypt.compareSync(password, user.passwordHash);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
// ignore comparison error
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
let result;
|
|
1116
|
+
if (isValid) {
|
|
1117
|
+
const groups = typeof user.group === "string"
|
|
1118
|
+
? user.group.split(",").map(g => g.trim()).filter(Boolean)
|
|
1119
|
+
: Array.isArray(user.group)
|
|
1120
|
+
? user.group
|
|
1121
|
+
: [];
|
|
1122
|
+
|
|
1123
|
+
result = {
|
|
1124
|
+
status: "Good",
|
|
1125
|
+
username: user.username,
|
|
1126
|
+
group: user.group,
|
|
1127
|
+
groups: groups
|
|
1128
|
+
};
|
|
1129
|
+
} else {
|
|
1130
|
+
result = {
|
|
1131
|
+
status: "erro",
|
|
1132
|
+
message: "Invalid username or password"
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (msg) {
|
|
1137
|
+
const outMsg = Object.assign({}, msg);
|
|
1138
|
+
outMsg.payload = result;
|
|
1139
|
+
|
|
1140
|
+
process.send({
|
|
1141
|
+
type: "send",
|
|
1142
|
+
data: outMsg,
|
|
1143
|
+
nodeId: nodeId
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
process.send({
|
|
1148
|
+
type: "status",
|
|
1149
|
+
data: {
|
|
1150
|
+
fill: isValid ? "green" : "yellow",
|
|
1151
|
+
shape: "dot",
|
|
1152
|
+
text: isValid ? "Login: Good" : "Login: erro"
|
|
1153
|
+
},
|
|
1154
|
+
nodeId: nodeId
|
|
1155
|
+
});
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
if (msg) {
|
|
1158
|
+
const outMsg = Object.assign({}, msg);
|
|
1159
|
+
outMsg.payload = {
|
|
1160
|
+
status: "erro",
|
|
1161
|
+
message: error.message
|
|
1162
|
+
};
|
|
1163
|
+
process.send({
|
|
1164
|
+
type: "send",
|
|
1165
|
+
data: outMsg,
|
|
1166
|
+
nodeId: nodeId
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
process.send({
|
|
1170
|
+
type: "status",
|
|
1171
|
+
data: { fill: "red", shape: "ring", text: "Login error: " + error.message },
|
|
1172
|
+
nodeId: nodeId
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1052
1177
|
}
|
|
1053
1178
|
|
|
1054
1179
|
/**
|
|
@@ -1205,6 +1330,9 @@ process.on("message", async (msg) => {
|
|
|
1205
1330
|
case "deleteActiveSessions":
|
|
1206
1331
|
await serverProcess.deleteActiveSessions(msg.msg, msg.nodeId);
|
|
1207
1332
|
break;
|
|
1333
|
+
case "validateLogin":
|
|
1334
|
+
await serverProcess.validateLogin(msg.msg, msg.nodeId);
|
|
1335
|
+
break;
|
|
1208
1336
|
|
|
1209
1337
|
|
|
1210
1338
|
|
|
@@ -1242,4 +1370,8 @@ process.on("unhandledRejection", (reason) => {
|
|
|
1242
1370
|
data: "Unhandled Rejection: " + (reason?.message || reason),
|
|
1243
1371
|
nodeId: serverProcess.node.id
|
|
1244
1372
|
});
|
|
1245
|
-
});
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
if (require.main !== module) {
|
|
1376
|
+
module.exports = { OpcUaServerProcess };
|
|
1377
|
+
}
|
|
@@ -160,6 +160,9 @@ class OpcUaServerRuntime {
|
|
|
160
160
|
this.ensureReady();
|
|
161
161
|
this.syncNamespaces(treeConfig);
|
|
162
162
|
this.treeConfig = treeConfig;
|
|
163
|
+
// Refresh the user list in the builder so newly added/removed users are
|
|
164
|
+
// recognised in access events without requiring a full server restart.
|
|
165
|
+
this.addressSpaceBuilder.updateUsers(this.users);
|
|
163
166
|
this.addressSpaceBuilder.sync(treeConfig);
|
|
164
167
|
}
|
|
165
168
|
|
|
@@ -99,10 +99,41 @@ function buildServerSnapshot(serverNode) {
|
|
|
99
99
|
},
|
|
100
100
|
endpoints: Array.isArray(server.endpoints)
|
|
101
101
|
? server.endpoints.map((endpoint) => buildEndpointSnapshot(endpoint))
|
|
102
|
-
: []
|
|
102
|
+
: [],
|
|
103
|
+
users: buildUsersSnapshot(serverNode),
|
|
104
|
+
groups: buildGroupsSnapshot(serverNode)
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
function buildUsersSnapshot(serverNode) {
|
|
109
|
+
const rawUsers = Array.isArray(serverNode.users) ? serverNode.users : [];
|
|
110
|
+
const allowAnonymous = Boolean(serverNode.allowAnonymous);
|
|
111
|
+
|
|
112
|
+
const result = rawUsers.map((user) => ({
|
|
113
|
+
name: extractText(user && user.username),
|
|
114
|
+
groups: resolveUserGroups(user, serverNode.groups)
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
if (allowAnonymous) {
|
|
118
|
+
result.unshift({ name: "anonymous", groups: [] });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildGroupsSnapshot(serverNode) {
|
|
125
|
+
const rawGroups = Array.isArray(serverNode.groups) ? serverNode.groups : [];
|
|
126
|
+
return rawGroups.map((g) => extractText(g && (g.name || g))).filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveUserGroups(user, rawGroups) {
|
|
130
|
+
if (!user) return [];
|
|
131
|
+
// normalizeUser stores groups as user.group (comma-separated string)
|
|
132
|
+
const groupsStr = extractText(user.group);
|
|
133
|
+
if (!groupsStr) return [];
|
|
134
|
+
return groupsStr.split(",").map((g) => g.trim()).filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
|
|
106
137
|
function buildEndpointSnapshot(endpoint) {
|
|
107
138
|
const descriptions = typeof endpoint.endpointDescriptions === "function"
|
|
108
139
|
? endpoint.endpointDescriptions()
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
<option value="activeAlarms">Active Alarms</option>
|
|
173
173
|
<option value="getSessions">Get Sessions</option>
|
|
174
174
|
<option value="deleteSessions">Delete Sessions</option>
|
|
175
|
+
<option value="validateLogin">Validate Login</option>
|
|
175
176
|
<option value="method-input">Method Input</option>
|
|
176
177
|
<option value="method-output">Method Output</option>
|
|
177
178
|
</select>
|
|
@@ -410,14 +411,29 @@ msg.payload = [
|
|
|
410
411
|
"error": "cannot find session …"
|
|
411
412
|
}
|
|
412
413
|
]</code></pre>
|
|
413
|
-
<p>Possible <code>status</code> values:</p>
|
|
414
414
|
<ul>
|
|
415
415
|
<li><b>deleted</b> — session was found and successfully closed with reason <code>Forcing</code>.</li>
|
|
416
416
|
<li><b>not_found</b> — no active session with that <code>sessionId</code> exists.</li>
|
|
417
|
-
<li><b>error</b> — the session was found but <code>engine.closeSession()</code> threw; the
|
|
418
|
-
<code>error</code> field contains the message.</li>
|
|
417
|
+
<li><b>error</b> — the session was found but <code>engine.closeSession()</code> threw; the <code>error</code> field contains the message.</li>
|
|
419
418
|
</ul>
|
|
420
419
|
|
|
421
|
-
|
|
422
|
-
|
|
420
|
+
<h3>Validate Login</h3>
|
|
421
|
+
<p>Validates a user's credentials against the OPC UA server's configured user database.</p>
|
|
422
|
+
<p><b>Input</b>: <code>msg.payload</code> must be a JSON object containing <code>userName</code> (or <code>username</code>) and <code>password</code>:</p>
|
|
423
|
+
<pre><code>{
|
|
424
|
+
"userName": "admin",
|
|
425
|
+
"password": "123"
|
|
426
|
+
}</code></pre>
|
|
427
|
+
<p><b>Output</b>: <code>msg.payload</code> contains the status of validation and user groups (excluding password hashes):</p>
|
|
428
|
+
<pre><code>{
|
|
429
|
+
"status": "Good",
|
|
430
|
+
"username": "admin",
|
|
431
|
+
"group": "admin,operator",
|
|
432
|
+
"groups": ["admin", "operator"]
|
|
433
|
+
}</code></pre>
|
|
434
|
+
<p>If authentication fails or the user is not found, the response contains:</p>
|
|
435
|
+
<pre><code>{
|
|
436
|
+
"status": "erro",
|
|
437
|
+
"message": "Invalid username or password"
|
|
438
|
+
}</code></pre>
|
|
423
439
|
</script>
|
|
@@ -119,6 +119,13 @@ module.exports = function (RED) {
|
|
|
119
119
|
node._pendingDoneCallbacks.delete(msg._msgid);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
else if (node.mode === "validateLogin") {
|
|
123
|
+
await handleValidateLogin(node, msg, { waitForServer: true, timeoutMs: 5000 });
|
|
124
|
+
done();
|
|
125
|
+
if (msg && msg._msgid) {
|
|
126
|
+
node._pendingDoneCallbacks.delete(msg._msgid);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
122
129
|
|
|
123
130
|
} catch (error) {
|
|
124
131
|
node.status({ fill: "red", shape: "ring", text: node.mode + " failed" });
|
|
@@ -255,7 +262,8 @@ module.exports = function (RED) {
|
|
|
255
262
|
opcua: {
|
|
256
263
|
server: msg.data.serverName,
|
|
257
264
|
method: msg.data.methodName,
|
|
258
|
-
data: msg.data
|
|
265
|
+
data: msg.data,
|
|
266
|
+
users: Array.isArray(msg.data.users) ? msg.data.users : []
|
|
259
267
|
},
|
|
260
268
|
_callId: msg.data.callId
|
|
261
269
|
});
|
|
@@ -286,7 +294,8 @@ module.exports = function (RED) {
|
|
|
286
294
|
opcua: {
|
|
287
295
|
server: msg.data.serverName,
|
|
288
296
|
method: msg.data.methodName,
|
|
289
|
-
data: msg.data
|
|
297
|
+
data: msg.data,
|
|
298
|
+
users: Array.isArray(msg.data.users) ? msg.data.users : []
|
|
290
299
|
},
|
|
291
300
|
_callId: msg.data.callId
|
|
292
301
|
});
|
|
@@ -345,6 +354,14 @@ module.exports = function (RED) {
|
|
|
345
354
|
}, options);
|
|
346
355
|
}
|
|
347
356
|
|
|
357
|
+
async function handleValidateLogin(node, msg, options) {
|
|
358
|
+
return sendToChild(node, {
|
|
359
|
+
type: "validateLogin",
|
|
360
|
+
msg: msg,
|
|
361
|
+
nodeId: node.id
|
|
362
|
+
}, options);
|
|
363
|
+
}
|
|
364
|
+
|
|
348
365
|
|
|
349
366
|
async function handleWrite(node, msg) {
|
|
350
367
|
|
|
@@ -474,6 +491,10 @@ module.exports = function (RED) {
|
|
|
474
491
|
registerMethodInput(node);
|
|
475
492
|
}
|
|
476
493
|
|
|
494
|
+
if (node.mode === "events") {
|
|
495
|
+
registerEvents(node, { throwOnError: false, silentOnError: true });
|
|
496
|
+
}
|
|
497
|
+
|
|
477
498
|
return true;
|
|
478
499
|
}
|
|
479
500
|
|
|
@@ -216,8 +216,14 @@ function getActiveAlarms(node) {
|
|
|
216
216
|
conditionName: ConditionName,
|
|
217
217
|
ConfirmedState: ConfirmedState,
|
|
218
218
|
ackedState: isAcked,
|
|
219
|
-
alarmNode:
|
|
220
|
-
|
|
219
|
+
alarmNode: {
|
|
220
|
+
nodeId: alarmNode.nodeId,
|
|
221
|
+
browseName: alarmNode.browseName,
|
|
222
|
+
displayName: alarmNode.displayName,
|
|
223
|
+
description: alarmNode.description,
|
|
224
|
+
nodeClass: alarmNode.nodeClass,
|
|
225
|
+
typeDefinition: alarmNode.typeDefinition
|
|
226
|
+
}
|
|
221
227
|
})
|
|
222
228
|
});
|
|
223
229
|
|
package/server/opcua-server.css
CHANGED
|
@@ -315,3 +315,15 @@
|
|
|
315
315
|
width: auto !important;
|
|
316
316
|
display: inline-block;
|
|
317
317
|
}
|
|
318
|
+
|
|
319
|
+
/* Drag and Drop styling for visual tree rows */
|
|
320
|
+
.opcua-tree-row.is-dragging {
|
|
321
|
+
opacity: 0.4;
|
|
322
|
+
border: 1px dashed var(--red-ui-form-input-border-color, #999);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.opcua-tree-row.drag-over {
|
|
326
|
+
background-color: var(--red-ui-list-item-background-hover, #f3f7fd) !important;
|
|
327
|
+
box-shadow: inset 0 0 0 2px var(--red-ui-form-input-border-color-selected, #2196F3);
|
|
328
|
+
}
|
|
329
|
+
|
package/server/opcua-server.html
CHANGED
|
@@ -211,6 +211,8 @@
|
|
|
211
211
|
<a href="#" data-action="add-method"><i class="fa fa-plus"></i> Add Method</a>
|
|
212
212
|
<a href="#" data-action="edit"><i class="fa fa-pencil"></i> Edit</a>
|
|
213
213
|
<a href="#" data-action="remove"><i class="fa fa-trash"></i> Remove</a>
|
|
214
|
+
<a href="#" data-action="expand-all-below"><i class="fa fa-angle-double-down"></i> Expand All Below</a>
|
|
215
|
+
<a href="#" data-action="collapse-all-below"><i class="fa fa-angle-double-up"></i> Collapse All Below</a>
|
|
214
216
|
</div>
|
|
215
217
|
</div>
|
|
216
218
|
</div>
|
|
@@ -576,4 +576,15 @@ body.opcua-tree-modal-open {
|
|
|
576
576
|
.opcua-config-input[type="checkbox"] {
|
|
577
577
|
width: auto !important;
|
|
578
578
|
display: inline-block;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/* Drag and Drop styling for visual tree rows */
|
|
582
|
+
.opcua-tree-row.is-dragging {
|
|
583
|
+
opacity: 0.4;
|
|
584
|
+
border: 1px dashed var(--red-ui-form-input-border-color, #999);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.opcua-tree-row.drag-over {
|
|
588
|
+
background-color: var(--red-ui-list-item-background-hover, #f3f7fd) !important;
|
|
589
|
+
box-shadow: inset 0 0 0 2px var(--red-ui-form-input-border-color-selected, #2196F3);
|
|
579
590
|
}
|