@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.
Files changed (111) hide show
  1. package/dist/esm/components/dialogs/acl/acl-actions.d.ts +12 -0
  2. package/dist/esm/components/dialogs/acl/acl-actions.d.ts.map +1 -0
  3. package/dist/esm/components/dialogs/acl/acl-actions.js +56 -0
  4. package/dist/esm/components/dialogs/acl/acl-actions.js.map +6 -0
  5. package/dist/esm/components/dialogs/acl/model.d.ts +2 -2
  6. package/dist/esm/components/dialogs/acl/model.d.ts.map +1 -1
  7. package/dist/esm/components/dialogs/acl/model.js +11 -15
  8. package/dist/esm/components/dialogs/acl/model.js.map +1 -1
  9. package/dist/esm/components/dialogs/acl/node-acl-add-dialog.d.ts +43 -0
  10. package/dist/esm/components/dialogs/acl/node-acl-add-dialog.d.ts.map +1 -0
  11. package/dist/esm/components/dialogs/acl/node-acl-add-dialog.js +353 -0
  12. package/dist/esm/components/dialogs/acl/node-acl-add-dialog.js.map +6 -0
  13. package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.d.ts +8 -0
  14. package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.d.ts.map +1 -0
  15. package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.js +15 -0
  16. package/dist/esm/components/dialogs/acl/show-node-acl-add-dialog.js.map +6 -0
  17. package/dist/esm/components/dialogs/binding/binding-actions.d.ts +17 -0
  18. package/dist/esm/components/dialogs/binding/binding-actions.d.ts.map +1 -0
  19. package/dist/esm/components/dialogs/binding/binding-actions.js +135 -0
  20. package/dist/esm/components/dialogs/binding/binding-actions.js.map +6 -0
  21. package/dist/esm/components/dialogs/binding/model.d.ts +1 -1
  22. package/dist/esm/components/dialogs/binding/model.d.ts.map +1 -1
  23. package/dist/esm/components/dialogs/binding/model.js +8 -3
  24. package/dist/esm/components/dialogs/binding/model.js.map +1 -1
  25. package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts +16 -24
  26. package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -1
  27. package/dist/esm/components/dialogs/binding/node-binding-dialog.js +210 -332
  28. package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
  29. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.d.ts +37 -0
  30. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.d.ts.map +1 -0
  31. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.js +387 -0
  32. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.js.map +6 -0
  33. package/dist/esm/pages/cluster-commands/clusters/binding-commands.d.ts +35 -0
  34. package/dist/esm/pages/cluster-commands/clusters/binding-commands.d.ts.map +1 -0
  35. package/dist/esm/pages/cluster-commands/clusters/binding-commands.js +254 -0
  36. package/dist/esm/pages/cluster-commands/clusters/binding-commands.js.map +6 -0
  37. package/dist/esm/pages/cluster-commands/index.d.ts +2 -0
  38. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  39. package/dist/esm/pages/cluster-commands/index.js +2 -0
  40. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  41. package/dist/esm/pages/components/node-details.d.ts +0 -1
  42. package/dist/esm/pages/components/node-details.d.ts.map +1 -1
  43. package/dist/esm/pages/components/node-details.js +2 -18
  44. package/dist/esm/pages/components/node-details.js.map +1 -1
  45. package/dist/esm/pages/components/server-details.js +3 -3
  46. package/dist/esm/pages/components/server-details.js.map +1 -1
  47. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
  48. package/dist/esm/pages/matter-cluster-view.js +2 -1
  49. package/dist/esm/pages/matter-cluster-view.js.map +1 -1
  50. package/dist/esm/pages/matter-endpoint-view.d.ts +3 -3
  51. package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
  52. package/dist/esm/pages/matter-endpoint-view.js +13 -10
  53. package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
  54. package/dist/esm/pages/matter-node-view.js +2 -2
  55. package/dist/esm/pages/matter-node-view.js.map +1 -1
  56. package/dist/esm/pages/network/network-utils.d.ts +1 -5
  57. package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
  58. package/dist/esm/pages/network/network-utils.js +1 -11
  59. package/dist/esm/pages/network/network-utils.js.map +1 -1
  60. package/dist/esm/util/access-control.d.ts +54 -0
  61. package/dist/esm/util/access-control.d.ts.map +1 -0
  62. package/dist/esm/util/access-control.js +100 -0
  63. package/dist/esm/util/access-control.js.map +6 -0
  64. package/dist/esm/util/binding.d.ts +54 -0
  65. package/dist/esm/util/binding.d.ts.map +1 -0
  66. package/dist/esm/util/binding.js +113 -0
  67. package/dist/esm/util/binding.js.map +6 -0
  68. package/dist/esm/util/endpoints.d.ts +9 -0
  69. package/dist/esm/util/endpoints.d.ts.map +1 -0
  70. package/dist/esm/util/endpoints.js +18 -0
  71. package/dist/esm/util/endpoints.js.map +6 -0
  72. package/dist/esm/util/node-name.d.ts +12 -0
  73. package/dist/esm/util/node-name.d.ts.map +1 -0
  74. package/dist/esm/util/node-name.js +15 -0
  75. package/dist/esm/util/node-name.js.map +6 -0
  76. package/dist/web/js/{attribute-write-dialog-W7xpCE2E.js → attribute-write-dialog-CqqdRniU.js} +1 -1
  77. package/dist/web/js/{command-invoke-dialog-BAqAAdJw.js → command-invoke-dialog-BuvBOrdC.js} +1 -1
  78. package/dist/web/js/{commission-node-dialog-BTzCGgdy.js → commission-node-dialog-nVZp3go0.js} +5 -5
  79. package/dist/web/js/{commission-node-existing-B2M2hyDh.js → commission-node-existing-Cx3Ahk2t.js} +2 -2
  80. package/dist/web/js/{commission-node-thread-djdz2dXW.js → commission-node-thread-CI8mWWPs.js} +2 -2
  81. package/dist/web/js/{commission-node-wifi-DxAYNS1A.js → commission-node-wifi-lUX4LK2R.js} +2 -2
  82. package/dist/web/js/{dialog-box-tHvPVxDN.js → dialog-box-bAdbnf-T.js} +1 -1
  83. package/dist/web/js/{fire_event-BleYfTLc.js → fire_event-tWhqPfdz.js} +1 -1
  84. package/dist/web/js/main.js +1 -1
  85. package/dist/web/js/{matter-dashboard-app-CVi_GDky.js → matter-dashboard-app-CONA_608.js} +1485 -390
  86. package/dist/web/js/node-acl-add-dialog-DlR-sF-b.js +320 -0
  87. package/dist/web/js/node-binding-dialog-DhnX_86M.js +267 -0
  88. package/dist/web/js/{node-label-dialog-DVZSjsXU.js → node-label-dialog-T3nPG-Qy.js} +1 -1
  89. package/dist/web/js/{settings-dialog-BG5MgZcO.js → settings-dialog-DrHzJtsi.js} +1 -1
  90. package/package.json +4 -4
  91. package/src/components/dialogs/acl/acl-actions.ts +71 -0
  92. package/src/components/dialogs/acl/model.ts +18 -17
  93. package/src/components/dialogs/acl/node-acl-add-dialog.ts +350 -0
  94. package/src/components/dialogs/acl/show-node-acl-add-dialog.ts +14 -0
  95. package/src/components/dialogs/binding/binding-actions.ts +201 -0
  96. package/src/components/dialogs/binding/model.ts +11 -4
  97. package/src/components/dialogs/binding/node-binding-dialog.ts +221 -399
  98. package/src/pages/cluster-commands/clusters/access-control-commands.ts +407 -0
  99. package/src/pages/cluster-commands/clusters/binding-commands.ts +273 -0
  100. package/src/pages/cluster-commands/index.ts +2 -0
  101. package/src/pages/components/node-details.ts +2 -21
  102. package/src/pages/components/server-details.ts +3 -3
  103. package/src/pages/matter-cluster-view.ts +4 -1
  104. package/src/pages/matter-endpoint-view.ts +16 -10
  105. package/src/pages/matter-node-view.ts +2 -2
  106. package/src/pages/network/network-utils.ts +1 -18
  107. package/src/util/access-control.ts +135 -0
  108. package/src/util/binding.ts +182 -0
  109. package/src/util/endpoints.ts +17 -0
  110. package/src/util/node-name.ts +18 -0
  111. package/dist/web/js/node-binding-dialog-B9IdqHrZ.js +0 -624
@@ -0,0 +1,320 @@
1
+ import { i, c, a as clientContext, n, r, b as i$1, P as Privilege, p as nodeIdKey, q as aclCapacity, s as showAlertDialog, u as targetServerClusters, v as clusters, x as AuthMode, y as addAclEntry, z as PRIVILEGE_NAMES, d as b, B as mdiClose, C as getDeviceName, h as handleAsync, g as t } from './matter-dashboard-app-CONA_608.js';
2
+ import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
+ import './main.js';
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __decorateClass = (decorators, target, key, kind) => {
8
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
9
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
10
+ if (kind && result) __defProp(target, key, result);
11
+ return result;
12
+ };
13
+ let NodeAclAddDialog = class extends i$1 {
14
+ constructor() {
15
+ super(...arguments);
16
+ this._privilege = Privilege.Operate;
17
+ this._subjects = new Array();
18
+ this._subjectInput = "";
19
+ this._targets = new Array();
20
+ this._targetEndpoint = "all";
21
+ this._targetCluster = "";
22
+ this._busy = false;
23
+ }
24
+ _knownNodes() {
25
+ return Object.values(this.client.nodes).sort((a, b) => {
26
+ const x = BigInt(a.node_id);
27
+ const y = BigInt(b.node_id);
28
+ return x < y ? -1 : x > y ? 1 : 0;
29
+ });
30
+ }
31
+ _addSubject(raw) {
32
+ const value = raw.trim();
33
+ if (!/^\d+$/.test(value)) return;
34
+ const id = BigInt(value);
35
+ const key = nodeIdKey(id);
36
+ if (this._subjects.some(s => nodeIdKey(s) === key)) return;
37
+ const max = aclCapacity(this.node).subjectsMax;
38
+ if (max > 0 && this._subjects.length >= max) {
39
+ void showAlertDialog({
40
+ title: "Limit reached",
41
+ text: `At most ${max} subjects per entry.`
42
+ });
43
+ return;
44
+ }
45
+ this._subjects = [...this._subjects, id];
46
+ this._subjectInput = "";
47
+ }
48
+ _removeSubject(key) {
49
+ this._subjects = this._subjects.filter(s => nodeIdKey(s) !== key);
50
+ }
51
+ _nodeEndpoints() {
52
+ const eps = /* @__PURE__ */new Set();
53
+ for (const key of Object.keys(this.node.attributes)) {
54
+ const m = /^(\d+)\/29\/0$/.exec(key);
55
+ if (m) eps.add(Number(m[1]));
56
+ }
57
+ return Array.from(eps).sort((a, b) => a - b);
58
+ }
59
+ _clusterOptions() {
60
+ if (this._targetEndpoint === "all") {
61
+ const all = /* @__PURE__ */new Set();
62
+ for (const ep of this._nodeEndpoints()) targetServerClusters(this.node, ep).forEach(c => all.add(c));
63
+ return Array.from(all).sort((a, b) => a - b);
64
+ }
65
+ return targetServerClusters(this.node, Number(this._targetEndpoint)).sort((a, b) => a - b);
66
+ }
67
+ _clusterLabel(id) {
68
+ var _clusters$id;
69
+ return `${((_clusters$id = clusters[id]) === null || _clusters$id === void 0 ? void 0 : _clusters$id.label) ?? "Cluster"} (0x${id.toString(16).padStart(2, "0").toUpperCase()})`;
70
+ }
71
+ _addTarget() {
72
+ const max = aclCapacity(this.node).targetsMax;
73
+ if (max > 0 && this._targets.length >= max) {
74
+ void showAlertDialog({
75
+ title: "Limit reached",
76
+ text: `At most ${max} targets per entry.`
77
+ });
78
+ return;
79
+ }
80
+ const endpoint = this._targetEndpoint === "all" || this._targetEndpoint === "" ? void 0 : Number(this._targetEndpoint);
81
+ const cluster = this._targetCluster === "all" || this._targetCluster === "" ? void 0 : Number(this._targetCluster);
82
+ if (endpoint === void 0 && cluster === void 0) {
83
+ void showAlertDialog({
84
+ title: "Validation error",
85
+ text: "Pick an endpoint and/or a cluster for the target."
86
+ });
87
+ return;
88
+ }
89
+ this._targets = [...this._targets, {
90
+ endpoint,
91
+ cluster,
92
+ deviceType: void 0
93
+ }];
94
+ this._targetEndpoint = "all";
95
+ this._targetCluster = "all";
96
+ }
97
+ _removeTarget(index) {
98
+ this._targets = this._targets.filter((_, i) => i !== index);
99
+ }
100
+ async _save() {
101
+ if (this._subjects.length === 0) {
102
+ await showAlertDialog({
103
+ title: "Validation error",
104
+ text: "Add at least one subject node."
105
+ });
106
+ return;
107
+ }
108
+ const entry = {
109
+ privilege: this._privilege,
110
+ authMode: AuthMode.Case,
111
+ subjects: this._subjects,
112
+ targets: this._targets.length ? this._targets : void 0,
113
+ fabricIndex: 0
114
+ };
115
+ this._busy = true;
116
+ try {
117
+ await addAclEntry(this.client, this.node.node_id, entry);
118
+ this._close();
119
+ } catch (err) {
120
+ await showAlertDialog({
121
+ title: "Failed to add entry",
122
+ text: err instanceof Error ? err.message : String(err)
123
+ });
124
+ } finally {
125
+ this._busy = false;
126
+ }
127
+ }
128
+ _close() {
129
+ this.shadowRoot.querySelector("md-dialog").close();
130
+ }
131
+ _handleClosed() {
132
+ var _this$parentNode;
133
+ (_this$parentNode = this.parentNode) === null || _this$parentNode === void 0 || _this$parentNode.removeChild(this);
134
+ }
135
+ render() {
136
+ return b`
137
+ <md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
138
+ <div slot="headline">Add ACL entry</div>
139
+ <div slot="content">
140
+ <div class="form">
141
+ <md-outlined-select
142
+ label="Privilege"
143
+ .value=${String(this._privilege)}
144
+ ?disabled=${this._busy}
145
+ @change=${e => this._privilege = Number(e.target.value)}
146
+ >
147
+ ${[Privilege.View, Privilege.Operate, Privilege.Manage, Privilege.Administer].map(p => b`<md-select-option value=${String(p)}
148
+ ><div slot="headline">${PRIVILEGE_NAMES[p]} · ${p}</div></md-select-option
149
+ >`)}
150
+ </md-outlined-select>
151
+ <div class="note">Auth mode: CASE (node). Group subjects are not supported yet.</div>
152
+
153
+ <div class="label">Subjects (nodes)</div>
154
+ <div class="chips">
155
+ ${this._subjects.length === 0 ? b`<span class="mut">none — add at least one</span>` : this._subjects.map(s => {
156
+ const known = this.client.nodes[nodeIdKey(s)];
157
+ return b`<span class="chip"
158
+ >${known ? getDeviceName(known) : "Node"} · ${s.toString()}
159
+ <ha-svg-icon
160
+ class="x"
161
+ .path=${mdiClose}
162
+ @click=${() => this._removeSubject(nodeIdKey(s))}
163
+ ></ha-svg-icon
164
+ ></span>`;
165
+ })}
166
+ </div>
167
+ <div class="row">
168
+ <md-outlined-select
169
+ label="Known nodes"
170
+ ?disabled=${this._busy}
171
+ @change=${e => {
172
+ const v = e.target.value;
173
+ if (v) this._addSubject(v);
174
+ }}
175
+ >
176
+ <md-select-option value=""><div slot="headline">— pick —</div></md-select-option>
177
+ ${this._knownNodes().map(n => b`<md-select-option value=${nodeIdKey(n.node_id)}
178
+ ><div slot="headline">
179
+ ${n.node_id.toString()} · ${getDeviceName(n)}
180
+ </div></md-select-option
181
+ >`)}
182
+ </md-outlined-select>
183
+ <md-outlined-text-field
184
+ label="or raw node id"
185
+ type="text"
186
+ pattern="[0-9]+"
187
+ .value=${this._subjectInput}
188
+ ?disabled=${this._busy}
189
+ @input=${e => this._subjectInput = e.target.value}
190
+ ></md-outlined-text-field>
191
+ <md-text-button ?disabled=${this._busy} @click=${() => this._addSubject(this._subjectInput)}
192
+ >Add</md-text-button
193
+ >
194
+ </div>
195
+
196
+ <div class="label">Targets (optional — none means whole node)</div>
197
+ <div class="chips">
198
+ ${this._targets.length === 0 ? b`<span class="mut">whole node</span>` : this._targets.map((t, i) => b`<span class="chip"
199
+ >${t.endpoint != null ? `EP ${t.endpoint}` : "All endpoints"}
200
+ ${t.cluster != null ? `\xB7 ${this._clusterLabel(t.cluster)}` : "\xB7 all clusters"}
201
+ <ha-svg-icon
202
+ class="x"
203
+ .path=${mdiClose}
204
+ @click=${() => this._removeTarget(i)}
205
+ ></ha-svg-icon
206
+ ></span>`)}
207
+ </div>
208
+ <div class="row">
209
+ <md-outlined-select
210
+ label="endpoint"
211
+ .value=${this._targetEndpoint}
212
+ ?disabled=${this._busy}
213
+ @change=${e => {
214
+ this._targetEndpoint = e.target.value;
215
+ this._targetCluster = "";
216
+ }}
217
+ >
218
+ <md-select-option value="all"
219
+ ><div slot="headline">All endpoints</div></md-select-option
220
+ >
221
+ ${this._nodeEndpoints().map(ep => b`<md-select-option value=${String(ep)}
222
+ ><div slot="headline">EP ${ep}</div></md-select-option
223
+ >`)}
224
+ </md-outlined-select>
225
+ <md-outlined-select
226
+ label="cluster"
227
+ .value=${this._targetCluster}
228
+ ?disabled=${this._busy}
229
+ @change=${e => this._targetCluster = e.target.value}
230
+ >
231
+ <md-select-option value="all"><div slot="headline">All clusters</div></md-select-option>
232
+ ${this._clusterOptions().map(c => b`<md-select-option value=${String(c)}
233
+ ><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
234
+ >`)}
235
+ </md-outlined-select>
236
+ <md-text-button ?disabled=${this._busy} @click=${() => this._addTarget()}
237
+ >Add target</md-text-button
238
+ >
239
+ </div>
240
+ </div>
241
+ </div>
242
+ <div slot="actions">
243
+ <md-text-button ?disabled=${this._busy} @click=${handleAsync(() => this._save())}
244
+ >Add</md-text-button
245
+ >
246
+ <md-text-button ?disabled=${this._busy} @click=${this._close}>Cancel</md-text-button>
247
+ </div>
248
+ </md-dialog>
249
+ `;
250
+ }
251
+ };
252
+ NodeAclAddDialog.styles = i`
253
+ .form {
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: 10px;
257
+ min-width: 360px;
258
+ }
259
+ .label {
260
+ font-size: 11px;
261
+ text-transform: uppercase;
262
+ letter-spacing: 0.04em;
263
+ opacity: 0.65;
264
+ margin-top: 6px;
265
+ }
266
+ .note {
267
+ font-size: 12px;
268
+ opacity: 0.7;
269
+ }
270
+ .row {
271
+ display: flex;
272
+ gap: 8px;
273
+ align-items: center;
274
+ flex-wrap: wrap;
275
+ }
276
+ .chips {
277
+ display: flex;
278
+ flex-wrap: wrap;
279
+ gap: 6px;
280
+ }
281
+ .chip {
282
+ display: inline-flex;
283
+ align-items: center;
284
+ gap: 4px;
285
+ padding: 3px 8px;
286
+ border-radius: 6px;
287
+ font-size: 12px;
288
+ background: var(--md-sys-color-surface-container-high);
289
+ color: var(--md-sys-color-on-surface);
290
+ }
291
+ .chip .x {
292
+ cursor: pointer;
293
+ --mdc-icon-size: 16px;
294
+ width: 16px;
295
+ height: 16px;
296
+ }
297
+ .mut {
298
+ opacity: 0.6;
299
+ font-size: 12px;
300
+ }
301
+ `;
302
+ __decorateClass([c({
303
+ context: clientContext,
304
+ subscribe: true
305
+ }), n({
306
+ attribute: false
307
+ })], NodeAclAddDialog.prototype, "client", 2);
308
+ __decorateClass([n({
309
+ attribute: false
310
+ })], NodeAclAddDialog.prototype, "node", 2);
311
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_privilege", 2);
312
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_subjects", 2);
313
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_subjectInput", 2);
314
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_targets", 2);
315
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_targetEndpoint", 2);
316
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_targetCluster", 2);
317
+ __decorateClass([r()], NodeAclAddDialog.prototype, "_busy", 2);
318
+ NodeAclAddDialog = __decorateClass([t("node-acl-add-dialog")], NodeAclAddDialog);
319
+
320
+ export { NodeAclAddDialog };
@@ -0,0 +1,267 @@
1
+ import { i, c, a as clientContext, n, r, b as i$1, p as nodeIdKey, v as clusters, s as showAlertDialog, E as targetAclCapacityForBinding, F as addBinding, G as bindableClusters, A, d as b, C as getDeviceName, H as getEndpointDeviceTypes, h as handleAsync, g as t } from './matter-dashboard-app-CONA_608.js';
2
+ import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
+ import './main.js';
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __decorateClass = (decorators, target, key, kind) => {
8
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
9
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
10
+ if (kind && result) __defProp(target, key, result);
11
+ return result;
12
+ };
13
+ const ALL_CLUSTERS = "all";
14
+ const CUSTOM_CLUSTER = "custom";
15
+ let NodeBindingDialog = class extends i$1 {
16
+ constructor() {
17
+ super(...arguments);
18
+ this._nodeIdInput = "";
19
+ this._endpointInput = "";
20
+ this._clusterSelection = ALL_CLUSTERS;
21
+ this._customClusterInput = "";
22
+ this._busy = false;
23
+ }
24
+ _knownNodes() {
25
+ return Object.values(this.client.nodes).filter(n => nodeIdKey(n.node_id) !== nodeIdKey(this.node.node_id)).sort((a, b) => {
26
+ const x = BigInt(a.node_id);
27
+ const y = BigInt(b.node_id);
28
+ return x < y ? -1 : x > y ? 1 : 0;
29
+ });
30
+ }
31
+ _resolveTarget() {
32
+ const raw = this._nodeIdInput.trim();
33
+ if (!/^\d+$/.test(raw)) return void 0;
34
+ return this.client.nodes[nodeIdKey(BigInt(raw))];
35
+ }
36
+ _nodeEndpoints(target) {
37
+ const eps = /* @__PURE__ */new Set();
38
+ for (const key of Object.keys(target.attributes)) {
39
+ const m = /^(\d+)\/29\/0$/.exec(key);
40
+ if (m) eps.add(Number(m[1]));
41
+ }
42
+ return Array.from(eps).sort((a, b) => a - b);
43
+ }
44
+ _clusterLabel(id) {
45
+ var _clusters$id;
46
+ return `${((_clusters$id = clusters[id]) === null || _clusters$id === void 0 ? void 0 : _clusters$id.label) ?? "Cluster"} (0x${id.toString(16).padStart(2, "0").toUpperCase()})`;
47
+ }
48
+ _onNodeSelect(e) {
49
+ const select = e.target;
50
+ this._nodeIdInput = select.value;
51
+ this._endpointInput = "";
52
+ this._clusterSelection = ALL_CLUSTERS;
53
+ }
54
+ async _add() {
55
+ const target = this._resolveTarget();
56
+ const rawNodeId = this._nodeIdInput.trim();
57
+ if (!/^\d+$/.test(rawNodeId) || BigInt(rawNodeId) <= 0n) {
58
+ await showAlertDialog({
59
+ title: "Validation error",
60
+ text: "Please enter a valid target node id."
61
+ });
62
+ return;
63
+ }
64
+ const targetNodeId = BigInt(rawNodeId);
65
+ const endpoint = parseInt(this._endpointInput, 10);
66
+ if (Number.isNaN(endpoint) || endpoint < 0 || endpoint > 65534) {
67
+ await showAlertDialog({
68
+ title: "Validation error",
69
+ text: "Please enter a valid target endpoint."
70
+ });
71
+ return;
72
+ }
73
+ let cluster;
74
+ if (this._clusterSelection === ALL_CLUSTERS) {
75
+ cluster = void 0;
76
+ } else if (this._clusterSelection === CUSTOM_CLUSTER) {
77
+ const c = parseInt(this._customClusterInput, 10);
78
+ if (Number.isNaN(c) || c < 0 || c > 32767) {
79
+ await showAlertDialog({
80
+ title: "Validation error",
81
+ text: "Please enter a valid cluster id."
82
+ });
83
+ return;
84
+ }
85
+ cluster = c;
86
+ } else {
87
+ cluster = parseInt(this._clusterSelection, 10);
88
+ }
89
+ if (target) {
90
+ const capacity = targetAclCapacityForBinding(target, this.node.node_id);
91
+ if (!capacity.canAdd) {
92
+ await showAlertDialog({
93
+ title: "Cannot add binding",
94
+ text: capacity.reason ?? "Target ACL is full."
95
+ });
96
+ return;
97
+ }
98
+ }
99
+ this._busy = true;
100
+ try {
101
+ await addBinding(this.client, this.node, this.endpoint, targetNodeId, endpoint, cluster);
102
+ this._close();
103
+ } catch (err) {
104
+ await showAlertDialog({
105
+ title: "Failed to add binding",
106
+ text: err instanceof Error ? err.message : String(err)
107
+ });
108
+ } finally {
109
+ this._busy = false;
110
+ }
111
+ }
112
+ _close() {
113
+ this.shadowRoot.querySelector("md-dialog").close();
114
+ }
115
+ _handleClosed() {
116
+ var _this$parentNode;
117
+ (_this$parentNode = this.parentNode) === null || _this$parentNode === void 0 || _this$parentNode.removeChild(this);
118
+ }
119
+ _renderClusterField(target, endpoint) {
120
+ const known = target !== void 0 && endpoint !== void 0 && !Number.isNaN(endpoint);
121
+ const split = known ? bindableClusters(this.node, this.endpoint, target, endpoint) : void 0;
122
+ const nonBindable = split !== void 0 && this._clusterSelection !== ALL_CLUSTERS && this._clusterSelection !== CUSTOM_CLUSTER && split.otherTarget.includes(parseInt(this._clusterSelection, 10));
123
+ return b`
124
+ <md-outlined-select
125
+ label="Cluster"
126
+ .value=${this._clusterSelection}
127
+ ?disabled=${this._busy}
128
+ @change=${e => this._clusterSelection = e.target.value}
129
+ >
130
+ <md-select-option value=${ALL_CLUSTERS}>
131
+ <div slot="headline">All clusters (any eligible)</div>
132
+ </md-select-option>
133
+ ${split && split.bindable.length ? b`<md-select-option disabled><div slot="headline">— Bindable —</div></md-select-option>
134
+ ${split.bindable.map(c => b`<md-select-option value=${String(c)}
135
+ ><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
136
+ >`)}` : A}
137
+ ${split && split.otherTarget.length ? b`<md-select-option disabled
138
+ ><div slot="headline">— Other target clusters (⚠) —</div></md-select-option
139
+ >
140
+ ${split.otherTarget.map(c => b`<md-select-option value=${String(c)}
141
+ ><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
142
+ >`)}` : A}
143
+ <md-select-option value=${CUSTOM_CLUSTER}
144
+ ><div slot="headline">Custom cluster id…</div></md-select-option
145
+ >
146
+ </md-outlined-select>
147
+ ${this._clusterSelection === CUSTOM_CLUSTER ? b`<md-outlined-text-field
148
+ label="cluster id"
149
+ type="number"
150
+ min="0"
151
+ max="32767"
152
+ .value=${this._customClusterInput}
153
+ ?disabled=${this._busy}
154
+ @input=${e => this._customClusterInput = e.target.value}
155
+ ></md-outlined-text-field>` : A}
156
+ ${nonBindable ? b`<div class="warn">
157
+ ⚠ This cluster is not a client cluster on the source endpoint. The binding may not function — it
158
+ will be added anyway on your request.
159
+ </div>` : A}
160
+ `;
161
+ }
162
+ render() {
163
+ if (!this.node) return A;
164
+ const target = this._resolveTarget();
165
+ const endpoint = this._endpointInput === "" ? void 0 : parseInt(this._endpointInput, 10);
166
+ const endpoints = target ? this._nodeEndpoints(target) : [];
167
+ return b`
168
+ <md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
169
+ <div slot="headline">Add binding</div>
170
+ <div slot="content">
171
+ <div class="form">
172
+ <md-outlined-select
173
+ label="Known nodes"
174
+ ?disabled=${this._busy}
175
+ .value=${target ? this._nodeIdInput : ""}
176
+ @change=${this._onNodeSelect}
177
+ >
178
+ <md-select-option value=""><div slot="headline">— pick a node —</div></md-select-option>
179
+ ${this._knownNodes().map(n => b`<md-select-option value=${nodeIdKey(n.node_id)}>
180
+ <div slot="headline">${n.node_id.toString()} · ${getDeviceName(n)}</div>
181
+ </md-select-option>`)}
182
+ </md-outlined-select>
183
+ <md-outlined-text-field
184
+ label="Target node id"
185
+ type="text"
186
+ pattern="[0-9]+"
187
+ supporting-text="required — pick above or enter a raw node id"
188
+ .value=${this._nodeIdInput}
189
+ ?disabled=${this._busy}
190
+ @input=${e => {
191
+ this._nodeIdInput = e.target.value;
192
+ this._endpointInput = "";
193
+ this._clusterSelection = ALL_CLUSTERS;
194
+ }}
195
+ ></md-outlined-text-field>
196
+
197
+ ${target ? b`<md-outlined-select
198
+ label="Target endpoint"
199
+ ?disabled=${this._busy}
200
+ .value=${this._endpointInput}
201
+ @change=${e => {
202
+ this._endpointInput = e.target.value;
203
+ this._clusterSelection = ALL_CLUSTERS;
204
+ }}
205
+ >
206
+ ${endpoints.map(ep => {
207
+ const dt = getEndpointDeviceTypes(target, ep)[0];
208
+ return b`<md-select-option value=${String(ep)}>
209
+ <div slot="headline">EP ${ep}${dt ? ` \xB7 ${dt.label}` : ""}</div>
210
+ </md-select-option>`;
211
+ })}
212
+ </md-outlined-select>` : b`<md-outlined-text-field
213
+ label="Target endpoint"
214
+ type="number"
215
+ min="0"
216
+ max="65534"
217
+ supporting-text=${this._nodeIdInput.trim() === "" ? "enter a node id first" : "unknown node \u2014 enter endpoint manually"}
218
+ ?disabled=${this._busy || this._nodeIdInput.trim() === ""}
219
+ .value=${this._endpointInput}
220
+ @input=${e => this._endpointInput = e.target.value}
221
+ ></md-outlined-text-field>`}
222
+ ${this._renderClusterField(target, endpoint)}
223
+ </div>
224
+ </div>
225
+ <div slot="actions">
226
+ <md-text-button ?disabled=${this._busy} @click=${handleAsync(() => this._add())}
227
+ >Add</md-text-button
228
+ >
229
+ <md-text-button ?disabled=${this._busy} @click=${this._close}>Cancel</md-text-button>
230
+ </div>
231
+ </md-dialog>
232
+ `;
233
+ }
234
+ };
235
+ NodeBindingDialog.styles = i`
236
+ .form {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: 12px;
240
+ min-width: 320px;
241
+ }
242
+ .warn {
243
+ font-size: 12px;
244
+ padding: 8px 10px;
245
+ border-radius: 7px;
246
+ background: var(--md-sys-color-error-container);
247
+ color: var(--md-sys-color-on-error-container);
248
+ }
249
+ `;
250
+ __decorateClass([c({
251
+ context: clientContext,
252
+ subscribe: true
253
+ }), n({
254
+ attribute: false
255
+ })], NodeBindingDialog.prototype, "client", 2);
256
+ __decorateClass([n()], NodeBindingDialog.prototype, "node", 2);
257
+ __decorateClass([n({
258
+ attribute: false
259
+ })], NodeBindingDialog.prototype, "endpoint", 2);
260
+ __decorateClass([r()], NodeBindingDialog.prototype, "_nodeIdInput", 2);
261
+ __decorateClass([r()], NodeBindingDialog.prototype, "_endpointInput", 2);
262
+ __decorateClass([r()], NodeBindingDialog.prototype, "_clusterSelection", 2);
263
+ __decorateClass([r()], NodeBindingDialog.prototype, "_customClusterInput", 2);
264
+ __decorateClass([r()], NodeBindingDialog.prototype, "_busy", 2);
265
+ NodeBindingDialog = __decorateClass([t("node-binding-dialog")], NodeBindingDialog);
266
+
267
+ export { NodeBindingDialog };
@@ -1,4 +1,4 @@
1
- import { r, n, g as t, b as i, M as MAX_NODE_LABEL_LENGTH, d as b, w as writeNodeLabel, s as showAlertDialog } from './matter-dashboard-app-CVi_GDky.js';
1
+ import { r, n, g as t, b as i, M as MAX_NODE_LABEL_LENGTH, d as b, w as writeNodeLabel, s as showAlertDialog } from './matter-dashboard-app-CONA_608.js';
2
2
  import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
3
  import './main.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiAccessPoint, k as mdiEyeOff, l as mdiEye } from './matter-dashboard-app-CVi_GDky.js';
1
+ import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiAccessPoint, k as mdiEyeOff, l as mdiEye } from './matter-dashboard-app-CONA_608.js';
2
2
  import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
3
3
  import './main.js';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matter-server/dashboard",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Dashboard for OHF Matter Server",
5
5
  "homepage": "https://github.com/matter-js/matterjs-server",
6
6
  "bugs": {
@@ -33,8 +33,8 @@
33
33
  "dependencies": {
34
34
  "@lit/context": "^1.1.6",
35
35
  "@material/web": "^2.4.1",
36
- "@matter-server/custom-clusters": "1.0.0",
37
- "@matter-server/ws-client": "1.0.0",
36
+ "@matter-server/custom-clusters": "1.1.0",
37
+ "@matter-server/ws-client": "1.1.0",
38
38
  "@mdi/js": "^7.4.47",
39
39
  "lit": "^3.3.3",
40
40
  "tslib": "^2.8.1",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@babel/preset-env": "^7.29.7",
45
- "@matter/main": "0.17.2",
45
+ "@matter/main": "0.17.3",
46
46
  "@rollup/plugin-babel": "^7.1.0",
47
47
  "@rollup/plugin-commonjs": "^29.0.3",
48
48
  "@rollup/plugin-json": "^6.1.0",