@matter-server/dashboard 0.3.4 → 0.3.5
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/binding/node-binding-dialog.d.ts.map +1 -1
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js +44 -17
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
- package/dist/esm/entrypoint/main.js +1 -1
- package/dist/esm/entrypoint/main.js.map +1 -1
- package/dist/esm/pages/matter-network-view.d.ts +5 -0
- package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-network-view.js +96 -13
- package/dist/esm/pages/matter-network-view.js.map +1 -1
- package/dist/esm/pages/network/base-network-graph.d.ts +34 -0
- package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/base-network-graph.js +106 -23
- package/dist/esm/pages/network/base-network-graph.js.map +1 -1
- package/dist/esm/util/matter-status.d.ts +49 -0
- package/dist/esm/util/matter-status.d.ts.map +1 -0
- package/dist/esm/util/matter-status.js +110 -0
- package/dist/esm/util/matter-status.js.map +6 -0
- package/dist/web/js/{commission-node-dialog-CcMuttYO.js → commission-node-dialog-DBugVQOl.js} +4 -4
- package/dist/web/js/{commission-node-existing-CqTRDMAr.js → commission-node-existing-ts2MN2bQ.js} +2 -2
- package/dist/web/js/{commission-node-thread-DgwtTVwK.js → commission-node-thread-CixfNuM3.js} +2 -2
- package/dist/web/js/{commission-node-wifi-XaN2SEnE.js → commission-node-wifi-CWC30EYn.js} +2 -2
- package/dist/web/js/{dialog-box-COpDD8i7.js → dialog-box-SeMuFiWx.js} +1 -1
- package/dist/web/js/{fire_event-mDYWi2sw.js → fire_event-B63oGTcK.js} +1 -1
- package/dist/web/js/{log-level-dialog-Bc32kZVw.js → log-level-dialog-J0gFiLLM.js} +1 -1
- package/dist/web/js/main.js +2 -2
- package/dist/web/js/{matter-dashboard-app-CrBHT4fT.js → matter-dashboard-app-D7YWrgXj.js} +222 -43
- package/dist/web/js/{node-binding-dialog-C8fqOJiB.js → node-binding-dialog-DSBwIJcQ.js} +146 -18
- package/package.json +6 -6
- package/src/components/dialogs/binding/node-binding-dialog.ts +45 -21
- package/src/entrypoint/main.ts +2 -0
- package/src/pages/matter-network-view.ts +102 -13
- package/src/pages/network/base-network-graph.ts +130 -30
- package/src/util/matter-status.ts +165 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { k as i, G as c, H as clientContext, n, a as e, i as i$1, A, j as b, F as handleAsync, t } from './matter-dashboard-app-
|
|
1
|
+
import { k as i, G as c, H as clientContext, n, a as e, i as i$1, A, j as b, F as handleAsync, t } from './matter-dashboard-app-D7YWrgXj.js';
|
|
2
2
|
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
|
@@ -116,6 +116,104 @@ class AccessControlEntryDataTransformer {
|
|
|
116
116
|
}
|
|
117
117
|
_staticBlock2();
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @license
|
|
121
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
122
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
123
|
+
*/
|
|
124
|
+
const MatterStatusNames = {
|
|
125
|
+
0: "Success",
|
|
126
|
+
1: "Failure",
|
|
127
|
+
125: "InvalidSubscription",
|
|
128
|
+
126: "UnsupportedAccess",
|
|
129
|
+
127: "UnsupportedEndpoint",
|
|
130
|
+
128: "InvalidAction",
|
|
131
|
+
129: "UnsupportedCommand",
|
|
132
|
+
133: "InvalidCommand",
|
|
133
|
+
134: "UnsupportedAttribute",
|
|
134
|
+
135: "ConstraintError",
|
|
135
|
+
136: "UnsupportedWrite",
|
|
136
|
+
137: "ResourceExhausted",
|
|
137
|
+
139: "NotFound",
|
|
138
|
+
140: "UnreportableAttribute",
|
|
139
|
+
141: "InvalidDataType",
|
|
140
|
+
143: "UnsupportedRead",
|
|
141
|
+
146: "DataVersionMismatch",
|
|
142
|
+
148: "Timeout",
|
|
143
|
+
155: "UnsupportedNode",
|
|
144
|
+
156: "Busy",
|
|
145
|
+
157: "AccessRestricted",
|
|
146
|
+
195: "UnsupportedCluster",
|
|
147
|
+
197: "NoUpstreamSubscription",
|
|
148
|
+
198: "NeedsTimedInteraction",
|
|
149
|
+
199: "UnsupportedEvent",
|
|
150
|
+
200: "PathsExhausted",
|
|
151
|
+
201: "TimedRequestMismatch",
|
|
152
|
+
202: "FailsafeRequired",
|
|
153
|
+
203: "InvalidInState",
|
|
154
|
+
204: "NoCommandResponse",
|
|
155
|
+
205: "TermsAndConditionsChanged",
|
|
156
|
+
206: "MaintenanceRequired",
|
|
157
|
+
207: "DynamicConstraintError",
|
|
158
|
+
208: "AlreadyExists",
|
|
159
|
+
209: "InvalidTransportType"
|
|
160
|
+
};
|
|
161
|
+
function getMatterStatusName(status) {
|
|
162
|
+
return MatterStatusNames[status] ?? `Unknown(${status})`;
|
|
163
|
+
}
|
|
164
|
+
function analyzeBatchResults(results) {
|
|
165
|
+
if (!results || results.length === 0) {
|
|
166
|
+
return {
|
|
167
|
+
outcome: "all_failed",
|
|
168
|
+
successCount: 0,
|
|
169
|
+
failureCount: 0,
|
|
170
|
+
errorCounts: {},
|
|
171
|
+
message: "No response/results returned"
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
let successCount = 0;
|
|
175
|
+
let failureCount = 0;
|
|
176
|
+
const errorCounts = {};
|
|
177
|
+
for (const result of results) {
|
|
178
|
+
if (result.status === 0) {
|
|
179
|
+
successCount++;
|
|
180
|
+
} else {
|
|
181
|
+
failureCount++;
|
|
182
|
+
errorCounts[result.status] = (errorCounts[result.status] ?? 0) + 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let outcome;
|
|
186
|
+
if (failureCount === 0) {
|
|
187
|
+
outcome = "all_success";
|
|
188
|
+
} else if (successCount === 0) {
|
|
189
|
+
outcome = "all_failed";
|
|
190
|
+
} else {
|
|
191
|
+
outcome = "partial";
|
|
192
|
+
}
|
|
193
|
+
const message = formatBatchMessage(outcome, successCount, failureCount, errorCounts);
|
|
194
|
+
return {
|
|
195
|
+
outcome,
|
|
196
|
+
successCount,
|
|
197
|
+
failureCount,
|
|
198
|
+
errorCounts,
|
|
199
|
+
message
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function formatBatchMessage(outcome, successCount, failureCount, errorCounts) {
|
|
203
|
+
const entryWord = count => count === 1 ? "entry" : "entries";
|
|
204
|
+
if (outcome === "all_success") {
|
|
205
|
+
return "Write successful";
|
|
206
|
+
}
|
|
207
|
+
const errorParts = Object.entries(errorCounts).map(([code, count]) => {
|
|
208
|
+
const statusCode = parseInt(code, 10);
|
|
209
|
+
return `${count}x ${getMatterStatusName(statusCode)} (${code})`;
|
|
210
|
+
}).join(", ");
|
|
211
|
+
if (outcome === "all_failed") {
|
|
212
|
+
return `All ${failureCount} ${entryWord(failureCount)} failed: ${errorParts}`;
|
|
213
|
+
}
|
|
214
|
+
return `Partial failure: ${successCount} ${entryWord(successCount)} succeeded, ${failureCount} failed (${errorParts}). Please verify.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
119
217
|
var __defProp = Object.defineProperty;
|
|
120
218
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
121
219
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
@@ -192,14 +290,23 @@ let NodeBindingDialog = class extends i$1 {
|
|
|
192
290
|
const entries = rawEntries ? Object.values(rawEntries).map(v => AccessControlEntryDataTransformer.transform(v)) : [];
|
|
193
291
|
entries.push(entry);
|
|
194
292
|
const apiEntries = entries.map(e => this.toAccessControlEntry(e));
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
293
|
+
const results = await this.client.setACLEntry(targetNodeId, apiEntries);
|
|
294
|
+
const batchResult = analyzeBatchResults(results);
|
|
295
|
+
if (batchResult.outcome !== "all_success") {
|
|
296
|
+
console.error(`Set ACL entry: ${batchResult.message}`);
|
|
198
297
|
}
|
|
199
|
-
return
|
|
298
|
+
return batchResult;
|
|
200
299
|
} catch (err) {
|
|
201
|
-
console.error("
|
|
202
|
-
return
|
|
300
|
+
console.error("Add ACL error:", err);
|
|
301
|
+
return {
|
|
302
|
+
outcome: "all_failed",
|
|
303
|
+
successCount: 0,
|
|
304
|
+
failureCount: 1,
|
|
305
|
+
errorCounts: {
|
|
306
|
+
1: 1
|
|
307
|
+
},
|
|
308
|
+
message: `Exception: ${err instanceof Error ? err.message : String(err)}`
|
|
309
|
+
};
|
|
203
310
|
}
|
|
204
311
|
}
|
|
205
312
|
/** Convert local BindingEntryStruct to API BindingTarget (without fabricIndex) */
|
|
@@ -230,14 +337,23 @@ let NodeBindingDialog = class extends i$1 {
|
|
|
230
337
|
bindings.push(bindingEntry);
|
|
231
338
|
try {
|
|
232
339
|
const apiBindings = bindings.map(b => this.toBindingTarget(b));
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
340
|
+
const results = await this.client.setNodeBinding(this.getNodeIdAsNumber(), endpoint, apiBindings);
|
|
341
|
+
const batchResult = analyzeBatchResults(results);
|
|
342
|
+
if (batchResult.outcome !== "all_success") {
|
|
343
|
+
console.error(`Set binding: ${batchResult.message}`);
|
|
236
344
|
}
|
|
237
|
-
return
|
|
345
|
+
return batchResult;
|
|
238
346
|
} catch (err) {
|
|
239
|
-
console.
|
|
240
|
-
return
|
|
347
|
+
console.error("Add bindings error:", err);
|
|
348
|
+
return {
|
|
349
|
+
outcome: "all_failed",
|
|
350
|
+
successCount: 0,
|
|
351
|
+
failureCount: 1,
|
|
352
|
+
errorCounts: {
|
|
353
|
+
1: 1
|
|
354
|
+
},
|
|
355
|
+
message: `Exception: ${err instanceof Error ? err.message : String(err)}`
|
|
356
|
+
};
|
|
241
357
|
}
|
|
242
358
|
}
|
|
243
359
|
async addBindingHandler() {
|
|
@@ -271,11 +387,16 @@ let NodeBindingDialog = class extends i$1 {
|
|
|
271
387
|
fabricIndex: 0
|
|
272
388
|
// Placeholder - server will use correct fabric index
|
|
273
389
|
};
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
alert(
|
|
390
|
+
const aclResult = await this.add_target_acl(targetNodeId, acl_entry);
|
|
391
|
+
if (aclResult.outcome === "all_failed") {
|
|
392
|
+
alert(`Failed to add ACL entry:
|
|
393
|
+
${aclResult.message}`);
|
|
277
394
|
return;
|
|
278
395
|
}
|
|
396
|
+
if (aclResult.outcome === "partial") {
|
|
397
|
+
alert(`ACL entry partially failed:
|
|
398
|
+
${aclResult.message}`);
|
|
399
|
+
}
|
|
279
400
|
const endpoint = this.endpoint;
|
|
280
401
|
const bindingEntry = {
|
|
281
402
|
node: targetNodeId,
|
|
@@ -285,12 +406,19 @@ let NodeBindingDialog = class extends i$1 {
|
|
|
285
406
|
fabricIndex: void 0
|
|
286
407
|
// Server will use correct fabric index
|
|
287
408
|
};
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
409
|
+
const bindingResult = await this.add_bindings(endpoint, bindingEntry);
|
|
410
|
+
if (bindingResult.outcome === "all_success") {
|
|
290
411
|
this._targetNodeId.value = "";
|
|
291
412
|
this._targetEndpoint.value = "";
|
|
292
413
|
this._targetCluster.value = "";
|
|
293
414
|
this.requestUpdate();
|
|
415
|
+
} else if (bindingResult.outcome === "partial") {
|
|
416
|
+
alert(`Binding partially failed:
|
|
417
|
+
${bindingResult.message}`);
|
|
418
|
+
this.requestUpdate();
|
|
419
|
+
} else {
|
|
420
|
+
alert(`Failed to add binding:
|
|
421
|
+
${bindingResult.message}`);
|
|
294
422
|
}
|
|
295
423
|
}
|
|
296
424
|
_close() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matter-server/dashboard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Dashboard for OHF Matter Server",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/matter-js/matterjs-server/issues"
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"bundle": "rollup -c"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@babel/preset-env": "^7.
|
|
26
|
-
"@matter/main": "0.16.9-alpha.0-
|
|
25
|
+
"@babel/preset-env": "^7.29.0",
|
|
26
|
+
"@matter/main": "0.16.9-alpha.0-20260204-fd5b6ed86",
|
|
27
27
|
"@rollup/plugin-babel": "^6.1.0",
|
|
28
28
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
29
29
|
"rollup-plugin-copy": "^3.5.0",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
32
32
|
"@rollup/plugin-terser": "^0.4.4",
|
|
33
33
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
34
|
-
"rollup": "^4.57.
|
|
34
|
+
"rollup": "^4.57.1",
|
|
35
35
|
"serve": "^14.2.5"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@lit/context": "^1.1.6",
|
|
39
39
|
"@material/web": "^2.4.1",
|
|
40
|
-
"@matter-server/ws-client": "0.3.
|
|
41
|
-
"@matter-server/custom-clusters": "0.3.
|
|
40
|
+
"@matter-server/ws-client": "0.3.5",
|
|
41
|
+
"@matter-server/custom-clusters": "0.3.5",
|
|
42
42
|
"@mdi/js": "^7.4.47",
|
|
43
43
|
"lit": "^3.3.2",
|
|
44
44
|
"tslib": "^2.8.1",
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
import { consume } from "@lit/context";
|
|
30
30
|
import { clientContext } from "../../../client/client-context.js";
|
|
31
|
+
import { analyzeBatchResults, type MatterBatchResult } from "../../../util/matter-status.js";
|
|
31
32
|
|
|
32
33
|
@customElement("node-binding-dialog")
|
|
33
34
|
export class NodeBindingDialog extends LitElement {
|
|
@@ -137,7 +138,7 @@ export class NodeBindingDialog extends LitElement {
|
|
|
137
138
|
console.error(`Binding deletion failed: ${errorMessage}`);
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
private async add_target_acl(targetNodeId: number, entry: AccessControlEntryStruct) {
|
|
141
|
+
private async add_target_acl(targetNodeId: number, entry: AccessControlEntryStruct): Promise<MatterBatchResult> {
|
|
141
142
|
try {
|
|
142
143
|
// Fetch existing ACL entries and transform to local struct format
|
|
143
144
|
const rawEntries = this.client.nodes[targetNodeId]?.attributes["0/31/0"] as InputType[] | undefined;
|
|
@@ -148,15 +149,22 @@ export class NodeBindingDialog extends LitElement {
|
|
|
148
149
|
|
|
149
150
|
// Convert to API format (without fabricIndex - server handles it)
|
|
150
151
|
const apiEntries = entries.map(e => this.toAccessControlEntry(e));
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const results = await this.client.setACLEntry(targetNodeId, apiEntries);
|
|
153
|
+
|
|
154
|
+
const batchResult = analyzeBatchResults(results);
|
|
155
|
+
if (batchResult.outcome !== "all_success") {
|
|
156
|
+
console.error(`Set ACL entry: ${batchResult.message}`);
|
|
155
157
|
}
|
|
156
|
-
return
|
|
158
|
+
return batchResult;
|
|
157
159
|
} catch (err) {
|
|
158
|
-
console.error("
|
|
159
|
-
return
|
|
160
|
+
console.error("Add ACL error:", err);
|
|
161
|
+
return {
|
|
162
|
+
outcome: "all_failed",
|
|
163
|
+
successCount: 0,
|
|
164
|
+
failureCount: 1,
|
|
165
|
+
errorCounts: { 1: 1 },
|
|
166
|
+
message: `Exception: ${err instanceof Error ? err.message : String(err)}`,
|
|
167
|
+
};
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
|
|
@@ -185,21 +193,28 @@ export class NodeBindingDialog extends LitElement {
|
|
|
185
193
|
};
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
private async add_bindings(endpoint: number, bindingEntry: BindingEntryStruct) {
|
|
196
|
+
private async add_bindings(endpoint: number, bindingEntry: BindingEntryStruct): Promise<MatterBatchResult> {
|
|
189
197
|
const bindings = this.fetchBindingEntry();
|
|
190
198
|
bindings.push(bindingEntry);
|
|
191
199
|
try {
|
|
192
200
|
// Convert to API format (without fabricIndex - server handles it)
|
|
193
201
|
const apiBindings = bindings.map(b => this.toBindingTarget(b));
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
202
|
+
const results = await this.client.setNodeBinding(this.getNodeIdAsNumber(), endpoint, apiBindings);
|
|
203
|
+
|
|
204
|
+
const batchResult = analyzeBatchResults(results);
|
|
205
|
+
if (batchResult.outcome !== "all_success") {
|
|
206
|
+
console.error(`Set binding: ${batchResult.message}`);
|
|
198
207
|
}
|
|
199
|
-
return
|
|
208
|
+
return batchResult;
|
|
200
209
|
} catch (err) {
|
|
201
|
-
console.
|
|
202
|
-
return
|
|
210
|
+
console.error("Add bindings error:", err);
|
|
211
|
+
return {
|
|
212
|
+
outcome: "all_failed",
|
|
213
|
+
successCount: 0,
|
|
214
|
+
failureCount: 1,
|
|
215
|
+
errorCounts: { 1: 1 },
|
|
216
|
+
message: `Exception: ${err instanceof Error ? err.message : String(err)}`,
|
|
217
|
+
};
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
|
|
@@ -243,11 +258,15 @@ export class NodeBindingDialog extends LitElement {
|
|
|
243
258
|
fabricIndex: 0, // Placeholder - server will use correct fabric index
|
|
244
259
|
};
|
|
245
260
|
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
alert(
|
|
261
|
+
const aclResult = await this.add_target_acl(targetNodeId, acl_entry);
|
|
262
|
+
if (aclResult.outcome === "all_failed") {
|
|
263
|
+
alert(`Failed to add ACL entry:\n${aclResult.message}`);
|
|
249
264
|
return;
|
|
250
265
|
}
|
|
266
|
+
if (aclResult.outcome === "partial") {
|
|
267
|
+
alert(`ACL entry partially failed:\n${aclResult.message}`);
|
|
268
|
+
// Continue with binding attempt since some ACL entries succeeded
|
|
269
|
+
}
|
|
251
270
|
|
|
252
271
|
const endpoint = this.endpoint;
|
|
253
272
|
// Note: fabricIndex is assigned by the server based on the device's fabric table
|
|
@@ -259,13 +278,18 @@ export class NodeBindingDialog extends LitElement {
|
|
|
259
278
|
fabricIndex: undefined, // Server will use correct fabric index
|
|
260
279
|
};
|
|
261
280
|
|
|
262
|
-
const
|
|
281
|
+
const bindingResult = await this.add_bindings(endpoint, bindingEntry);
|
|
263
282
|
|
|
264
|
-
if (
|
|
283
|
+
if (bindingResult.outcome === "all_success") {
|
|
265
284
|
this._targetNodeId.value = "";
|
|
266
285
|
this._targetEndpoint.value = "";
|
|
267
286
|
this._targetCluster.value = "";
|
|
268
287
|
this.requestUpdate();
|
|
288
|
+
} else if (bindingResult.outcome === "partial") {
|
|
289
|
+
alert(`Binding partially failed:\n${bindingResult.message}`);
|
|
290
|
+
this.requestUpdate(); // Update UI to show what succeeded
|
|
291
|
+
} else {
|
|
292
|
+
alert(`Failed to add binding:\n${bindingResult.message}`);
|
|
269
293
|
}
|
|
270
294
|
}
|
|
271
295
|
|
package/src/entrypoint/main.ts
CHANGED
|
@@ -13,7 +13,9 @@ async function main() {
|
|
|
13
13
|
let url = "";
|
|
14
14
|
|
|
15
15
|
// Detect if we're running in the (production) webserver included in the matter server or not.
|
|
16
|
+
// Priority: 1) Server-injected flag (for reverse proxy setups), 2) URL-based detection
|
|
16
17
|
const isProductionServer =
|
|
18
|
+
(window as unknown as { __MATTERJS_PRODUCTION_MODE__?: boolean }).__MATTERJS_PRODUCTION_MODE__ === true ||
|
|
17
19
|
location.origin.includes(":5580") ||
|
|
18
20
|
location.href.includes("hassio_ingress") ||
|
|
19
21
|
location.href.includes("/api/ingress/");
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
8
|
-
import { mdiFitToScreen } from "@mdi/js";
|
|
8
|
+
import { mdiFitToScreen, mdiMagnifyMinus, mdiMagnifyPlus, mdiPause, mdiPlay } from "@mdi/js";
|
|
9
9
|
import { css, html, LitElement } from "lit";
|
|
10
10
|
import { customElement, property, query, state } from "lit/decorators.js";
|
|
11
11
|
import "../components/ha-svg-icon";
|
|
@@ -51,6 +51,9 @@ class MatterNetworkView extends LitElement {
|
|
|
51
51
|
@state()
|
|
52
52
|
private _selectedNodeId: number | string | null = null;
|
|
53
53
|
|
|
54
|
+
@state()
|
|
55
|
+
private _physicsEnabled = true;
|
|
56
|
+
|
|
54
57
|
private _initialSelectionApplied = false;
|
|
55
58
|
private _selectRetryTimer?: ReturnType<typeof setTimeout>;
|
|
56
59
|
|
|
@@ -127,16 +130,67 @@ class MatterNetworkView extends LitElement {
|
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
private _handleZoomIn(): void {
|
|
134
|
+
if (this.networkType === "thread") {
|
|
135
|
+
this._threadGraph?.zoomIn();
|
|
136
|
+
} else {
|
|
137
|
+
this._wifiGraph?.zoomIn();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private _handleZoomOut(): void {
|
|
142
|
+
if (this.networkType === "thread") {
|
|
143
|
+
this._threadGraph?.zoomOut();
|
|
144
|
+
} else {
|
|
145
|
+
this._wifiGraph?.zoomOut();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private _handleTogglePhysics(): void {
|
|
150
|
+
const newState = !this._physicsEnabled;
|
|
151
|
+
this._physicsEnabled = newState;
|
|
152
|
+
// Keep both graphs in sync so switching between Thread and WiFi
|
|
153
|
+
// does not cause a mismatch between the UI button and graph state
|
|
154
|
+
this._threadGraph?.setPhysicsEnabled(newState);
|
|
155
|
+
this._wifiGraph?.setPhysicsEnabled(newState);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private _handlePhysicsChanged(event: CustomEvent<{ enabled: boolean }>): void {
|
|
159
|
+
// Update UI state when graph auto-freezes and keep both graphs in sync
|
|
160
|
+
this._physicsEnabled = event.detail.enabled;
|
|
161
|
+
this._threadGraph?.setPhysicsEnabled(event.detail.enabled);
|
|
162
|
+
this._wifiGraph?.setPhysicsEnabled(event.detail.enabled);
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
private _renderThreadView() {
|
|
131
166
|
return html`
|
|
132
167
|
<div class="graph-section">
|
|
133
168
|
<div class="graph-header">
|
|
134
169
|
<h2>Thread Network Mesh</h2>
|
|
135
|
-
<
|
|
136
|
-
<
|
|
137
|
-
|
|
170
|
+
<div class="graph-controls">
|
|
171
|
+
<button class="control-button" @click=${this._handleZoomIn} title="Zoom in">
|
|
172
|
+
<ha-svg-icon .path=${mdiMagnifyPlus}></ha-svg-icon>
|
|
173
|
+
</button>
|
|
174
|
+
<button class="control-button" @click=${this._handleZoomOut} title="Zoom out">
|
|
175
|
+
<ha-svg-icon .path=${mdiMagnifyMinus}></ha-svg-icon>
|
|
176
|
+
</button>
|
|
177
|
+
<button class="control-button" @click=${this._handleFitToScreen} title="Fit to screen">
|
|
178
|
+
<ha-svg-icon .path=${mdiFitToScreen}></ha-svg-icon>
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
class="control-button ${this._physicsEnabled ? "" : "active"}"
|
|
182
|
+
@click=${this._handleTogglePhysics}
|
|
183
|
+
title="${this._physicsEnabled ? "Freeze layout" : "Unfreeze layout"}"
|
|
184
|
+
>
|
|
185
|
+
<ha-svg-icon .path=${this._physicsEnabled ? mdiPause : mdiPlay}></ha-svg-icon>
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
138
188
|
</div>
|
|
139
|
-
<thread-graph
|
|
189
|
+
<thread-graph
|
|
190
|
+
.nodes=${this.nodes}
|
|
191
|
+
@node-selected=${this._handleNodeSelected}
|
|
192
|
+
@physics-changed=${this._handlePhysicsChanged}
|
|
193
|
+
></thread-graph>
|
|
140
194
|
</div>
|
|
141
195
|
`;
|
|
142
196
|
}
|
|
@@ -146,11 +200,30 @@ class MatterNetworkView extends LitElement {
|
|
|
146
200
|
<div class="graph-section">
|
|
147
201
|
<div class="graph-header">
|
|
148
202
|
<h2>WiFi Network</h2>
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
|
|
203
|
+
<div class="graph-controls">
|
|
204
|
+
<button class="control-button" @click=${this._handleZoomIn} title="Zoom in">
|
|
205
|
+
<ha-svg-icon .path=${mdiMagnifyPlus}></ha-svg-icon>
|
|
206
|
+
</button>
|
|
207
|
+
<button class="control-button" @click=${this._handleZoomOut} title="Zoom out">
|
|
208
|
+
<ha-svg-icon .path=${mdiMagnifyMinus}></ha-svg-icon>
|
|
209
|
+
</button>
|
|
210
|
+
<button class="control-button" @click=${this._handleFitToScreen} title="Fit to screen">
|
|
211
|
+
<ha-svg-icon .path=${mdiFitToScreen}></ha-svg-icon>
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
class="control-button ${this._physicsEnabled ? "" : "active"}"
|
|
215
|
+
@click=${this._handleTogglePhysics}
|
|
216
|
+
title="${this._physicsEnabled ? "Freeze layout" : "Unfreeze layout"}"
|
|
217
|
+
>
|
|
218
|
+
<ha-svg-icon .path=${this._physicsEnabled ? mdiPause : mdiPlay}></ha-svg-icon>
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
152
221
|
</div>
|
|
153
|
-
<wifi-graph
|
|
222
|
+
<wifi-graph
|
|
223
|
+
.nodes=${this.nodes}
|
|
224
|
+
@node-selected=${this._handleNodeSelected}
|
|
225
|
+
@physics-changed=${this._handlePhysicsChanged}
|
|
226
|
+
></wifi-graph>
|
|
154
227
|
</div>
|
|
155
228
|
`;
|
|
156
229
|
}
|
|
@@ -245,7 +318,12 @@ class MatterNetworkView extends LitElement {
|
|
|
245
318
|
color: var(--md-sys-color-on-background, #333);
|
|
246
319
|
}
|
|
247
320
|
|
|
248
|
-
.
|
|
321
|
+
.graph-controls {
|
|
322
|
+
display: flex;
|
|
323
|
+
gap: 4px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.control-button {
|
|
249
327
|
background: none;
|
|
250
328
|
border: 1px solid var(--md-sys-color-outline-variant, #ccc);
|
|
251
329
|
border-radius: 4px;
|
|
@@ -254,17 +332,28 @@ class MatterNetworkView extends LitElement {
|
|
|
254
332
|
display: flex;
|
|
255
333
|
align-items: center;
|
|
256
334
|
justify-content: center;
|
|
257
|
-
transition:
|
|
335
|
+
transition:
|
|
336
|
+
background-color 0.2s,
|
|
337
|
+
border-color 0.2s;
|
|
258
338
|
}
|
|
259
339
|
|
|
260
|
-
.
|
|
340
|
+
.control-button:hover {
|
|
261
341
|
background-color: var(--md-sys-color-surface-container-high, #e8e8e8);
|
|
262
342
|
}
|
|
263
343
|
|
|
264
|
-
.
|
|
344
|
+
.control-button.active {
|
|
345
|
+
background-color: var(--md-sys-color-primary-container, #e8def8);
|
|
346
|
+
border-color: var(--md-sys-color-primary, #6750a4);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.control-button ha-svg-icon {
|
|
265
350
|
--icon-primary-color: var(--md-sys-color-on-surface-variant, #666);
|
|
266
351
|
}
|
|
267
352
|
|
|
353
|
+
.control-button.active ha-svg-icon {
|
|
354
|
+
--icon-primary-color: var(--md-sys-color-on-primary-container, #21005d);
|
|
355
|
+
}
|
|
356
|
+
|
|
268
357
|
.graph-section thread-graph,
|
|
269
358
|
.graph-section wifi-graph {
|
|
270
359
|
flex: 1 1 0;
|