@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
|
@@ -6,27 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
import "@material/web/button/text-button";
|
|
8
8
|
import "@material/web/dialog/dialog";
|
|
9
|
-
import
|
|
10
|
-
import "@material/web/
|
|
11
|
-
import "@material/web/list/list-item";
|
|
9
|
+
import "@material/web/select/outlined-select";
|
|
10
|
+
import "@material/web/select/select-option";
|
|
12
11
|
import "@material/web/textfield/outlined-text-field";
|
|
12
|
+
import { consume } from "@lit/context";
|
|
13
13
|
import type { MdDialog } from "@material/web/dialog/dialog.js";
|
|
14
|
-
import "
|
|
15
|
-
import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field.js";
|
|
16
|
-
import { AccessControlEntry, BindingTarget, MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
14
|
+
import { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
17
15
|
import { css, html, LitElement, nothing } from "lit";
|
|
18
|
-
import { customElement, property,
|
|
16
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
19
17
|
import { clientContext } from "../../../client/client-context.js";
|
|
18
|
+
import { clusters } from "../../../client/models/descriptions.js";
|
|
19
|
+
import { nodeIdKey } from "../../../util/access-control.js";
|
|
20
20
|
import { handleAsync } from "../../../util/async-handler.js";
|
|
21
|
-
import {
|
|
21
|
+
import { bindableClusters, targetAclCapacityForBinding } from "../../../util/binding.js";
|
|
22
|
+
import { getEndpointDeviceTypes } from "../../../util/endpoints.js";
|
|
23
|
+
import { getDeviceName } from "../../../util/node-name.js";
|
|
22
24
|
import { preventDefault } from "../../../util/prevent_default.js";
|
|
23
|
-
import { showAlertDialog
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from "../acl/model.js";
|
|
29
|
-
import { BindingEntryDataTransformer, BindingEntryStruct, InputType } from "./model.js";
|
|
25
|
+
import { showAlertDialog } from "../../dialog-box/show-dialog-box.js";
|
|
26
|
+
import { addBinding } from "./binding-actions.js";
|
|
27
|
+
|
|
28
|
+
const ALL_CLUSTERS = "all";
|
|
29
|
+
const CUSTOM_CLUSTER = "custom";
|
|
30
30
|
|
|
31
31
|
@customElement("node-binding-dialog")
|
|
32
32
|
export class NodeBindingDialog extends LitElement {
|
|
@@ -40,290 +40,95 @@ export class NodeBindingDialog extends LitElement {
|
|
|
40
40
|
@property({ attribute: false })
|
|
41
41
|
endpoint!: number;
|
|
42
42
|
|
|
43
|
-
@
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
@
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
private fetchACLEntry(targetNodeId: number | bigint): AccessControlEntryStruct[] {
|
|
59
|
-
const acl_cluster_raw = this.client.nodes[String(targetNodeId)]?.attributes["0/31/0"] as
|
|
60
|
-
| InputType[]
|
|
61
|
-
| undefined;
|
|
62
|
-
if (!acl_cluster_raw) return [];
|
|
63
|
-
return Object.values(acl_cluster_raw).map((value: InputType) =>
|
|
64
|
-
AccessControlEntryDataTransformer.transform(value),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private async deleteBindingHandler(index: number): Promise<void> {
|
|
69
|
-
const rawBindings = this.fetchBindingEntry();
|
|
70
|
-
try {
|
|
71
|
-
const targetNodeId = rawBindings[index].node;
|
|
72
|
-
const endpoint = rawBindings[index].endpoint;
|
|
73
|
-
if (targetNodeId === undefined || endpoint === undefined) return;
|
|
74
|
-
let aclCleanedUp = false;
|
|
75
|
-
try {
|
|
76
|
-
await this.removeNodeAtACLEntry(this.node!.node_id, endpoint, targetNodeId);
|
|
77
|
-
aclCleanedUp = true;
|
|
78
|
-
} catch (aclError) {
|
|
79
|
-
const errorMessage = aclError instanceof Error ? aclError.message : String(aclError);
|
|
80
|
-
const proceed = await showPromptDialog({
|
|
81
|
-
title: "ACL cleanup failed",
|
|
82
|
-
text:
|
|
83
|
-
`Could not clean up ACL on target node ${targetNodeId}: ${errorMessage}. ` +
|
|
84
|
-
"The target node may no longer exist or be unreachable. " +
|
|
85
|
-
"Do you want to remove the binding anyway? " +
|
|
86
|
-
"Note: The target device may retain an outdated ACL entry.",
|
|
87
|
-
confirmText: "Remove binding",
|
|
88
|
-
});
|
|
89
|
-
if (!proceed) return;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const updatedBindings = this.removeBindingAtIndex(rawBindings, index);
|
|
93
|
-
await this.syncBindingUpdates(updatedBindings, index);
|
|
94
|
-
} catch (bindingError) {
|
|
95
|
-
const errorMessage = bindingError instanceof Error ? bindingError.message : String(bindingError);
|
|
96
|
-
await showAlertDialog({
|
|
97
|
-
title: "Binding removal failed",
|
|
98
|
-
text:
|
|
99
|
-
`Failed to remove the binding: ${errorMessage}. ` +
|
|
100
|
-
(aclCleanedUp
|
|
101
|
-
? "The ACL on the target device was already updated. " +
|
|
102
|
-
"The binding and ACL may now be out of sync."
|
|
103
|
-
: "No changes were made."),
|
|
104
|
-
});
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
this.handleBindingDeletionError(error);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private async removeNodeAtACLEntry(
|
|
113
|
-
sourceNodeId: number | bigint,
|
|
114
|
-
sourceEndpoint: number,
|
|
115
|
-
targetNodeId: number | bigint,
|
|
116
|
-
): Promise<void> {
|
|
117
|
-
const aclEntries = this.fetchACLEntry(targetNodeId);
|
|
118
|
-
|
|
119
|
-
const updatedACLEntries = aclEntries
|
|
120
|
-
.map(entry => this.removeEntryAtACL(sourceNodeId, sourceEndpoint, entry))
|
|
121
|
-
.filter((entry): entry is AccessControlEntryStruct => entry !== undefined);
|
|
122
|
-
|
|
123
|
-
// Convert to API format (without fabricIndex - server handles it)
|
|
124
|
-
const apiEntries = updatedACLEntries.map(e => this.toAccessControlEntry(e));
|
|
125
|
-
await this.client.setACLEntry(targetNodeId, apiEntries);
|
|
43
|
+
@state() private _nodeIdInput = "";
|
|
44
|
+
@state() private _endpointInput = "";
|
|
45
|
+
@state() private _clusterSelection = ALL_CLUSTERS;
|
|
46
|
+
@state() private _customClusterInput = "";
|
|
47
|
+
@state() private _busy = false;
|
|
48
|
+
|
|
49
|
+
private _knownNodes(): MatterNode[] {
|
|
50
|
+
return Object.values(this.client.nodes)
|
|
51
|
+
.filter(n => nodeIdKey(n.node_id) !== nodeIdKey(this.node!.node_id))
|
|
52
|
+
.sort((a, b) => {
|
|
53
|
+
const x = BigInt(a.node_id);
|
|
54
|
+
const y = BigInt(b.node_id);
|
|
55
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
56
|
+
});
|
|
126
57
|
}
|
|
127
58
|
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
): AccessControlEntryStruct | undefined {
|
|
133
|
-
const hasSubject = entry.subjects.includes(nodeId);
|
|
134
|
-
if (!hasSubject) return entry;
|
|
135
|
-
|
|
136
|
-
const hasTarget = entry.targets!.filter(item => item.endpoint === sourceEndpoint);
|
|
137
|
-
return hasTarget.length > 0 ? undefined : entry;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private removeBindingAtIndex(bindings: BindingEntryStruct[], index: number): BindingEntryStruct[] {
|
|
141
|
-
return [...bindings.slice(0, index), ...bindings.slice(index + 1)];
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private async syncBindingUpdates(updatedBindings: BindingEntryStruct[], index: number): Promise<void> {
|
|
145
|
-
// Convert to API format (without fabricIndex - server handles it)
|
|
146
|
-
const apiBindings = updatedBindings.map(b => this.toBindingTarget(b));
|
|
147
|
-
await this.client.setNodeBinding(this.node!.node_id, this.endpoint, apiBindings);
|
|
148
|
-
|
|
149
|
-
const attributePath = `${this.endpoint}/30/0`;
|
|
150
|
-
const currentBindings = this.node!.attributes[attributePath] as BindingEntryStruct[] | undefined;
|
|
151
|
-
const updatedAttributes = {
|
|
152
|
-
...this.node!.attributes,
|
|
153
|
-
[attributePath]: currentBindings ? this.removeBindingAtIndex(currentBindings, index) : [],
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
this.node!.attributes = updatedAttributes;
|
|
157
|
-
this.requestUpdate();
|
|
59
|
+
private _resolveTarget(): MatterNode | undefined {
|
|
60
|
+
const raw = this._nodeIdInput.trim();
|
|
61
|
+
if (!/^\d+$/.test(raw)) return undefined;
|
|
62
|
+
return this.client.nodes[nodeIdKey(BigInt(raw))];
|
|
158
63
|
}
|
|
159
64
|
|
|
160
|
-
private
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
private async add_target_acl(
|
|
166
|
-
targetNodeId: number | bigint,
|
|
167
|
-
entry: AccessControlEntryStruct,
|
|
168
|
-
): Promise<MatterBatchResult> {
|
|
169
|
-
try {
|
|
170
|
-
// Fetch existing ACL entries and transform to local struct format
|
|
171
|
-
const rawEntries = this.client.nodes[String(targetNodeId)]?.attributes["0/31/0"] as InputType[] | undefined;
|
|
172
|
-
const entries = rawEntries
|
|
173
|
-
? Object.values(rawEntries).map(v => AccessControlEntryDataTransformer.transform(v))
|
|
174
|
-
: [];
|
|
175
|
-
entries.push(entry);
|
|
176
|
-
|
|
177
|
-
// Convert to API format (without fabricIndex - server handles it)
|
|
178
|
-
const apiEntries = entries.map(e => this.toAccessControlEntry(e));
|
|
179
|
-
const results = await this.client.setACLEntry(targetNodeId, apiEntries);
|
|
180
|
-
|
|
181
|
-
const batchResult = analyzeBatchResults(results);
|
|
182
|
-
if (batchResult.outcome !== "all_success") {
|
|
183
|
-
console.error(`Set ACL entry: ${batchResult.message}`);
|
|
184
|
-
}
|
|
185
|
-
return batchResult;
|
|
186
|
-
} catch (err) {
|
|
187
|
-
console.error("Add ACL error:", err);
|
|
188
|
-
return {
|
|
189
|
-
outcome: "all_failed",
|
|
190
|
-
successCount: 0,
|
|
191
|
-
failureCount: 1,
|
|
192
|
-
errorCounts: { 1: 1 },
|
|
193
|
-
message: `Exception: ${err instanceof Error ? err.message : String(err)}`,
|
|
194
|
-
};
|
|
65
|
+
private _nodeEndpoints(target: MatterNode): number[] {
|
|
66
|
+
const eps = new Set<number>();
|
|
67
|
+
for (const key of Object.keys(target.attributes)) {
|
|
68
|
+
const m = /^(\d+)\/29\/0$/.exec(key);
|
|
69
|
+
if (m) eps.add(Number(m[1]));
|
|
195
70
|
}
|
|
71
|
+
return Array.from(eps).sort((a, b) => a - b);
|
|
196
72
|
}
|
|
197
73
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
node: entry.node ?? null,
|
|
202
|
-
group: entry.group ?? null,
|
|
203
|
-
endpoint: entry.endpoint ?? null,
|
|
204
|
-
cluster: entry.cluster ?? null,
|
|
205
|
-
};
|
|
74
|
+
private _clusterLabel(id: number): string {
|
|
75
|
+
return `${clusters[id]?.label ?? "Cluster"} (0x${id.toString(16).padStart(2, "0").toUpperCase()})`;
|
|
206
76
|
}
|
|
207
77
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
subjects: entry.subjects ?? null,
|
|
214
|
-
targets:
|
|
215
|
-
entry.targets?.map(t => ({
|
|
216
|
-
cluster: t.cluster ?? null,
|
|
217
|
-
endpoint: t.endpoint ?? null,
|
|
218
|
-
device_type: t.deviceType ?? null,
|
|
219
|
-
})) ?? null,
|
|
220
|
-
};
|
|
78
|
+
private _onNodeSelect(e: Event) {
|
|
79
|
+
const select = e.target as HTMLSelectElement;
|
|
80
|
+
this._nodeIdInput = select.value;
|
|
81
|
+
this._endpointInput = "";
|
|
82
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
221
83
|
}
|
|
222
84
|
|
|
223
|
-
private async
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const apiBindings = bindings.map(b => this.toBindingTarget(b));
|
|
229
|
-
const results = await this.client.setNodeBinding(this.node!.node_id, endpoint, apiBindings);
|
|
230
|
-
|
|
231
|
-
const batchResult = analyzeBatchResults(results);
|
|
232
|
-
if (batchResult.outcome !== "all_success") {
|
|
233
|
-
console.error(`Set binding: ${batchResult.message}`);
|
|
234
|
-
}
|
|
235
|
-
return batchResult;
|
|
236
|
-
} catch (err) {
|
|
237
|
-
console.error("Add bindings error:", err);
|
|
238
|
-
return {
|
|
239
|
-
outcome: "all_failed",
|
|
240
|
-
successCount: 0,
|
|
241
|
-
failureCount: 1,
|
|
242
|
-
errorCounts: { 1: 1 },
|
|
243
|
-
message: `Exception: ${err instanceof Error ? err.message : String(err)}`,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async addBindingHandler() {
|
|
249
|
-
let targetNodeId: bigint | undefined;
|
|
250
|
-
const rawNodeId = this._targetNodeId.value?.trim();
|
|
251
|
-
if (rawNodeId) {
|
|
252
|
-
if (!/^\d+$/.test(rawNodeId)) {
|
|
253
|
-
showAlertDialog({ title: "Validation error", text: "Please enter a valid target node ID" });
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
targetNodeId = BigInt(rawNodeId);
|
|
257
|
-
}
|
|
258
|
-
const targetEndpoint = this._targetEndpoint.value ? parseInt(this._targetEndpoint.value, 10) : undefined;
|
|
259
|
-
const targetCluster = this._targetCluster.value ? parseInt(this._targetCluster.value, 10) : undefined;
|
|
260
|
-
|
|
261
|
-
if (targetNodeId === undefined || targetNodeId <= 0n) {
|
|
262
|
-
showAlertDialog({ title: "Validation error", text: "Please enter a valid target node ID" });
|
|
85
|
+
private async _add() {
|
|
86
|
+
const target = this._resolveTarget();
|
|
87
|
+
const rawNodeId = this._nodeIdInput.trim();
|
|
88
|
+
if (!/^\d+$/.test(rawNodeId) || BigInt(rawNodeId) <= 0n) {
|
|
89
|
+
await showAlertDialog({ title: "Validation error", text: "Please enter a valid target node id." });
|
|
263
90
|
return;
|
|
264
91
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
92
|
+
const targetNodeId = BigInt(rawNodeId);
|
|
93
|
+
const endpoint = parseInt(this._endpointInput, 10);
|
|
94
|
+
if (Number.isNaN(endpoint) || endpoint < 0 || endpoint > 0xfffe) {
|
|
95
|
+
await showAlertDialog({ title: "Validation error", text: "Please enter a valid target endpoint." });
|
|
268
96
|
return;
|
|
269
97
|
}
|
|
270
98
|
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
99
|
+
let cluster: number | undefined;
|
|
100
|
+
if (this._clusterSelection === ALL_CLUSTERS) {
|
|
101
|
+
cluster = undefined;
|
|
102
|
+
} else if (this._clusterSelection === CUSTOM_CLUSTER) {
|
|
103
|
+
const c = parseInt(this._customClusterInput, 10);
|
|
104
|
+
if (Number.isNaN(c) || c < 0 || c > 0x7fff) {
|
|
105
|
+
await showAlertDialog({ title: "Validation error", text: "Please enter a valid cluster id." });
|
|
276
106
|
return;
|
|
277
107
|
}
|
|
108
|
+
cluster = c;
|
|
109
|
+
} else {
|
|
110
|
+
cluster = parseInt(this._clusterSelection, 10);
|
|
278
111
|
}
|
|
279
112
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Note: fabricIndex is assigned by the server based on the device's fabric table
|
|
287
|
-
const acl_entry: AccessControlEntryStruct = {
|
|
288
|
-
privilege: 3,
|
|
289
|
-
authMode: 2,
|
|
290
|
-
subjects: [this.node!.node_id],
|
|
291
|
-
targets: [targets],
|
|
292
|
-
fabricIndex: 0, // Placeholder - server will use correct fabric index
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const aclResult = await this.add_target_acl(targetNodeId, acl_entry);
|
|
296
|
-
if (aclResult.outcome === "all_failed") {
|
|
297
|
-
showAlertDialog({ title: "Failed to add ACL entry", text: aclResult.message });
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (aclResult.outcome === "partial") {
|
|
301
|
-
showAlertDialog({ title: "ACL entry partially failed", text: aclResult.message });
|
|
302
|
-
// Continue with binding attempt since some ACL entries succeeded
|
|
113
|
+
if (target) {
|
|
114
|
+
const capacity = targetAclCapacityForBinding(target, this.node!.node_id);
|
|
115
|
+
if (!capacity.canAdd) {
|
|
116
|
+
await showAlertDialog({ title: "Cannot add binding", text: capacity.reason ?? "Target ACL is full." });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
303
119
|
}
|
|
304
120
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (bindingResult.outcome === "all_success") {
|
|
318
|
-
this._targetNodeId.value = "";
|
|
319
|
-
this._targetEndpoint.value = "";
|
|
320
|
-
this._targetCluster.value = "";
|
|
321
|
-
this.requestUpdate();
|
|
322
|
-
} else if (bindingResult.outcome === "partial") {
|
|
323
|
-
showAlertDialog({ title: "Binding partially failed", text: bindingResult.message });
|
|
324
|
-
this.requestUpdate(); // Update UI to show what succeeded
|
|
325
|
-
} else {
|
|
326
|
-
showAlertDialog({ title: "Failed to add binding", text: bindingResult.message });
|
|
121
|
+
this._busy = true;
|
|
122
|
+
try {
|
|
123
|
+
await addBinding(this.client, this.node!, this.endpoint, targetNodeId, endpoint, cluster);
|
|
124
|
+
this._close();
|
|
125
|
+
} catch (err) {
|
|
126
|
+
await showAlertDialog({
|
|
127
|
+
title: "Failed to add binding",
|
|
128
|
+
text: err instanceof Error ? err.message : String(err),
|
|
129
|
+
});
|
|
130
|
+
} finally {
|
|
131
|
+
this._busy = false;
|
|
327
132
|
}
|
|
328
133
|
}
|
|
329
134
|
|
|
@@ -332,149 +137,166 @@ export class NodeBindingDialog extends LitElement {
|
|
|
332
137
|
}
|
|
333
138
|
|
|
334
139
|
private _handleClosed() {
|
|
335
|
-
this.parentNode
|
|
140
|
+
this.parentNode?.removeChild(this);
|
|
336
141
|
}
|
|
337
142
|
|
|
338
|
-
private
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
143
|
+
private _renderClusterField(target: MatterNode | undefined, endpoint: number | undefined) {
|
|
144
|
+
const known = target !== undefined && endpoint !== undefined && !Number.isNaN(endpoint);
|
|
145
|
+
const split = known ? bindableClusters(this.node!, this.endpoint, target, endpoint) : undefined;
|
|
146
|
+
const nonBindable =
|
|
147
|
+
split !== undefined &&
|
|
148
|
+
this._clusterSelection !== ALL_CLUSTERS &&
|
|
149
|
+
this._clusterSelection !== CUSTOM_CLUSTER &&
|
|
150
|
+
split.otherTarget.includes(parseInt(this._clusterSelection, 10));
|
|
151
|
+
|
|
152
|
+
return html`
|
|
153
|
+
<md-outlined-select
|
|
154
|
+
label="Cluster"
|
|
155
|
+
.value=${this._clusterSelection}
|
|
156
|
+
?disabled=${this._busy}
|
|
157
|
+
@change=${(e: Event) => (this._clusterSelection = (e.target as HTMLSelectElement).value)}
|
|
158
|
+
>
|
|
159
|
+
<md-select-option value=${ALL_CLUSTERS}>
|
|
160
|
+
<div slot="headline">All clusters (any eligible)</div>
|
|
161
|
+
</md-select-option>
|
|
162
|
+
${split && split.bindable.length
|
|
163
|
+
? html`<md-select-option disabled><div slot="headline">— Bindable —</div></md-select-option>
|
|
164
|
+
${split.bindable.map(
|
|
165
|
+
c =>
|
|
166
|
+
html`<md-select-option value=${String(c)}
|
|
167
|
+
><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
|
|
168
|
+
>`,
|
|
169
|
+
)}`
|
|
170
|
+
: nothing}
|
|
171
|
+
${split && split.otherTarget.length
|
|
172
|
+
? html`<md-select-option disabled
|
|
173
|
+
><div slot="headline">— Other target clusters (⚠) —</div></md-select-option
|
|
174
|
+
>
|
|
175
|
+
${split.otherTarget.map(
|
|
176
|
+
c =>
|
|
177
|
+
html`<md-select-option value=${String(c)}
|
|
178
|
+
><div slot="headline">${this._clusterLabel(c)}</div></md-select-option
|
|
179
|
+
>`,
|
|
180
|
+
)}`
|
|
181
|
+
: nothing}
|
|
182
|
+
<md-select-option value=${CUSTOM_CLUSTER}
|
|
183
|
+
><div slot="headline">Custom cluster id…</div></md-select-option
|
|
184
|
+
>
|
|
185
|
+
</md-outlined-select>
|
|
186
|
+
${this._clusterSelection === CUSTOM_CLUSTER
|
|
187
|
+
? html`<md-outlined-text-field
|
|
188
|
+
label="cluster id"
|
|
189
|
+
type="number"
|
|
190
|
+
min="0"
|
|
191
|
+
max="32767"
|
|
192
|
+
.value=${this._customClusterInput}
|
|
193
|
+
?disabled=${this._busy}
|
|
194
|
+
@input=${(e: Event) => (this._customClusterInput = (e.target as HTMLInputElement).value)}
|
|
195
|
+
></md-outlined-text-field>`
|
|
196
|
+
: nothing}
|
|
197
|
+
${nonBindable
|
|
198
|
+
? html`<div class="warn">
|
|
199
|
+
⚠ This cluster is not a client cluster on the source endpoint. The binding may not function — it
|
|
200
|
+
will be added anyway on your request.
|
|
201
|
+
</div>`
|
|
202
|
+
: nothing}
|
|
203
|
+
`;
|
|
355
204
|
}
|
|
356
205
|
|
|
357
206
|
protected override render() {
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
207
|
+
if (!this.node) return nothing;
|
|
208
|
+
const target = this._resolveTarget();
|
|
209
|
+
const endpoint = this._endpointInput === "" ? undefined : parseInt(this._endpointInput, 10);
|
|
210
|
+
const endpoints = target ? this._nodeEndpoints(target) : [];
|
|
362
211
|
|
|
363
212
|
return html`
|
|
364
213
|
<md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
|
|
365
|
-
<div slot="headline">
|
|
366
|
-
<div>Binding</div>
|
|
367
|
-
</div>
|
|
214
|
+
<div slot="headline">Add binding</div>
|
|
368
215
|
<div slot="content">
|
|
369
|
-
<div>
|
|
370
|
-
<md-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
>delete</md-text-button
|
|
383
|
-
</div>
|
|
384
|
-
</md-list-item>
|
|
385
|
-
`,
|
|
216
|
+
<div class="form">
|
|
217
|
+
<md-outlined-select
|
|
218
|
+
label="Known nodes"
|
|
219
|
+
?disabled=${this._busy}
|
|
220
|
+
.value=${target ? this._nodeIdInput : ""}
|
|
221
|
+
@change=${this._onNodeSelect}
|
|
222
|
+
>
|
|
223
|
+
<md-select-option value=""><div slot="headline">— pick a node —</div></md-select-option>
|
|
224
|
+
${this._knownNodes().map(
|
|
225
|
+
n =>
|
|
226
|
+
html`<md-select-option value=${nodeIdKey(n.node_id)}>
|
|
227
|
+
<div slot="headline">${n.node_id.toString()} · ${getDeviceName(n)}</div>
|
|
228
|
+
</md-select-option>`,
|
|
386
229
|
)}
|
|
387
|
-
</md-
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
230
|
+
</md-outlined-select>
|
|
231
|
+
<md-outlined-text-field
|
|
232
|
+
label="Target node id"
|
|
233
|
+
type="text"
|
|
234
|
+
pattern="[0-9]+"
|
|
235
|
+
supporting-text="required — pick above or enter a raw node id"
|
|
236
|
+
.value=${this._nodeIdInput}
|
|
237
|
+
?disabled=${this._busy}
|
|
238
|
+
@input=${(e: Event) => {
|
|
239
|
+
this._nodeIdInput = (e.target as HTMLInputElement).value;
|
|
240
|
+
this._endpointInput = "";
|
|
241
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
242
|
+
}}
|
|
243
|
+
></md-outlined-text-field>
|
|
244
|
+
|
|
245
|
+
${target
|
|
246
|
+
? html`<md-outlined-select
|
|
247
|
+
label="Target endpoint"
|
|
248
|
+
?disabled=${this._busy}
|
|
249
|
+
.value=${this._endpointInput}
|
|
250
|
+
@change=${(e: Event) => {
|
|
251
|
+
this._endpointInput = (e.target as HTMLSelectElement).value;
|
|
252
|
+
this._clusterSelection = ALL_CLUSTERS;
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
${endpoints.map(ep => {
|
|
256
|
+
const dt = getEndpointDeviceTypes(target, ep)[0];
|
|
257
|
+
return html`<md-select-option value=${String(ep)}>
|
|
258
|
+
<div slot="headline">EP ${ep}${dt ? ` · ${dt.label}` : ""}</div>
|
|
259
|
+
</md-select-option>`;
|
|
260
|
+
})}
|
|
261
|
+
</md-outlined-select>`
|
|
262
|
+
: html`<md-outlined-text-field
|
|
263
|
+
label="Target endpoint"
|
|
264
|
+
type="number"
|
|
265
|
+
min="0"
|
|
266
|
+
max="65534"
|
|
267
|
+
supporting-text=${this._nodeIdInput.trim() === ""
|
|
268
|
+
? "enter a node id first"
|
|
269
|
+
: "unknown node — enter endpoint manually"}
|
|
270
|
+
?disabled=${this._busy || this._nodeIdInput.trim() === ""}
|
|
271
|
+
.value=${this._endpointInput}
|
|
272
|
+
@input=${(e: Event) => (this._endpointInput = (e.target as HTMLInputElement).value)}
|
|
273
|
+
></md-outlined-text-field>`}
|
|
274
|
+
${this._renderClusterField(target, endpoint)}
|
|
431
275
|
</div>
|
|
432
276
|
</div>
|
|
433
277
|
<div slot="actions">
|
|
434
|
-
<md-text-button @click=${handleAsync(() => this.
|
|
435
|
-
|
|
278
|
+
<md-text-button ?disabled=${this._busy} @click=${handleAsync(() => this._add())}
|
|
279
|
+
>Add</md-text-button
|
|
280
|
+
>
|
|
281
|
+
<md-text-button ?disabled=${this._busy} @click=${this._close}>Cancel</md-text-button>
|
|
436
282
|
</div>
|
|
437
283
|
</md-dialog>
|
|
438
284
|
`;
|
|
439
285
|
}
|
|
440
286
|
|
|
441
287
|
static override styles = css`
|
|
442
|
-
.
|
|
443
|
-
background: var(--md-sys-color-surface-container-high);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
.inline-group {
|
|
288
|
+
.form {
|
|
447
289
|
display: flex;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
position: relative;
|
|
452
|
-
margin: 8px;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
gap: 12px;
|
|
292
|
+
min-width: 320px;
|
|
453
293
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
.target-item {
|
|
461
|
-
display: inline-block;
|
|
462
|
-
padding: 16px 8px 8px 8px;
|
|
463
|
-
border-radius: 4px;
|
|
464
|
-
vertical-align: middle;
|
|
465
|
-
min-width: 80px;
|
|
466
|
-
text-align: center;
|
|
467
|
-
width: -webkit-fill-available;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
.group-label {
|
|
471
|
-
position: absolute;
|
|
472
|
-
left: 16px;
|
|
473
|
-
top: -12px;
|
|
474
|
-
background: var(--md-sys-color-primary);
|
|
475
|
-
color: var(--md-sys-color-on-primary);
|
|
476
|
-
padding: 4px 16px;
|
|
477
|
-
border-radius: 4px;
|
|
294
|
+
.warn {
|
|
295
|
+
font-size: 12px;
|
|
296
|
+
padding: 8px 10px;
|
|
297
|
+
border-radius: 7px;
|
|
298
|
+
background: var(--md-sys-color-error-container);
|
|
299
|
+
color: var(--md-sys-color-on-error-container);
|
|
478
300
|
}
|
|
479
301
|
`;
|
|
480
302
|
}
|