@matter-server/dashboard 0.6.2 → 0.6.3

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 (52) hide show
  1. package/dist/esm/pages/matter-network-view.d.ts +15 -0
  2. package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
  3. package/dist/esm/pages/matter-network-view.js +171 -1
  4. package/dist/esm/pages/matter-network-view.js.map +1 -1
  5. package/dist/esm/pages/network/base-network-graph.d.ts +4 -0
  6. package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
  7. package/dist/esm/pages/network/base-network-graph.js +9 -0
  8. package/dist/esm/pages/network/base-network-graph.js.map +1 -1
  9. package/dist/esm/pages/network/border-router-store.d.ts +20 -0
  10. package/dist/esm/pages/network/border-router-store.d.ts.map +1 -0
  11. package/dist/esm/pages/network/border-router-store.js +29 -0
  12. package/dist/esm/pages/network/border-router-store.js.map +6 -0
  13. package/dist/esm/pages/network/network-details.d.ts +40 -12
  14. package/dist/esm/pages/network/network-details.d.ts.map +1 -1
  15. package/dist/esm/pages/network/network-details.js +440 -112
  16. package/dist/esm/pages/network/network-details.js.map +1 -1
  17. package/dist/esm/pages/network/network-types.d.ts +76 -0
  18. package/dist/esm/pages/network/network-types.d.ts.map +1 -1
  19. package/dist/esm/pages/network/network-types.js.map +1 -1
  20. package/dist/esm/pages/network/network-utils.d.ts +89 -22
  21. package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
  22. package/dist/esm/pages/network/network-utils.js +233 -95
  23. package/dist/esm/pages/network/network-utils.js.map +1 -1
  24. package/dist/esm/pages/network/thread-graph.d.ts +68 -9
  25. package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
  26. package/dist/esm/pages/network/thread-graph.js +388 -50
  27. package/dist/esm/pages/network/thread-graph.js.map +2 -2
  28. package/dist/esm/util/device-icons.d.ts +6 -0
  29. package/dist/esm/util/device-icons.d.ts.map +1 -1
  30. package/dist/esm/util/device-icons.js +6 -0
  31. package/dist/esm/util/device-icons.js.map +1 -1
  32. package/dist/web/js/{attribute-write-dialog-g4B6BoRt.js → attribute-write-dialog-DlMTUiLK.js} +1 -1
  33. package/dist/web/js/{command-invoke-dialog-D6G704VK.js → command-invoke-dialog-DO3IyFcm.js} +1 -1
  34. package/dist/web/js/{commission-node-dialog-Bg3oo5ub.js → commission-node-dialog-CMSvCm0i.js} +4 -4
  35. package/dist/web/js/{commission-node-existing-DO3g1aQJ.js → commission-node-existing-D08jghFu.js} +2 -2
  36. package/dist/web/js/{commission-node-thread-DM432aH1.js → commission-node-thread-D5waY758.js} +2 -2
  37. package/dist/web/js/{commission-node-wifi-Bx40FXij.js → commission-node-wifi-ClBlCFTZ.js} +2 -2
  38. package/dist/web/js/{dialog-box-DjyfULWB.js → dialog-box-D9vS2SmP.js} +1 -1
  39. package/dist/web/js/{fire_event-BstgNPuh.js → fire_event-BPhROjTC.js} +1 -1
  40. package/dist/web/js/main.js +1 -1
  41. package/dist/web/js/{matter-dashboard-app-Cj88TtbZ.js → matter-dashboard-app-C9zTE5uH.js} +1359 -302
  42. package/dist/web/js/{node-binding-dialog-9yy2LE3_.js → node-binding-dialog-B5p-gbim.js} +1 -1
  43. package/dist/web/js/{settings-dialog-Cs2xMsXb.js → settings-dialog-BMFhom0W.js} +1 -1
  44. package/package.json +4 -4
  45. package/src/pages/matter-network-view.ts +185 -1
  46. package/src/pages/network/base-network-graph.ts +10 -0
  47. package/src/pages/network/border-router-store.ts +38 -0
  48. package/src/pages/network/network-details.ts +535 -140
  49. package/src/pages/network/network-types.ts +76 -0
  50. package/src/pages/network/network-utils.ts +390 -171
  51. package/src/pages/network/thread-graph.ts +532 -73
  52. package/src/util/device-icons.ts +13 -0
@@ -1,4 +1,4 @@
1
- import { q as i, P as c, Q as clientContext, n, d as e, i as i$1, R as showPromptDialog, J as showAlertDialog, A, j as b, K as handleAsync, t } from './matter-dashboard-app-Cj88TtbZ.js';
1
+ import { q as i, P as c, Q as clientContext, n, d as e, i as i$1, R as showPromptDialog, J as showAlertDialog, A, j as b, K as handleAsync, t } from './matter-dashboard-app-C9zTE5uH.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 { e, m as mixinDelegatesAria, a as mixinConstraintValidation, b as mixinFormAssociated, c as mixinElementInternals, i, _ as __decorate, n as n$1, d as e$1, f as isActivationClick, g as dispatchActivationClick, A, h as e$2, j as b, r as redispatchEvent, k as getFormValue, l as getFormState, o as createValidator, C as CheckboxValidator, p as getValidityAnchor, q as i$1, t, N as NavigableKeys, s as o, u as r, v as createAnimationSignal, L as ListController, w as getActiveItem, x as getLastActivatableItem, y as getFirstActivatableItem, z as o$1, E as EASING, V as Validator, D, B as mixinOnReportValidity, F as onReportValidity, G as u, H as i$2, I as fireAndForget, J as showAlertDialog, K as handleAsync, M as DevModeService } from './matter-dashboard-app-Cj88TtbZ.js';
1
+ import { e, m as mixinDelegatesAria, a as mixinConstraintValidation, b as mixinFormAssociated, c as mixinElementInternals, i, _ as __decorate, n as n$1, d as e$1, f as isActivationClick, g as dispatchActivationClick, A, h as e$2, j as b, r as redispatchEvent, k as getFormValue, l as getFormState, o as createValidator, C as CheckboxValidator, p as getValidityAnchor, q as i$1, t, N as NavigableKeys, s as o, u as r, v as createAnimationSignal, L as ListController, w as getActiveItem, x as getLastActivatableItem, y as getFirstActivatableItem, z as o$1, E as EASING, V as Validator, D, B as mixinOnReportValidity, F as onReportValidity, G as u, H as i$2, I as fireAndForget, J as showAlertDialog, K as handleAsync, M as DevModeService } from './matter-dashboard-app-C9zTE5uH.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": "0.6.2",
3
+ "version": "0.6.3",
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": "0.6.2",
37
- "@matter-server/ws-client": "0.6.2",
36
+ "@matter-server/custom-clusters": "0.6.3",
37
+ "@matter-server/ws-client": "0.6.3",
38
38
  "@mdi/js": "^7.4.47",
39
39
  "lit": "^3.3.2",
40
40
  "tslib": "^2.8.1",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@babel/preset-env": "^7.29.2",
45
- "@matter/main": "0.17.0-alpha.0-20260426-5bf9dab53",
45
+ "@matter/main": "0.17.0-alpha.0-20260429-8fd498888",
46
46
  "@rollup/plugin-babel": "^7.0.0",
47
47
  "@rollup/plugin-commonjs": "^29.0.2",
48
48
  "@rollup/plugin-json": "^6.1.0",
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { MatterClient, MatterNode } from "@matter-server/ws-client";
8
- import { mdiFitToScreen, mdiMagnifyMinus, mdiMagnifyPlus, mdiPause, mdiPlay } from "@mdi/js";
8
+ import { mdiEyeOff, 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";
@@ -13,6 +13,7 @@ import { reducedMotionStyles } from "../util/shared-styles.js";
13
13
  import "./components/footer";
14
14
  import "./components/header";
15
15
  import type { ActiveView } from "./components/header.js";
16
+ import { BorderRouterStore } from "./network/border-router-store.js";
16
17
  import "./network/device-panel";
17
18
  import "./network/network-details";
18
19
  import "./network/thread-graph";
@@ -26,6 +27,15 @@ declare global {
26
27
  }
27
28
  }
28
29
 
30
+ type HideOptionKey = "_hideOfflineNodes" | "_hideWeakSignalEdges" | "_hideMediumSignalEdges" | "_hideStrongSignalEdges";
31
+
32
+ const HIDE_OPTIONS: readonly { key: HideOptionKey; label: string }[] = [
33
+ { key: "_hideOfflineNodes", label: "Offline nodes" },
34
+ { key: "_hideWeakSignalEdges", label: "Weak signal edges" },
35
+ { key: "_hideMediumSignalEdges", label: "Medium signal edges" },
36
+ { key: "_hideStrongSignalEdges", label: "Strong signal edges" },
37
+ ];
38
+
29
39
  @customElement("matter-network-view")
30
40
  class MatterNetworkView extends LitElement {
31
41
  public client!: MatterClient;
@@ -61,6 +71,24 @@ class MatterNetworkView extends LitElement {
61
71
  @state()
62
72
  private _threadAddressSearchStatus: "idle" | "found" | "not-found" = "idle";
63
73
 
74
+ @state()
75
+ private _borderRouterStore = new BorderRouterStore();
76
+
77
+ @state()
78
+ private _showHideMenu = false;
79
+
80
+ @state()
81
+ private _hideOfflineNodes = false;
82
+
83
+ @state()
84
+ private _hideWeakSignalEdges = false;
85
+
86
+ @state()
87
+ private _hideMediumSignalEdges = false;
88
+
89
+ @state()
90
+ private _hideStrongSignalEdges = false;
91
+
64
92
  private _initialSelectionApplied = false;
65
93
  private _selectRetryTimer?: ReturnType<typeof setTimeout>;
66
94
 
@@ -78,9 +106,37 @@ class MatterNetworkView extends LitElement {
78
106
  }
79
107
  }
80
108
 
109
+ override firstUpdated(): void {
110
+ if (this.networkType === "thread") {
111
+ void this._refreshBorderRouters();
112
+ }
113
+ }
114
+
115
+ private async _refreshBorderRouters(): Promise<void> {
116
+ try {
117
+ await this._borderRouterStore.refresh(this.client);
118
+ this.requestUpdate();
119
+ } catch (err) {
120
+ console.warn("Failed to refresh border router store:", err);
121
+ }
122
+ }
123
+
124
+ private _handleConnectionsUpdated(): void {
125
+ // Skip the BR snapshot refresh when the user updated a WiFi node — the dialog fires
126
+ // the same event for both network types but BR data is Thread-only.
127
+ if (this.networkType !== "thread") return;
128
+ void this._refreshBorderRouters();
129
+ }
130
+
81
131
  override updated(changedProperties: Map<string, unknown>): void {
82
132
  super.updated(changedProperties);
83
133
 
134
+ // Lazily refresh the BR snapshot the first time the user switches into the Thread
135
+ // view, in case firstUpdated() fired while the WiFi view was active.
136
+ if (changedProperties.has("networkType") && this.networkType === "thread") {
137
+ void this._refreshBorderRouters();
138
+ }
139
+
84
140
  // After render, select the node in the graph
85
141
  if (!this._initialSelectionApplied && this.initialSelectedNodeId !== null) {
86
142
  this._initialSelectionApplied = true;
@@ -89,8 +145,16 @@ class MatterNetworkView extends LitElement {
89
145
  }
90
146
  }
91
147
 
148
+ override connectedCallback(): void {
149
+ super.connectedCallback();
150
+ document.addEventListener("click", this._documentClickHandler);
151
+ document.addEventListener("keydown", this._documentKeyHandler);
152
+ }
153
+
92
154
  override disconnectedCallback(): void {
93
155
  super.disconnectedCallback();
156
+ document.removeEventListener("click", this._documentClickHandler);
157
+ document.removeEventListener("keydown", this._documentKeyHandler);
94
158
  if (this._selectRetryTimer) {
95
159
  clearTimeout(this._selectRetryTimer);
96
160
  }
@@ -116,6 +180,12 @@ class MatterNetworkView extends LitElement {
116
180
 
117
181
  private _handleDetailsClose(): void {
118
182
  this._selectedNodeId = null;
183
+ // Tell the graph to deselect and clear highlights
184
+ if (this.networkType === "thread") {
185
+ this._threadGraph?.deselectAll();
186
+ } else {
187
+ this._wifiGraph?.deselectAll();
188
+ }
119
189
  }
120
190
 
121
191
  private _handleSelectNode(event: CustomEvent<{ nodeId: number | string }>): void {
@@ -153,6 +223,31 @@ class MatterNetworkView extends LitElement {
153
223
  }
154
224
  }
155
225
 
226
+ private _handleToggleHideMenu(): void {
227
+ this._showHideMenu = !this._showHideMenu;
228
+ }
229
+
230
+ private readonly _documentClickHandler = (event: MouseEvent): void => {
231
+ const path = event.composedPath();
232
+ if (!path.some(el => el instanceof HTMLElement && el.classList.contains("hide-menu-container"))) {
233
+ this._showHideMenu = false;
234
+ }
235
+ };
236
+
237
+ private readonly _documentKeyHandler = (event: KeyboardEvent): void => {
238
+ if (event.key === "Escape" && this._showHideMenu) {
239
+ this._showHideMenu = false;
240
+ }
241
+ };
242
+
243
+ private _handleToggleHideOption(option: HideOptionKey): void {
244
+ this[option] = !this[option];
245
+ }
246
+
247
+ private _isAnyHideOptionActive(): boolean {
248
+ return HIDE_OPTIONS.some(option => this[option.key]);
249
+ }
250
+
156
251
  private _handleTogglePhysics(): void {
157
252
  const newState = !this._physicsEnabled;
158
253
  this._physicsEnabled = newState;
@@ -231,6 +326,41 @@ class MatterNetworkView extends LitElement {
231
326
  <button class="control-button" @click=${this._handleFitToScreen} title="Fit to screen">
232
327
  <ha-svg-icon .path=${mdiFitToScreen}></ha-svg-icon>
233
328
  </button>
329
+ <div class="hide-menu-container">
330
+ <button
331
+ class="control-button ${this._isAnyHideOptionActive() ? "active" : ""}"
332
+ @click=${this._handleToggleHideMenu}
333
+ title="Hide options"
334
+ aria-haspopup="true"
335
+ aria-expanded=${this._showHideMenu}
336
+ aria-controls="hide-options-menu"
337
+ >
338
+ <ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
339
+ </button>
340
+ ${this._showHideMenu
341
+ ? html`
342
+ <div
343
+ id="hide-options-menu"
344
+ class="hide-dropdown"
345
+ role="group"
346
+ aria-label="Hide options"
347
+ >
348
+ ${HIDE_OPTIONS.map(
349
+ option => html`
350
+ <label class="hide-option">
351
+ <input
352
+ type="checkbox"
353
+ .checked=${this[option.key]}
354
+ @change=${() => this._handleToggleHideOption(option.key)}
355
+ />
356
+ <span>${option.label}</span>
357
+ </label>
358
+ `,
359
+ )}
360
+ </div>
361
+ `
362
+ : ""}
363
+ </div>
234
364
  <button
235
365
  class="control-button ${this._physicsEnabled ? "" : "active"}"
236
366
  @click=${this._handleTogglePhysics}
@@ -250,6 +380,11 @@ class MatterNetworkView extends LitElement {
250
380
  </div>`}
251
381
  <thread-graph
252
382
  .nodes=${this.nodes}
383
+ .borderRouters=${this._borderRouterStore.entries}
384
+ .hideOfflineNodes=${this._hideOfflineNodes}
385
+ .hideWeakSignalEdges=${this._hideWeakSignalEdges}
386
+ .hideMediumSignalEdges=${this._hideMediumSignalEdges}
387
+ .hideStrongSignalEdges=${this._hideStrongSignalEdges}
253
388
  @node-selected=${this._handleNodeSelected}
254
389
  @physics-changed=${this._handlePhysicsChanged}
255
390
  ></thread-graph>
@@ -294,6 +429,7 @@ class MatterNetworkView extends LitElement {
294
429
  const showSidebar = this._selectedNodeId !== null;
295
430
  const unknownDevices = this._threadGraph?.unknownDevicesMap ?? new Map();
296
431
  const wifiAccessPoints = this._wifiGraph?.wifiAccessPointsMap ?? new Map();
432
+ const threadEdgePairs = this._threadGraph?.edgePairs ?? new Map();
297
433
 
298
434
  return html`
299
435
  <dashboard-header
@@ -314,9 +450,16 @@ class MatterNetworkView extends LitElement {
314
450
  .selectedNodeId=${this._selectedNodeId}
315
451
  .nodes=${this.nodes}
316
452
  .unknownDevices=${unknownDevices}
453
+ .borderRouters=${this._borderRouterStore.entries}
317
454
  .wifiAccessPoints=${wifiAccessPoints}
455
+ .threadEdgePairs=${threadEdgePairs}
456
+ .hideOfflineNodes=${this._hideOfflineNodes}
457
+ .hideWeakSignalEdges=${this._hideWeakSignalEdges}
458
+ .hideMediumSignalEdges=${this._hideMediumSignalEdges}
459
+ .hideStrongSignalEdges=${this._hideStrongSignalEdges}
318
460
  @close=${this._handleDetailsClose}
319
461
  @select-node=${this._handleSelectNode}
462
+ @connections-updated=${this._handleConnectionsUpdated}
320
463
  ></network-details>
321
464
  </aside>
322
465
  </div>
@@ -480,6 +623,47 @@ class MatterNetworkView extends LitElement {
480
623
  --icon-primary-color: var(--md-sys-color-on-primary-container, #21005d);
481
624
  }
482
625
 
626
+ .hide-menu-container {
627
+ position: relative;
628
+ }
629
+
630
+ .hide-dropdown {
631
+ position: absolute;
632
+ top: calc(100% + 4px);
633
+ right: 0;
634
+ background: var(--md-sys-color-surface-container, #fff);
635
+ border: 1px solid var(--md-sys-color-outline-variant, #ccc);
636
+ border-radius: 4px;
637
+ box-shadow: var(--md-sys-elevation-level2, 0 2px 6px var(--md-sys-color-shadow, rgba(0, 0, 0, 0.15)));
638
+ min-width: 150px;
639
+ z-index: 100;
640
+ padding: 4px 0;
641
+ }
642
+
643
+ .hide-option {
644
+ display: flex;
645
+ align-items: center;
646
+ gap: 8px;
647
+ padding: 8px 12px;
648
+ cursor: pointer;
649
+ color: var(--md-sys-color-on-surface, #1c1b1f);
650
+ font-size: 0.9rem;
651
+ user-select: none;
652
+ }
653
+
654
+ .hide-option:hover {
655
+ background-color: var(--md-sys-color-surface-container-high, #e8e8e8);
656
+ }
657
+
658
+ .hide-option input[type="checkbox"] {
659
+ cursor: pointer;
660
+ margin: 0;
661
+ }
662
+
663
+ .hide-option span {
664
+ flex: 1;
665
+ }
666
+
483
667
  .graph-section thread-graph,
484
668
  .graph-section wifi-graph {
485
669
  flex: 1 1 0;
@@ -406,6 +406,16 @@ export abstract class BaseNetworkGraph extends LitElement {
406
406
  });
407
407
  }
408
408
 
409
+ /**
410
+ * Deselects all nodes, clears highlights, and restores default styling.
411
+ */
412
+ public deselectAll(): void {
413
+ if (!this._network) return;
414
+ this._selectedNodeId = null;
415
+ this._network.unselectAll();
416
+ this._clearHighlights();
417
+ }
418
+
409
419
  protected _dispatchNodeSelected(nodeId: number | string | null): void {
410
420
  this.dispatchEvent(
411
421
  new CustomEvent("node-selected", {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import type { BorderRouterEntry, MatterClient } from "@matter-server/ws-client";
8
+
9
+ /**
10
+ * Snapshot of mDNS-discovered Thread Border Routers.
11
+ *
12
+ * Refreshed on Thread-graph mount and on the reload-button click. The map is keyed by the
13
+ * uppercase 16-char xa hex so callers can join against neighbor-table extended addresses
14
+ * normalized to the same casing.
15
+ */
16
+ export class BorderRouterStore {
17
+ // Replaced (not mutated) on every refresh so Lit consumers passing this map as a
18
+ // @property() detect the snapshot change via the default `===` identity compare and
19
+ // re-render. Mutating in place would keep the same reference and silently skip updates.
20
+ #entries: ReadonlyMap<string, BorderRouterEntry> = new Map();
21
+
22
+ get entries(): ReadonlyMap<string, BorderRouterEntry> {
23
+ return this.#entries;
24
+ }
25
+
26
+ async refresh(client: MatterClient): Promise<void> {
27
+ const list = await client.sendCommand("get_thread_border_routers", 0, {});
28
+ const next = new Map<string, BorderRouterEntry>();
29
+ for (const entry of list) {
30
+ next.set(entry.extAddressHex.toUpperCase(), entry);
31
+ }
32
+ this.#entries = next;
33
+ }
34
+
35
+ reset(): void {
36
+ this.#entries = new Map();
37
+ }
38
+ }