@matter-server/dashboard 1.0.0 → 1.1.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/dist/esm/components/dialogs/acl/acl-actions.d.ts +12 -0
- package/dist/esm/components/dialogs/acl/acl-actions.d.ts.map +1 -0
- package/dist/esm/components/dialogs/acl/acl-actions.js +56 -0
- package/dist/esm/components/dialogs/acl/acl-actions.js.map +6 -0
- package/dist/esm/components/dialogs/acl/model.d.ts +2 -2
- package/dist/esm/components/dialogs/acl/model.d.ts.map +1 -1
- package/dist/esm/components/dialogs/acl/model.js +11 -15
- package/dist/esm/components/dialogs/acl/model.js.map +1 -1
- package/dist/esm/components/dialogs/acl/node-acl-add-dialog.d.ts +43 -0
- package/dist/esm/components/dialogs/acl/node-acl-add-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/acl/node-acl-add-dialog.js +353 -0
- package/dist/esm/components/dialogs/acl/node-acl-add-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.d.ts +8 -0
- package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.js +15 -0
- package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.js.map +6 -0
- package/dist/esm/components/dialogs/binding/binding-actions.d.ts +17 -0
- package/dist/esm/components/dialogs/binding/binding-actions.d.ts.map +1 -0
- package/dist/esm/components/dialogs/binding/binding-actions.js +135 -0
- package/dist/esm/components/dialogs/binding/binding-actions.js.map +6 -0
- package/dist/esm/components/dialogs/binding/model.d.ts +1 -1
- package/dist/esm/components/dialogs/binding/model.d.ts.map +1 -1
- package/dist/esm/components/dialogs/binding/model.js +8 -3
- package/dist/esm/components/dialogs/binding/model.js.map +1 -1
- package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts +16 -24
- package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -1
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js +210 -332
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
- package/dist/esm/pages/cluster-commands/clusters/access-control-commands.d.ts +37 -0
- package/dist/esm/pages/cluster-commands/clusters/access-control-commands.d.ts.map +1 -0
- package/dist/esm/pages/cluster-commands/clusters/access-control-commands.js +387 -0
- package/dist/esm/pages/cluster-commands/clusters/access-control-commands.js.map +6 -0
- package/dist/esm/pages/cluster-commands/clusters/binding-commands.d.ts +35 -0
- package/dist/esm/pages/cluster-commands/clusters/binding-commands.d.ts.map +1 -0
- package/dist/esm/pages/cluster-commands/clusters/binding-commands.js +254 -0
- package/dist/esm/pages/cluster-commands/clusters/binding-commands.js.map +6 -0
- package/dist/esm/pages/cluster-commands/index.d.ts +2 -0
- package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
- package/dist/esm/pages/cluster-commands/index.js +2 -0
- package/dist/esm/pages/cluster-commands/index.js.map +1 -1
- package/dist/esm/pages/components/node-details.d.ts +0 -1
- package/dist/esm/pages/components/node-details.d.ts.map +1 -1
- package/dist/esm/pages/components/node-details.js +2 -18
- package/dist/esm/pages/components/node-details.js.map +1 -1
- package/dist/esm/pages/components/server-details.js +3 -3
- package/dist/esm/pages/components/server-details.js.map +1 -1
- package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-cluster-view.js +2 -1
- package/dist/esm/pages/matter-cluster-view.js.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.d.ts +3 -3
- package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.js +13 -10
- package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
- package/dist/esm/pages/matter-node-view.js +2 -2
- package/dist/esm/pages/matter-node-view.js.map +1 -1
- package/dist/esm/pages/network/network-utils.d.ts +1 -5
- package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.js +1 -11
- package/dist/esm/pages/network/network-utils.js.map +1 -1
- package/dist/esm/util/access-control.d.ts +54 -0
- package/dist/esm/util/access-control.d.ts.map +1 -0
- package/dist/esm/util/access-control.js +100 -0
- package/dist/esm/util/access-control.js.map +6 -0
- package/dist/esm/util/binding.d.ts +54 -0
- package/dist/esm/util/binding.d.ts.map +1 -0
- package/dist/esm/util/binding.js +113 -0
- package/dist/esm/util/binding.js.map +6 -0
- package/dist/esm/util/endpoints.d.ts +9 -0
- package/dist/esm/util/endpoints.d.ts.map +1 -0
- package/dist/esm/util/endpoints.js +18 -0
- package/dist/esm/util/endpoints.js.map +6 -0
- package/dist/esm/util/node-name.d.ts +12 -0
- package/dist/esm/util/node-name.d.ts.map +1 -0
- package/dist/esm/util/node-name.js +15 -0
- package/dist/esm/util/node-name.js.map +6 -0
- package/dist/web/js/{attribute-write-dialog-W7xpCE2E.js → attribute-write-dialog-CqqdRniU.js} +1 -1
- package/dist/web/js/{command-invoke-dialog-BAqAAdJw.js → command-invoke-dialog-BuvBOrdC.js} +1 -1
- package/dist/web/js/{commission-node-dialog-BTzCGgdy.js → commission-node-dialog-nVZp3go0.js} +5 -5
- package/dist/web/js/{commission-node-existing-B2M2hyDh.js → commission-node-existing-Cx3Ahk2t.js} +2 -2
- package/dist/web/js/{commission-node-thread-djdz2dXW.js → commission-node-thread-CI8mWWPs.js} +2 -2
- package/dist/web/js/{commission-node-wifi-DxAYNS1A.js → commission-node-wifi-lUX4LK2R.js} +2 -2
- package/dist/web/js/{dialog-box-tHvPVxDN.js → dialog-box-bAdbnf-T.js} +1 -1
- package/dist/web/js/{fire_event-BleYfTLc.js → fire_event-tWhqPfdz.js} +1 -1
- package/dist/web/js/main.js +1 -1
- package/dist/web/js/{matter-dashboard-app-CVi_GDky.js → matter-dashboard-app-CONA_608.js} +1485 -390
- package/dist/web/js/node-acl-add-dialog-DlR-sF-b.js +320 -0
- package/dist/web/js/node-binding-dialog-DhnX_86M.js +267 -0
- package/dist/web/js/{node-label-dialog-DVZSjsXU.js → node-label-dialog-T3nPG-Qy.js} +1 -1
- package/dist/web/js/{settings-dialog-BG5MgZcO.js → settings-dialog-DrHzJtsi.js} +1 -1
- package/package.json +4 -4
- package/src/components/dialogs/acl/acl-actions.ts +71 -0
- package/src/components/dialogs/acl/model.ts +18 -17
- package/src/components/dialogs/acl/node-acl-add-dialog.ts +350 -0
- package/src/components/dialogs/acl/show-node-acl-add-dialog.ts +14 -0
- package/src/components/dialogs/binding/binding-actions.ts +201 -0
- package/src/components/dialogs/binding/model.ts +11 -4
- package/src/components/dialogs/binding/node-binding-dialog.ts +221 -399
- package/src/pages/cluster-commands/clusters/access-control-commands.ts +407 -0
- package/src/pages/cluster-commands/clusters/binding-commands.ts +273 -0
- package/src/pages/cluster-commands/index.ts +2 -0
- package/src/pages/components/node-details.ts +2 -21
- package/src/pages/components/server-details.ts +3 -3
- package/src/pages/matter-cluster-view.ts +4 -1
- package/src/pages/matter-endpoint-view.ts +16 -10
- package/src/pages/matter-node-view.ts +2 -2
- package/src/pages/network/network-utils.ts +1 -18
- package/src/util/access-control.ts +135 -0
- package/src/util/binding.ts +182 -0
- package/src/util/endpoints.ts +17 -0
- package/src/util/node-name.ts +18 -0
- package/dist/web/js/node-binding-dialog-B9IdqHrZ.js +0 -624
|
@@ -15,377 +15,249 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
15
15
|
*/
|
|
16
16
|
import "@material/web/button/text-button";
|
|
17
17
|
import "@material/web/dialog/dialog";
|
|
18
|
-
import
|
|
19
|
-
import "@material/web/
|
|
20
|
-
import "@material/web/list/list-item";
|
|
18
|
+
import "@material/web/select/outlined-select";
|
|
19
|
+
import "@material/web/select/select-option";
|
|
21
20
|
import "@material/web/textfield/outlined-text-field";
|
|
22
|
-
import "
|
|
21
|
+
import { consume } from "@lit/context";
|
|
23
22
|
import { css, html, LitElement, nothing } from "lit";
|
|
24
|
-
import { customElement, property,
|
|
23
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
25
24
|
import { clientContext } from "../../../client/client-context.js";
|
|
25
|
+
import { clusters } from "../../../client/models/descriptions.js";
|
|
26
|
+
import { nodeIdKey } from "../../../util/access-control.js";
|
|
26
27
|
import { handleAsync } from "../../../util/async-handler.js";
|
|
27
|
-
import {
|
|
28
|
+
import { bindableClusters, targetAclCapacityForBinding } from "../../../util/binding.js";
|
|
29
|
+
import { getEndpointDeviceTypes } from "../../../util/endpoints.js";
|
|
30
|
+
import { getDeviceName } from "../../../util/node-name.js";
|
|
28
31
|
import { preventDefault } from "../../../util/prevent_default.js";
|
|
29
|
-
import { showAlertDialog
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import { BindingEntryDataTransformer } from "./model.js";
|
|
32
|
+
import { showAlertDialog } from "../../dialog-box/show-dialog-box.js";
|
|
33
|
+
import { addBinding } from "./binding-actions.js";
|
|
34
|
+
const ALL_CLUSTERS = "all";
|
|
35
|
+
const CUSTOM_CLUSTER = "custom";
|
|
34
36
|
let NodeBindingDialog = class extends LitElement {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
constructor() {
|
|
38
|
+
super(...arguments);
|
|
39
|
+
this._nodeIdInput = "";
|
|
40
|
+
this._endpointInput = "";
|
|
41
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
42
|
+
this._customClusterInput = "";
|
|
43
|
+
this._busy = false;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
45
|
+
_knownNodes() {
|
|
46
|
+
return Object.values(this.client.nodes).filter((n) => nodeIdKey(n.node_id) !== nodeIdKey(this.node.node_id)).sort((a, b) => {
|
|
47
|
+
const x = BigInt(a.node_id);
|
|
48
|
+
const y = BigInt(b.node_id);
|
|
49
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
50
|
+
});
|
|
46
51
|
}
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const endpoint = rawBindings[index].endpoint;
|
|
52
|
-
if (targetNodeId === void 0 || endpoint === void 0) return;
|
|
53
|
-
let aclCleanedUp = false;
|
|
54
|
-
try {
|
|
55
|
-
await this.removeNodeAtACLEntry(this.node.node_id, endpoint, targetNodeId);
|
|
56
|
-
aclCleanedUp = true;
|
|
57
|
-
} catch (aclError) {
|
|
58
|
-
const errorMessage = aclError instanceof Error ? aclError.message : String(aclError);
|
|
59
|
-
const proceed = await showPromptDialog({
|
|
60
|
-
title: "ACL cleanup failed",
|
|
61
|
-
text: `Could not clean up ACL on target node ${targetNodeId}: ${errorMessage}. The target node may no longer exist or be unreachable. Do you want to remove the binding anyway? Note: The target device may retain an outdated ACL entry.`,
|
|
62
|
-
confirmText: "Remove binding"
|
|
63
|
-
});
|
|
64
|
-
if (!proceed) return;
|
|
65
|
-
}
|
|
66
|
-
try {
|
|
67
|
-
const updatedBindings = this.removeBindingAtIndex(rawBindings, index);
|
|
68
|
-
await this.syncBindingUpdates(updatedBindings, index);
|
|
69
|
-
} catch (bindingError) {
|
|
70
|
-
const errorMessage = bindingError instanceof Error ? bindingError.message : String(bindingError);
|
|
71
|
-
await showAlertDialog({
|
|
72
|
-
title: "Binding removal failed",
|
|
73
|
-
text: `Failed to remove the binding: ${errorMessage}. ` + (aclCleanedUp ? "The ACL on the target device was already updated. The binding and ACL may now be out of sync." : "No changes were made.")
|
|
74
|
-
});
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
} catch (error) {
|
|
78
|
-
this.handleBindingDeletionError(error);
|
|
79
|
-
}
|
|
52
|
+
_resolveTarget() {
|
|
53
|
+
const raw = this._nodeIdInput.trim();
|
|
54
|
+
if (!/^\d+$/.test(raw)) return void 0;
|
|
55
|
+
return this.client.nodes[nodeIdKey(BigInt(raw))];
|
|
80
56
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
removeEntryAtACL(nodeId, sourceEndpoint, entry) {
|
|
88
|
-
const hasSubject = entry.subjects.includes(nodeId);
|
|
89
|
-
if (!hasSubject) return entry;
|
|
90
|
-
const hasTarget = entry.targets.filter((item) => item.endpoint === sourceEndpoint);
|
|
91
|
-
return hasTarget.length > 0 ? void 0 : entry;
|
|
92
|
-
}
|
|
93
|
-
removeBindingAtIndex(bindings, index) {
|
|
94
|
-
return [...bindings.slice(0, index), ...bindings.slice(index + 1)];
|
|
95
|
-
}
|
|
96
|
-
async syncBindingUpdates(updatedBindings, index) {
|
|
97
|
-
const apiBindings = updatedBindings.map((b) => this.toBindingTarget(b));
|
|
98
|
-
await this.client.setNodeBinding(this.node.node_id, this.endpoint, apiBindings);
|
|
99
|
-
const attributePath = `${this.endpoint}/30/0`;
|
|
100
|
-
const currentBindings = this.node.attributes[attributePath];
|
|
101
|
-
const updatedAttributes = {
|
|
102
|
-
...this.node.attributes,
|
|
103
|
-
[attributePath]: currentBindings ? this.removeBindingAtIndex(currentBindings, index) : []
|
|
104
|
-
};
|
|
105
|
-
this.node.attributes = updatedAttributes;
|
|
106
|
-
this.requestUpdate();
|
|
107
|
-
}
|
|
108
|
-
handleBindingDeletionError(error) {
|
|
109
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
110
|
-
console.error(`Binding deletion failed: ${errorMessage}`);
|
|
111
|
-
}
|
|
112
|
-
async add_target_acl(targetNodeId, entry) {
|
|
113
|
-
try {
|
|
114
|
-
const rawEntries = this.client.nodes[String(targetNodeId)]?.attributes["0/31/0"];
|
|
115
|
-
const entries = rawEntries ? Object.values(rawEntries).map((v) => AccessControlEntryDataTransformer.transform(v)) : [];
|
|
116
|
-
entries.push(entry);
|
|
117
|
-
const apiEntries = entries.map((e) => this.toAccessControlEntry(e));
|
|
118
|
-
const results = await this.client.setACLEntry(targetNodeId, apiEntries);
|
|
119
|
-
const batchResult = analyzeBatchResults(results);
|
|
120
|
-
if (batchResult.outcome !== "all_success") {
|
|
121
|
-
console.error(`Set ACL entry: ${batchResult.message}`);
|
|
122
|
-
}
|
|
123
|
-
return batchResult;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error("Add ACL error:", err);
|
|
126
|
-
return {
|
|
127
|
-
outcome: "all_failed",
|
|
128
|
-
successCount: 0,
|
|
129
|
-
failureCount: 1,
|
|
130
|
-
errorCounts: { 1: 1 },
|
|
131
|
-
message: `Exception: ${err instanceof Error ? err.message : String(err)}`
|
|
132
|
-
};
|
|
57
|
+
_nodeEndpoints(target) {
|
|
58
|
+
const eps = /* @__PURE__ */ new Set();
|
|
59
|
+
for (const key of Object.keys(target.attributes)) {
|
|
60
|
+
const m = /^(\d+)\/29\/0$/.exec(key);
|
|
61
|
+
if (m) eps.add(Number(m[1]));
|
|
133
62
|
}
|
|
63
|
+
return Array.from(eps).sort((a, b) => a - b);
|
|
134
64
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
node: entry.node ?? null,
|
|
139
|
-
group: entry.group ?? null,
|
|
140
|
-
endpoint: entry.endpoint ?? null,
|
|
141
|
-
cluster: entry.cluster ?? null
|
|
142
|
-
};
|
|
65
|
+
_clusterLabel(id) {
|
|
66
|
+
return `${clusters[id]?.label ?? "Cluster"} (0x${id.toString(16).padStart(2, "0").toUpperCase()})`;
|
|
143
67
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
subjects: entry.subjects ?? null,
|
|
150
|
-
targets: entry.targets?.map((t) => ({
|
|
151
|
-
cluster: t.cluster ?? null,
|
|
152
|
-
endpoint: t.endpoint ?? null,
|
|
153
|
-
device_type: t.deviceType ?? null
|
|
154
|
-
})) ?? null
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
async add_bindings(endpoint, bindingEntry) {
|
|
158
|
-
const bindings = this.fetchBindingEntry();
|
|
159
|
-
bindings.push(bindingEntry);
|
|
160
|
-
try {
|
|
161
|
-
const apiBindings = bindings.map((b) => this.toBindingTarget(b));
|
|
162
|
-
const results = await this.client.setNodeBinding(this.node.node_id, endpoint, apiBindings);
|
|
163
|
-
const batchResult = analyzeBatchResults(results);
|
|
164
|
-
if (batchResult.outcome !== "all_success") {
|
|
165
|
-
console.error(`Set binding: ${batchResult.message}`);
|
|
166
|
-
}
|
|
167
|
-
return batchResult;
|
|
168
|
-
} catch (err) {
|
|
169
|
-
console.error("Add bindings error:", err);
|
|
170
|
-
return {
|
|
171
|
-
outcome: "all_failed",
|
|
172
|
-
successCount: 0,
|
|
173
|
-
failureCount: 1,
|
|
174
|
-
errorCounts: { 1: 1 },
|
|
175
|
-
message: `Exception: ${err instanceof Error ? err.message : String(err)}`
|
|
176
|
-
};
|
|
177
|
-
}
|
|
68
|
+
_onNodeSelect(e) {
|
|
69
|
+
const select = e.target;
|
|
70
|
+
this._nodeIdInput = select.value;
|
|
71
|
+
this._endpointInput = "";
|
|
72
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
178
73
|
}
|
|
179
|
-
async
|
|
180
|
-
|
|
181
|
-
const rawNodeId = this.
|
|
182
|
-
if (rawNodeId) {
|
|
183
|
-
|
|
184
|
-
showAlertDialog({ title: "Validation error", text: "Please enter a valid target node ID" });
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
targetNodeId = BigInt(rawNodeId);
|
|
188
|
-
}
|
|
189
|
-
const targetEndpoint = this._targetEndpoint.value ? parseInt(this._targetEndpoint.value, 10) : void 0;
|
|
190
|
-
const targetCluster = this._targetCluster.value ? parseInt(this._targetCluster.value, 10) : void 0;
|
|
191
|
-
if (targetNodeId === void 0 || targetNodeId <= 0n) {
|
|
192
|
-
showAlertDialog({ title: "Validation error", text: "Please enter a valid target node ID" });
|
|
74
|
+
async _add() {
|
|
75
|
+
const target = this._resolveTarget();
|
|
76
|
+
const rawNodeId = this._nodeIdInput.trim();
|
|
77
|
+
if (!/^\d+$/.test(rawNodeId) || BigInt(rawNodeId) <= 0n) {
|
|
78
|
+
await showAlertDialog({ title: "Validation error", text: "Please enter a valid target node id." });
|
|
193
79
|
return;
|
|
194
80
|
}
|
|
195
|
-
|
|
196
|
-
|
|
81
|
+
const targetNodeId = BigInt(rawNodeId);
|
|
82
|
+
const endpoint = parseInt(this._endpointInput, 10);
|
|
83
|
+
if (Number.isNaN(endpoint) || endpoint < 0 || endpoint > 65534) {
|
|
84
|
+
await showAlertDialog({ title: "Validation error", text: "Please enter a valid target endpoint." });
|
|
197
85
|
return;
|
|
198
86
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
87
|
+
let cluster;
|
|
88
|
+
if (this._clusterSelection === ALL_CLUSTERS) {
|
|
89
|
+
cluster = void 0;
|
|
90
|
+
} else if (this._clusterSelection === CUSTOM_CLUSTER) {
|
|
91
|
+
const c = parseInt(this._customClusterInput, 10);
|
|
92
|
+
if (Number.isNaN(c) || c < 0 || c > 32767) {
|
|
93
|
+
await showAlertDialog({ title: "Validation error", text: "Please enter a valid cluster id." });
|
|
202
94
|
return;
|
|
203
95
|
}
|
|
96
|
+
cluster = c;
|
|
97
|
+
} else {
|
|
98
|
+
cluster = parseInt(this._clusterSelection, 10);
|
|
204
99
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
privilege: 3,
|
|
212
|
-
authMode: 2,
|
|
213
|
-
subjects: [this.node.node_id],
|
|
214
|
-
targets: [targets],
|
|
215
|
-
fabricIndex: 0
|
|
216
|
-
// Placeholder - server will use correct fabric index
|
|
217
|
-
};
|
|
218
|
-
const aclResult = await this.add_target_acl(targetNodeId, acl_entry);
|
|
219
|
-
if (aclResult.outcome === "all_failed") {
|
|
220
|
-
showAlertDialog({ title: "Failed to add ACL entry", text: aclResult.message });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (aclResult.outcome === "partial") {
|
|
224
|
-
showAlertDialog({ title: "ACL entry partially failed", text: aclResult.message });
|
|
100
|
+
if (target) {
|
|
101
|
+
const capacity = targetAclCapacityForBinding(target, this.node.node_id);
|
|
102
|
+
if (!capacity.canAdd) {
|
|
103
|
+
await showAlertDialog({ title: "Cannot add binding", text: capacity.reason ?? "Target ACL is full." });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
225
106
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
node
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this._targetNodeId.value = "";
|
|
238
|
-
this._targetEndpoint.value = "";
|
|
239
|
-
this._targetCluster.value = "";
|
|
240
|
-
this.requestUpdate();
|
|
241
|
-
} else if (bindingResult.outcome === "partial") {
|
|
242
|
-
showAlertDialog({ title: "Binding partially failed", text: bindingResult.message });
|
|
243
|
-
this.requestUpdate();
|
|
244
|
-
} else {
|
|
245
|
-
showAlertDialog({ title: "Failed to add binding", text: bindingResult.message });
|
|
107
|
+
this._busy = true;
|
|
108
|
+
try {
|
|
109
|
+
await addBinding(this.client, this.node, this.endpoint, targetNodeId, endpoint, cluster);
|
|
110
|
+
this._close();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
await showAlertDialog({
|
|
113
|
+
title: "Failed to add binding",
|
|
114
|
+
text: err instanceof Error ? err.message : String(err)
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
this._busy = false;
|
|
246
118
|
}
|
|
247
119
|
}
|
|
248
120
|
_close() {
|
|
249
121
|
this.shadowRoot.querySelector("md-dialog").close();
|
|
250
122
|
}
|
|
251
123
|
_handleClosed() {
|
|
252
|
-
this.parentNode
|
|
124
|
+
this.parentNode?.removeChild(this);
|
|
253
125
|
}
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
126
|
+
_renderClusterField(target, endpoint) {
|
|
127
|
+
const known = target !== void 0 && endpoint !== void 0 && !Number.isNaN(endpoint);
|
|
128
|
+
const split = known ? bindableClusters(this.node, this.endpoint, target, endpoint) : void 0;
|
|
129
|
+
const nonBindable = split !== void 0 && this._clusterSelection !== ALL_CLUSTERS && this._clusterSelection !== CUSTOM_CLUSTER && split.otherTarget.includes(parseInt(this._clusterSelection, 10));
|
|
130
|
+
return html`
|
|
131
|
+
<md-outlined-select
|
|
132
|
+
label="Cluster"
|
|
133
|
+
.value=${this._clusterSelection}
|
|
134
|
+
?disabled=${this._busy}
|
|
135
|
+
@change=${(e) => this._clusterSelection = e.target.value}
|
|
136
|
+
>
|
|
137
|
+
<md-select-option value=${ALL_CLUSTERS}>
|
|
138
|
+
<div slot="headline">All clusters (any eligible)</div>
|
|
139
|
+
</md-select-option>
|
|
140
|
+
${split && split.bindable.length ? html`<md-select-option disabled><div slot="headline">— Bindable —</div></md-select-option>
|
|
141
|
+
${split.bindable.map(
|
|
142
|
+
(c) => html`<md-select-option value=${String(c)}
|
|
143
|
+
><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
|
|
144
|
+
>`
|
|
145
|
+
)}` : nothing}
|
|
146
|
+
${split && split.otherTarget.length ? html`<md-select-option disabled
|
|
147
|
+
><div slot="headline">— Other target clusters (⚠) —</div></md-select-option
|
|
148
|
+
>
|
|
149
|
+
${split.otherTarget.map(
|
|
150
|
+
(c) => html`<md-select-option value=${String(c)}
|
|
151
|
+
><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
|
|
152
|
+
>`
|
|
153
|
+
)}` : nothing}
|
|
154
|
+
<md-select-option value=${CUSTOM_CLUSTER}
|
|
155
|
+
><div slot="headline">Custom cluster id…</div></md-select-option
|
|
156
|
+
>
|
|
157
|
+
</md-outlined-select>
|
|
158
|
+
${this._clusterSelection === CUSTOM_CLUSTER ? html`<md-outlined-text-field
|
|
159
|
+
label="cluster id"
|
|
160
|
+
type="number"
|
|
161
|
+
min="0"
|
|
162
|
+
max="32767"
|
|
163
|
+
.value=${this._customClusterInput}
|
|
164
|
+
?disabled=${this._busy}
|
|
165
|
+
@input=${(e) => this._customClusterInput = e.target.value}
|
|
166
|
+
></md-outlined-text-field>` : nothing}
|
|
167
|
+
${nonBindable ? html`<div class="warn">
|
|
168
|
+
⚠ This cluster is not a client cluster on the source endpoint. The binding may not function — it
|
|
169
|
+
will be added anyway on your request.
|
|
170
|
+
</div>` : nothing}
|
|
171
|
+
`;
|
|
270
172
|
}
|
|
271
173
|
render() {
|
|
272
|
-
|
|
273
|
-
const
|
|
174
|
+
if (!this.node) return nothing;
|
|
175
|
+
const target = this._resolveTarget();
|
|
176
|
+
const endpoint = this._endpointInput === "" ? void 0 : parseInt(this._endpointInput, 10);
|
|
177
|
+
const endpoints = target ? this._nodeEndpoints(target) : [];
|
|
274
178
|
return html`
|
|
275
179
|
<md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
|
|
276
|
-
<div slot="headline">
|
|
277
|
-
<div>Binding</div>
|
|
278
|
-
</div>
|
|
180
|
+
<div slot="headline">Add binding</div>
|
|
279
181
|
<div slot="content">
|
|
280
|
-
<div>
|
|
281
|
-
<md-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
@click=${handleAsync(() => this.deleteBindingHandler(index))}
|
|
293
|
-
>delete</md-text-button
|
|
294
|
-
</div>
|
|
295
|
-
</md-list-item>
|
|
296
|
-
`
|
|
182
|
+
<div class="form">
|
|
183
|
+
<md-outlined-select
|
|
184
|
+
label="Known nodes"
|
|
185
|
+
?disabled=${this._busy}
|
|
186
|
+
.value=${target ? this._nodeIdInput : ""}
|
|
187
|
+
@change=${this._onNodeSelect}
|
|
188
|
+
>
|
|
189
|
+
<md-select-option value=""><div slot="headline">— pick a node —</div></md-select-option>
|
|
190
|
+
${this._knownNodes().map(
|
|
191
|
+
(n) => html`<md-select-option value=${nodeIdKey(n.node_id)}>
|
|
192
|
+
<div slot="headline">${n.node_id.toString()} · ${getDeviceName(n)}</div>
|
|
193
|
+
</md-select-option>`
|
|
297
194
|
)}
|
|
298
|
-
</md-
|
|
299
|
-
<
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
explicitly.
|
|
340
|
-
</span>
|
|
341
|
-
</div>
|
|
195
|
+
</md-outlined-select>
|
|
196
|
+
<md-outlined-text-field
|
|
197
|
+
label="Target node id"
|
|
198
|
+
type="text"
|
|
199
|
+
pattern="[0-9]+"
|
|
200
|
+
supporting-text="required — pick above or enter a raw node id"
|
|
201
|
+
.value=${this._nodeIdInput}
|
|
202
|
+
?disabled=${this._busy}
|
|
203
|
+
@input=${(e) => {
|
|
204
|
+
this._nodeIdInput = e.target.value;
|
|
205
|
+
this._endpointInput = "";
|
|
206
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
207
|
+
}}
|
|
208
|
+
></md-outlined-text-field>
|
|
209
|
+
|
|
210
|
+
${target ? html`<md-outlined-select
|
|
211
|
+
label="Target endpoint"
|
|
212
|
+
?disabled=${this._busy}
|
|
213
|
+
.value=${this._endpointInput}
|
|
214
|
+
@change=${(e) => {
|
|
215
|
+
this._endpointInput = e.target.value;
|
|
216
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
${endpoints.map((ep) => {
|
|
220
|
+
const dt = getEndpointDeviceTypes(target, ep)[0];
|
|
221
|
+
return html`<md-select-option value=${String(ep)}>
|
|
222
|
+
<div slot="headline">EP ${ep}${dt ? ` \xB7 ${dt.label}` : ""}</div>
|
|
223
|
+
</md-select-option>`;
|
|
224
|
+
})}
|
|
225
|
+
</md-outlined-select>` : html`<md-outlined-text-field
|
|
226
|
+
label="Target endpoint"
|
|
227
|
+
type="number"
|
|
228
|
+
min="0"
|
|
229
|
+
max="65534"
|
|
230
|
+
supporting-text=${this._nodeIdInput.trim() === "" ? "enter a node id first" : "unknown node \u2014 enter endpoint manually"}
|
|
231
|
+
?disabled=${this._busy || this._nodeIdInput.trim() === ""}
|
|
232
|
+
.value=${this._endpointInput}
|
|
233
|
+
@input=${(e) => this._endpointInput = e.target.value}
|
|
234
|
+
></md-outlined-text-field>`}
|
|
235
|
+
${this._renderClusterField(target, endpoint)}
|
|
342
236
|
</div>
|
|
343
237
|
</div>
|
|
344
238
|
<div slot="actions">
|
|
345
|
-
<md-text-button @click=${handleAsync(() => this.
|
|
346
|
-
|
|
239
|
+
<md-text-button ?disabled=${this._busy} @click=${handleAsync(() => this._add())}
|
|
240
|
+
>Add</md-text-button
|
|
241
|
+
>
|
|
242
|
+
<md-text-button ?disabled=${this._busy} @click=${this._close}>Cancel</md-text-button>
|
|
347
243
|
</div>
|
|
348
244
|
</md-dialog>
|
|
349
245
|
`;
|
|
350
246
|
}
|
|
351
247
|
};
|
|
352
248
|
NodeBindingDialog.styles = css`
|
|
353
|
-
.
|
|
354
|
-
background: var(--md-sys-color-surface-container-high);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
.inline-group {
|
|
358
|
-
display: flex;
|
|
359
|
-
border: 2px solid var(--md-sys-color-primary);
|
|
360
|
-
padding: 1px;
|
|
361
|
-
border-radius: 8px;
|
|
362
|
-
position: relative;
|
|
363
|
-
margin: 8px;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
.group-input {
|
|
249
|
+
.form {
|
|
367
250
|
display: flex;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
.target-item {
|
|
372
|
-
display: inline-block;
|
|
373
|
-
padding: 16px 8px 8px 8px;
|
|
374
|
-
border-radius: 4px;
|
|
375
|
-
vertical-align: middle;
|
|
376
|
-
min-width: 80px;
|
|
377
|
-
text-align: center;
|
|
378
|
-
width: -webkit-fill-available;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: 12px;
|
|
253
|
+
min-width: 320px;
|
|
379
254
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
color: var(--md-sys-color-on-primary);
|
|
387
|
-
padding: 4px 16px;
|
|
388
|
-
border-radius: 4px;
|
|
255
|
+
.warn {
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
padding: 8px 10px;
|
|
258
|
+
border-radius: 7px;
|
|
259
|
+
background: var(--md-sys-color-error-container);
|
|
260
|
+
color: var(--md-sys-color-on-error-container);
|
|
389
261
|
}
|
|
390
262
|
`;
|
|
391
263
|
__decorateClass([
|
|
@@ -399,14 +271,20 @@ __decorateClass([
|
|
|
399
271
|
property({ attribute: false })
|
|
400
272
|
], NodeBindingDialog.prototype, "endpoint", 2);
|
|
401
273
|
__decorateClass([
|
|
402
|
-
|
|
403
|
-
], NodeBindingDialog.prototype, "
|
|
274
|
+
state()
|
|
275
|
+
], NodeBindingDialog.prototype, "_nodeIdInput", 2);
|
|
276
|
+
__decorateClass([
|
|
277
|
+
state()
|
|
278
|
+
], NodeBindingDialog.prototype, "_endpointInput", 2);
|
|
279
|
+
__decorateClass([
|
|
280
|
+
state()
|
|
281
|
+
], NodeBindingDialog.prototype, "_clusterSelection", 2);
|
|
404
282
|
__decorateClass([
|
|
405
|
-
|
|
406
|
-
], NodeBindingDialog.prototype, "
|
|
283
|
+
state()
|
|
284
|
+
], NodeBindingDialog.prototype, "_customClusterInput", 2);
|
|
407
285
|
__decorateClass([
|
|
408
|
-
|
|
409
|
-
], NodeBindingDialog.prototype, "
|
|
286
|
+
state()
|
|
287
|
+
], NodeBindingDialog.prototype, "_busy", 2);
|
|
410
288
|
NodeBindingDialog = __decorateClass([
|
|
411
289
|
customElement("node-binding-dialog")
|
|
412
290
|
], NodeBindingDialog);
|