@matter-server/dashboard 0.3.2 → 0.3.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.
- package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +2 -2
- package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
- package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts +36 -0
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts.map +1 -0
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js +159 -0
- package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js.map +6 -0
- package/dist/esm/pages/cluster-commands/index.d.ts +1 -0
- package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
- package/dist/esm/pages/cluster-commands/index.js +1 -0
- package/dist/esm/pages/cluster-commands/index.js.map +1 -1
- package/dist/esm/pages/components/footer.d.ts.map +1 -1
- package/dist/esm/pages/components/footer.js +4 -7
- package/dist/esm/pages/components/footer.js.map +1 -1
- package/dist/esm/pages/components/header.d.ts +5 -0
- package/dist/esm/pages/components/header.d.ts.map +1 -1
- package/dist/esm/pages/components/header.js +75 -0
- package/dist/esm/pages/components/header.js.map +1 -1
- package/dist/esm/pages/components/node-details.js +1 -1
- package/dist/esm/pages/components/node-details.js.map +1 -1
- package/dist/esm/pages/components/server-details.d.ts.map +1 -1
- package/dist/esm/pages/components/server-details.js +0 -1
- package/dist/esm/pages/components/server-details.js.map +1 -1
- package/dist/esm/pages/matter-dashboard-app.d.ts +12 -0
- package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
- package/dist/esm/pages/matter-dashboard-app.js +84 -4
- package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
- package/dist/esm/pages/matter-network-view.d.ts +52 -0
- package/dist/esm/pages/matter-network-view.d.ts.map +1 -0
- package/dist/esm/pages/matter-network-view.js +309 -0
- package/dist/esm/pages/matter-network-view.js.map +6 -0
- package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-node-view.js +70 -1
- package/dist/esm/pages/matter-node-view.js.map +1 -1
- package/dist/esm/pages/matter-server-view.d.ts +4 -0
- package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-server-view.js +16 -1
- package/dist/esm/pages/matter-server-view.js.map +1 -1
- package/dist/esm/pages/network/base-network-graph.d.ts +74 -0
- package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -0
- package/dist/esm/pages/network/base-network-graph.js +403 -0
- package/dist/esm/pages/network/base-network-graph.js.map +6 -0
- package/dist/esm/pages/network/device-icons.d.ts +52 -0
- package/dist/esm/pages/network/device-icons.d.ts.map +1 -0
- package/dist/esm/pages/network/device-icons.js +197 -0
- package/dist/esm/pages/network/device-icons.js.map +6 -0
- package/dist/esm/pages/network/device-panel.d.ts +31 -0
- package/dist/esm/pages/network/device-panel.d.ts.map +1 -0
- package/dist/esm/pages/network/device-panel.js +183 -0
- package/dist/esm/pages/network/device-panel.js.map +6 -0
- package/dist/esm/pages/network/network-details.d.ts +47 -0
- package/dist/esm/pages/network/network-details.d.ts.map +1 -0
- package/dist/esm/pages/network/network-details.js +686 -0
- package/dist/esm/pages/network/network-details.js.map +6 -0
- package/dist/esm/pages/network/network-types.d.ts +153 -0
- package/dist/esm/pages/network/network-types.d.ts.map +1 -0
- package/dist/esm/pages/network/network-types.js +19 -0
- package/dist/esm/pages/network/network-types.js.map +6 -0
- package/dist/esm/pages/network/network-utils.d.ts +170 -0
- package/dist/esm/pages/network/network-utils.d.ts.map +1 -0
- package/dist/esm/pages/network/network-utils.js +472 -0
- package/dist/esm/pages/network/network-utils.js.map +6 -0
- package/dist/esm/pages/network/thread-graph.d.ts +27 -0
- package/dist/esm/pages/network/thread-graph.d.ts.map +1 -0
- package/dist/esm/pages/network/thread-graph.js +134 -0
- package/dist/esm/pages/network/thread-graph.js.map +6 -0
- package/dist/esm/pages/network/wifi-graph.d.ts +27 -0
- package/dist/esm/pages/network/wifi-graph.d.ts.map +1 -0
- package/dist/esm/pages/network/wifi-graph.js +167 -0
- package/dist/esm/pages/network/wifi-graph.js.map +6 -0
- package/dist/web/js/{commission-node-dialog-CBSDiqRW.js → commission-node-dialog-B1_khzZb.js} +5 -5
- package/dist/web/js/{commission-node-existing-TP6s8Tez.js → commission-node-existing-RpdajrwF.js} +2 -5
- package/dist/web/js/{commission-node-thread-DOB8pu6x.js → commission-node-thread-5f2itkTG.js} +2 -5
- package/dist/web/js/{commission-node-wifi-tzavmk1j.js → commission-node-wifi-DZ_pWqsa.js} +2 -5
- package/dist/web/js/{dialog-box-Dknil_Be.js → dialog-box-DEUxM4B1.js} +2 -2
- package/dist/web/js/{fire_event-DRpOSjJR.js → fire_event-BczBMT8E.js} +1 -1
- package/dist/web/js/{log-level-dialog-TXkma-7Z.js → log-level-dialog-Cr3PfX1X.js} +2 -3
- package/dist/web/js/main.js +1 -1
- package/dist/web/js/matter-dashboard-app-BuCe_Jxf.js +29990 -0
- package/dist/web/js/{node-binding-dialog-D52FCBFP.js → node-binding-dialog-DMiHNDLA.js} +2 -4
- package/dist/web/js/{prevent_default-BPgSQsuY.js → prevent_default-D4FX_PIh.js} +2 -42
- package/package.json +5 -4
- package/src/pages/cluster-commands/base-cluster-commands.ts +2 -2
- package/src/pages/cluster-commands/clusters/basic-information-commands.ts +171 -0
- package/src/pages/cluster-commands/index.ts +1 -0
- package/src/pages/components/footer.ts +4 -7
- package/src/pages/components/header.ts +81 -0
- package/src/pages/components/node-details.ts +2 -2
- package/src/pages/components/server-details.ts +0 -1
- package/src/pages/matter-dashboard-app.ts +105 -5
- package/src/pages/matter-network-view.ts +325 -0
- package/src/pages/matter-node-view.ts +75 -1
- package/src/pages/matter-server-view.ts +17 -1
- package/src/pages/network/base-network-graph.ts +463 -0
- package/src/pages/network/device-icons.ts +283 -0
- package/src/pages/network/device-panel.ts +180 -0
- package/src/pages/network/network-details.ts +750 -0
- package/src/pages/network/network-types.ts +161 -0
- package/src/pages/network/network-utils.ts +752 -0
- package/src/pages/network/thread-graph.ts +164 -0
- package/src/pages/network/wifi-graph.ts +192 -0
- package/dist/web/js/matter-dashboard-app-B7GUghkC.js +0 -17254
- package/dist/web/js/outlined-text-field-D1DyKQY-.js +0 -968
- package/dist/web/js/validator-C735j770.js +0 -1122
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { p as preventDefault } from './prevent_default-
|
|
3
|
-
import './outlined-text-field-D1DyKQY-.js';
|
|
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-BuCe_Jxf.js';
|
|
2
|
+
import { p as preventDefault } from './prevent_default-D4FX_PIh.js';
|
|
4
3
|
import './main.js';
|
|
5
|
-
import './validator-C735j770.js';
|
|
6
4
|
|
|
7
5
|
var _staticBlock$1;
|
|
8
6
|
/**
|
|
@@ -1,44 +1,4 @@
|
|
|
1
|
-
import { E as EASING, m as mixinDelegatesAria, i, _ as __decorate, n, a as e, r,
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @license
|
|
5
|
-
* Copyright 2021 Google LLC
|
|
6
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Re-dispatches an event from the provided element.
|
|
10
|
-
*
|
|
11
|
-
* This function is useful for forwarding non-composed events, such as `change`
|
|
12
|
-
* events.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* class MyInput extends LitElement {
|
|
16
|
-
* render() {
|
|
17
|
-
* return html`<input @change=${this.redispatchEvent}>`;
|
|
18
|
-
* }
|
|
19
|
-
*
|
|
20
|
-
* protected redispatchEvent(event: Event) {
|
|
21
|
-
* redispatchEvent(this, event);
|
|
22
|
-
* }
|
|
23
|
-
* }
|
|
24
|
-
*
|
|
25
|
-
* @param element The element to dispatch the event from.
|
|
26
|
-
* @param event The event to re-dispatch.
|
|
27
|
-
* @return Whether or not the event was dispatched (if cancelable).
|
|
28
|
-
*/
|
|
29
|
-
function redispatchEvent(element, event) {
|
|
30
|
-
// For bubbling events in SSR light DOM (or composed), stop their propagation
|
|
31
|
-
// and dispatch the copy.
|
|
32
|
-
if (event.bubbles && (!element.shadowRoot || event.composed)) {
|
|
33
|
-
event.stopPropagation();
|
|
34
|
-
}
|
|
35
|
-
const copy = Reflect.construct(event.constructor, [event.type, event]);
|
|
36
|
-
const dispatched = element.dispatchEvent(copy);
|
|
37
|
-
if (!dispatched) {
|
|
38
|
-
event.preventDefault();
|
|
39
|
-
}
|
|
40
|
-
return dispatched;
|
|
41
|
-
}
|
|
1
|
+
import { E as EASING, m as mixinDelegatesAria, i, _ as __decorate, n, a as e, r, j as b, A, f as e$1, w as redispatchEvent, k as i$1, t } from './matter-dashboard-app-BuCe_Jxf.js';
|
|
42
2
|
|
|
43
3
|
/**
|
|
44
4
|
* @license
|
|
@@ -811,4 +771,4 @@ MdDialog = __decorate([t('md-dialog')], MdDialog);
|
|
|
811
771
|
*/
|
|
812
772
|
const preventDefault = ev => ev.preventDefault();
|
|
813
773
|
|
|
814
|
-
export { preventDefault as p
|
|
774
|
+
export { preventDefault as p };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matter-server/dashboard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Dashboard for OHF Matter Server",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/matter-js/matterjs-server/issues"
|
|
@@ -37,11 +37,12 @@
|
|
|
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.3",
|
|
41
|
+
"@matter-server/custom-clusters": "0.3.3",
|
|
42
42
|
"@mdi/js": "^7.4.47",
|
|
43
43
|
"lit": "^3.3.2",
|
|
44
|
-
"tslib": "^2.8.1"
|
|
44
|
+
"tslib": "^2.8.1",
|
|
45
|
+
"vis-network": "^9.1.9"
|
|
45
46
|
},
|
|
46
47
|
"files": [
|
|
47
48
|
"dist/**/*",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
8
|
-
import { LitElement, css } from "lit";
|
|
8
|
+
import { LitElement, css, type CSSResultGroup } from "lit";
|
|
9
9
|
import { property } from "lit/decorators.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -39,7 +39,7 @@ export abstract class BaseClusterCommands extends LitElement {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
static override styles = css`
|
|
42
|
+
static override styles: CSSResultGroup = css`
|
|
43
43
|
:host {
|
|
44
44
|
display: block;
|
|
45
45
|
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import "@material/web/button/filled-button";
|
|
8
|
+
import "@material/web/textfield/outlined-text-field";
|
|
9
|
+
import { css, html, nothing, type CSSResultGroup } from "lit";
|
|
10
|
+
import { customElement, state } from "lit/decorators.js";
|
|
11
|
+
import { showAlertDialog } from "../../../components/dialog-box/show-dialog-box.js";
|
|
12
|
+
import { handleAsync } from "../../../util/async-handler.js";
|
|
13
|
+
import { BaseClusterCommands } from "../base-cluster-commands.js";
|
|
14
|
+
import { registerClusterCommands } from "../registry.js";
|
|
15
|
+
|
|
16
|
+
const CLUSTER_ID = 0x28; // BasicInformation cluster (40 decimal)
|
|
17
|
+
const NODE_LABEL_ATTRIBUTE_ID = 5;
|
|
18
|
+
const MAX_NODE_LABEL_LENGTH = 32;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Command panel for BasicInformation cluster (ID: 0x28 / 40).
|
|
22
|
+
* Provides ability to edit the NodeLabel attribute.
|
|
23
|
+
*/
|
|
24
|
+
@customElement("basic-information-cluster-commands")
|
|
25
|
+
export class BasicInformationClusterCommands extends BaseClusterCommands {
|
|
26
|
+
@state()
|
|
27
|
+
private _nodeLabel: string = "";
|
|
28
|
+
|
|
29
|
+
@state()
|
|
30
|
+
private _saving: boolean = false;
|
|
31
|
+
|
|
32
|
+
override updated(changedProperties: Map<string, unknown>) {
|
|
33
|
+
super.updated(changedProperties);
|
|
34
|
+
// Load node label when node property is first set
|
|
35
|
+
if (changedProperties.has("node") && this.node) {
|
|
36
|
+
this._loadCurrentNodeLabel();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load the current NodeLabel from the node's cached attributes.
|
|
42
|
+
*/
|
|
43
|
+
private _loadCurrentNodeLabel() {
|
|
44
|
+
if (!this.node) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// First check the direct nodeLabel property on the node
|
|
49
|
+
if (this.node.nodeLabel) {
|
|
50
|
+
this._nodeLabel = this.node.nodeLabel;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback to attribute path: endpoint/cluster/attribute = 0/40/5
|
|
55
|
+
// BasicInformation cluster is always on endpoint 0 per Matter specification
|
|
56
|
+
const attributePath = `0/${CLUSTER_ID}/${NODE_LABEL_ATTRIBUTE_ID}`;
|
|
57
|
+
const currentValue = this.node.attributes[attributePath];
|
|
58
|
+
if (typeof currentValue === "string") {
|
|
59
|
+
this._nodeLabel = currentValue;
|
|
60
|
+
} else {
|
|
61
|
+
this._nodeLabel = "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if the node is available (not offline).
|
|
67
|
+
*/
|
|
68
|
+
private get _isNodeAvailable(): boolean {
|
|
69
|
+
return this.node.available;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override render() {
|
|
73
|
+
return html`
|
|
74
|
+
<details class="command-panel">
|
|
75
|
+
<summary>Node Label</summary>
|
|
76
|
+
<div class="command-content">
|
|
77
|
+
${!this._isNodeAvailable
|
|
78
|
+
? html`<div class="offline-warning">Node is offline - cannot edit label</div>`
|
|
79
|
+
: nothing}
|
|
80
|
+
<div class="command-row">
|
|
81
|
+
<md-outlined-text-field
|
|
82
|
+
label="Node Label"
|
|
83
|
+
.value=${this._nodeLabel}
|
|
84
|
+
@input=${this._handleInput}
|
|
85
|
+
maxlength=${MAX_NODE_LABEL_LENGTH}
|
|
86
|
+
?disabled=${!this._isNodeAvailable || this._saving}
|
|
87
|
+
supporting-text="Max ${MAX_NODE_LABEL_LENGTH} characters"
|
|
88
|
+
></md-outlined-text-field>
|
|
89
|
+
<md-filled-button
|
|
90
|
+
@click=${handleAsync(() => this._handleSave())}
|
|
91
|
+
?disabled=${!this._isNodeAvailable || this._saving}
|
|
92
|
+
>
|
|
93
|
+
${this._saving ? "Saving..." : "Save"}
|
|
94
|
+
</md-filled-button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</details>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _handleInput(e: Event) {
|
|
102
|
+
const input = e.target as HTMLInputElement;
|
|
103
|
+
this._nodeLabel = input.value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async _handleSave() {
|
|
107
|
+
if (!this._isNodeAvailable) {
|
|
108
|
+
showAlertDialog({
|
|
109
|
+
title: "Cannot Save",
|
|
110
|
+
text: "Node is offline. Please wait until the node is available.",
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._saving = true;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// BasicInformation cluster is always on endpoint 0 per Matter specification
|
|
119
|
+
const attributePath = `0/${CLUSTER_ID}/${NODE_LABEL_ATTRIBUTE_ID}`;
|
|
120
|
+
await this.client.writeAttribute(this.node.node_id, attributePath, this._nodeLabel);
|
|
121
|
+
|
|
122
|
+
showAlertDialog({
|
|
123
|
+
title: "Success",
|
|
124
|
+
text: `Node label set to "${this._nodeLabel}"`,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
128
|
+
showAlertDialog({
|
|
129
|
+
title: "Failed to set node label",
|
|
130
|
+
text: errorMessage,
|
|
131
|
+
});
|
|
132
|
+
} finally {
|
|
133
|
+
this._saving = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static override styles: CSSResultGroup = [
|
|
138
|
+
BaseClusterCommands.styles,
|
|
139
|
+
css`
|
|
140
|
+
.command-row {
|
|
141
|
+
align-items: flex-start;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
md-outlined-text-field {
|
|
145
|
+
width: 36ch; /* 32 chars + some padding for field chrome */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
md-filled-button {
|
|
149
|
+
margin-top: 8px; /* Align with text field input area, accounting for label */
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.offline-warning {
|
|
153
|
+
color: var(--danger-color, #d32f2f);
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
margin-bottom: 12px;
|
|
156
|
+
padding: 8px 12px;
|
|
157
|
+
background-color: var(--md-sys-color-error-container, #fdecea);
|
|
158
|
+
border-radius: 4px;
|
|
159
|
+
}
|
|
160
|
+
`,
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Register this component for cluster ID 0x28 (40 decimal)
|
|
165
|
+
registerClusterCommands(CLUSTER_ID, "basic-information-cluster-commands");
|
|
166
|
+
|
|
167
|
+
declare global {
|
|
168
|
+
interface HTMLElementTagNameMap {
|
|
169
|
+
"basic-information-cluster-commands": BasicInformationClusterCommands;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -16,5 +16,6 @@ export { getClusterCommandsTag, hasClusterCommands, registerClusterCommands } fr
|
|
|
16
16
|
export { BaseClusterCommands } from "./base-cluster-commands.js";
|
|
17
17
|
|
|
18
18
|
// Cluster command components (auto-register on import)
|
|
19
|
+
import "./clusters/basic-information-commands.js";
|
|
19
20
|
import "./clusters/level-control-commands.js";
|
|
20
21
|
import "./clusters/on-off-commands.js";
|
|
@@ -22,14 +22,11 @@ export class DashboardFooter extends LitElement {
|
|
|
22
22
|
|
|
23
23
|
static override styles = css`
|
|
24
24
|
.footer {
|
|
25
|
-
padding: 16px;
|
|
25
|
+
padding: 4px 16px;
|
|
26
26
|
text-align: center;
|
|
27
|
-
font-size: 0.
|
|
28
|
-
color: var(--md-sys-color-on-surface);
|
|
29
|
-
|
|
30
|
-
flex-direction: column;
|
|
31
|
-
position: relative;
|
|
32
|
-
clear: both;
|
|
27
|
+
font-size: 0.75em;
|
|
28
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
29
|
+
flex-shrink: 0;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
.footer a {
|
|
@@ -23,10 +23,15 @@ interface HeaderAction {
|
|
|
23
23
|
action: void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type ActiveView = "nodes" | "thread" | "wifi";
|
|
27
|
+
|
|
26
28
|
@customElement("dashboard-header")
|
|
27
29
|
export class DashboardHeader extends LitElement {
|
|
28
30
|
@property() public backButton?: string;
|
|
29
31
|
@property() public actions?: HeaderAction[];
|
|
32
|
+
@property() public activeView?: ActiveView;
|
|
33
|
+
@property({ type: Boolean }) public hasThreadDevices?: boolean;
|
|
34
|
+
@property({ type: Boolean }) public hasWifiDevices?: boolean;
|
|
30
35
|
|
|
31
36
|
public client?: MatterClient;
|
|
32
37
|
|
|
@@ -80,6 +85,48 @@ export class DashboardHeader extends LitElement {
|
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
private _renderNavTabs() {
|
|
89
|
+
if (this.activeView === undefined) {
|
|
90
|
+
return nothing;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Only show tabs if at least one network type has devices
|
|
94
|
+
const showThreadTab = this.hasThreadDevices === true;
|
|
95
|
+
const showWifiTab = this.hasWifiDevices === true;
|
|
96
|
+
|
|
97
|
+
// Don't show nav tabs if no network devices exist
|
|
98
|
+
if (!showThreadTab && !showWifiTab) {
|
|
99
|
+
return nothing;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return html`
|
|
103
|
+
<nav class="nav-tabs" aria-label="View navigation">
|
|
104
|
+
<a
|
|
105
|
+
href="#nodes"
|
|
106
|
+
class="nav-tab ${this.activeView === "nodes" ? "active" : ""}"
|
|
107
|
+
aria-current=${this.activeView === "nodes" ? "page" : nothing}
|
|
108
|
+
>Nodes</a
|
|
109
|
+
>
|
|
110
|
+
${showThreadTab
|
|
111
|
+
? html`<a
|
|
112
|
+
href="#thread"
|
|
113
|
+
class="nav-tab ${this.activeView === "thread" ? "active" : ""}"
|
|
114
|
+
aria-current=${this.activeView === "thread" ? "page" : nothing}
|
|
115
|
+
>Thread</a
|
|
116
|
+
>`
|
|
117
|
+
: nothing}
|
|
118
|
+
${showWifiTab
|
|
119
|
+
? html`<a
|
|
120
|
+
href="#wifi"
|
|
121
|
+
class="nav-tab ${this.activeView === "wifi" ? "active" : ""}"
|
|
122
|
+
aria-current=${this.activeView === "wifi" ? "page" : nothing}
|
|
123
|
+
>WiFi</a
|
|
124
|
+
>`
|
|
125
|
+
: nothing}
|
|
126
|
+
</nav>
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
83
130
|
protected override render() {
|
|
84
131
|
return html`
|
|
85
132
|
<div class="header">
|
|
@@ -93,6 +140,7 @@ export class DashboardHeader extends LitElement {
|
|
|
93
140
|
: ""}
|
|
94
141
|
|
|
95
142
|
<div class="title">${this.title ?? ""}</div>
|
|
143
|
+
${this._renderNavTabs()}
|
|
96
144
|
<div class="actions">
|
|
97
145
|
${this.actions?.map(action => {
|
|
98
146
|
return html`
|
|
@@ -156,5 +204,38 @@ export class DashboardHeader extends LitElement {
|
|
|
156
204
|
max-width: 100%;
|
|
157
205
|
align-items: center;
|
|
158
206
|
}
|
|
207
|
+
|
|
208
|
+
.nav-tabs {
|
|
209
|
+
display: flex;
|
|
210
|
+
margin-left: 24px;
|
|
211
|
+
gap: 4px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.nav-tab {
|
|
215
|
+
padding: 8px 16px;
|
|
216
|
+
color: var(--md-sys-color-on-primary);
|
|
217
|
+
text-decoration: none;
|
|
218
|
+
font-size: 0.875rem;
|
|
219
|
+
font-weight: 500;
|
|
220
|
+
border-radius: 4px 4px 0 0;
|
|
221
|
+
opacity: 0.7;
|
|
222
|
+
transition: opacity 0.2s;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.nav-tab:hover {
|
|
226
|
+
opacity: 0.9;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.nav-tab.active {
|
|
230
|
+
opacity: 1;
|
|
231
|
+
background-color: rgba(255, 255, 255, 0.15);
|
|
232
|
+
border-bottom: 2px solid var(--md-sys-color-on-primary);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@media (max-width: 768px) {
|
|
236
|
+
.nav-tabs {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
159
240
|
`;
|
|
160
241
|
}
|
|
@@ -199,8 +199,8 @@ export class NodeDetails extends LitElement {
|
|
|
199
199
|
private async _binding() {
|
|
200
200
|
try {
|
|
201
201
|
showNodeBindingDialog(this.client, this.node!, this.endpoint);
|
|
202
|
-
} catch (err:
|
|
203
|
-
console.
|
|
202
|
+
} catch (err: unknown) {
|
|
203
|
+
console.error("Binding error:", err);
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -14,10 +14,13 @@ import "../components/ha-svg-icon";
|
|
|
14
14
|
import { clone } from "../util/clone_class.js";
|
|
15
15
|
import type { Route } from "../util/routing.js";
|
|
16
16
|
import "./components/header";
|
|
17
|
+
import type { ActiveView } from "./components/header.js";
|
|
17
18
|
import "./matter-cluster-view";
|
|
18
19
|
import "./matter-endpoint-view";
|
|
20
|
+
import "./matter-network-view";
|
|
19
21
|
import "./matter-node-view";
|
|
20
22
|
import "./matter-server-view";
|
|
23
|
+
import { categorizeDevices } from "./network/network-utils.js";
|
|
21
24
|
|
|
22
25
|
declare global {
|
|
23
26
|
interface HTMLElementTagNameMap {
|
|
@@ -32,27 +35,73 @@ class MatterDashboardApp extends LitElement {
|
|
|
32
35
|
path: [],
|
|
33
36
|
};
|
|
34
37
|
|
|
38
|
+
@state() private _activeView: ActiveView = "nodes";
|
|
39
|
+
|
|
40
|
+
/** Initial selected node ID from URL (string to avoid BigInt precision loss) */
|
|
41
|
+
@state() private _initialSelectedNodeId: string | null = null;
|
|
42
|
+
|
|
35
43
|
public client!: MatterClient;
|
|
36
44
|
|
|
37
45
|
@state()
|
|
38
46
|
private _state: "connecting" | "connected" | "error" | "disconnected" = "connecting";
|
|
39
47
|
|
|
48
|
+
/** Track whether nodes have been loaded at least once (to avoid redirecting before data arrives) */
|
|
49
|
+
private _nodesLoaded = false;
|
|
50
|
+
|
|
40
51
|
private provider = new ContextProvider(this, { context: clientContext, initialValue: this.client });
|
|
41
52
|
|
|
53
|
+
/** Reference to updateRoute function so it can be called from event listeners */
|
|
54
|
+
private _updateRoute?: () => void;
|
|
55
|
+
|
|
42
56
|
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
|
43
57
|
super.firstUpdated(_changedProperties);
|
|
44
58
|
this._connect();
|
|
45
59
|
|
|
46
60
|
// Handle history changes
|
|
47
|
-
|
|
48
|
-
const
|
|
61
|
+
this._updateRoute = () => {
|
|
62
|
+
const hash = location.hash.substring(1);
|
|
63
|
+
const pathParts = hash.split("/");
|
|
64
|
+
|
|
65
|
+
// Reset initial selected node
|
|
66
|
+
this._initialSelectedNodeId = null;
|
|
67
|
+
|
|
68
|
+
// Get device counts for conditional navigation
|
|
69
|
+
const { hasThreadDevices, hasWifiDevices } = this._getDeviceCounts();
|
|
70
|
+
|
|
71
|
+
// Determine active view from hash
|
|
72
|
+
if (pathParts[0] === "thread") {
|
|
73
|
+
// Only redirect if nodes have been loaded (avoid redirecting on initial load before data arrives)
|
|
74
|
+
if (this._nodesLoaded && !hasThreadDevices) {
|
|
75
|
+
location.hash = "#nodes";
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this._activeView = "thread";
|
|
79
|
+
// Check for node ID: #thread/123 - keep as string to avoid BigInt precision loss
|
|
80
|
+
if (pathParts.length > 1 && pathParts[1]) {
|
|
81
|
+
this._initialSelectedNodeId = pathParts[1];
|
|
82
|
+
}
|
|
83
|
+
} else if (pathParts[0] === "wifi") {
|
|
84
|
+
// Only redirect if nodes have been loaded (avoid redirecting on initial load before data arrives)
|
|
85
|
+
if (this._nodesLoaded && !hasWifiDevices) {
|
|
86
|
+
location.hash = "#nodes";
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this._activeView = "wifi";
|
|
90
|
+
// Check for node ID: #wifi/123 - keep as string to avoid BigInt precision loss
|
|
91
|
+
if (pathParts.length > 1 && pathParts[1]) {
|
|
92
|
+
this._initialSelectedNodeId = pathParts[1];
|
|
93
|
+
}
|
|
94
|
+
} else if (hash === "nodes" || hash === "" || pathParts[0] === "node") {
|
|
95
|
+
this._activeView = "nodes";
|
|
96
|
+
}
|
|
97
|
+
|
|
49
98
|
this._route = {
|
|
50
99
|
prefix: pathParts.length == 1 ? "" : pathParts[0],
|
|
51
100
|
path: pathParts.length == 1 ? pathParts : pathParts.slice(1),
|
|
52
101
|
};
|
|
53
102
|
};
|
|
54
|
-
window.addEventListener("hashchange",
|
|
55
|
-
|
|
103
|
+
window.addEventListener("hashchange", this._updateRoute);
|
|
104
|
+
this._updateRoute();
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
private _connect() {
|
|
@@ -69,6 +118,13 @@ class MatterDashboardApp extends LitElement {
|
|
|
69
118
|
|
|
70
119
|
private _setupEventListeners() {
|
|
71
120
|
this.client.addEventListener("nodes_changed", () => {
|
|
121
|
+
// Mark nodes as loaded and re-evaluate route (for redirect logic)
|
|
122
|
+
const wasFirstLoad = !this._nodesLoaded;
|
|
123
|
+
this._nodesLoaded = true;
|
|
124
|
+
if (wasFirstLoad && this._updateRoute) {
|
|
125
|
+
// Re-run route check now that nodes are available
|
|
126
|
+
this._updateRoute();
|
|
127
|
+
}
|
|
72
128
|
this.requestUpdate();
|
|
73
129
|
this.provider.setValue(clone(this.client));
|
|
74
130
|
});
|
|
@@ -85,6 +141,20 @@ class MatterDashboardApp extends LitElement {
|
|
|
85
141
|
this._connect();
|
|
86
142
|
};
|
|
87
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Get device counts for Thread and WiFi networks.
|
|
146
|
+
*/
|
|
147
|
+
private _getDeviceCounts(): { hasThreadDevices: boolean; hasWifiDevices: boolean } {
|
|
148
|
+
if (!this.client?.nodes) {
|
|
149
|
+
return { hasThreadDevices: false, hasWifiDevices: false };
|
|
150
|
+
}
|
|
151
|
+
const categorized = categorizeDevices(this.client.nodes);
|
|
152
|
+
return {
|
|
153
|
+
hasThreadDevices: categorized.thread.length > 0,
|
|
154
|
+
hasWifiDevices: categorized.wifi.length > 0 || categorized.ethernet.length > 0,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
88
158
|
override render() {
|
|
89
159
|
if (this._state === "connecting") {
|
|
90
160
|
return html`
|
|
@@ -154,11 +224,41 @@ class MatterDashboardApp extends LitElement {
|
|
|
154
224
|
></matter-node-view>
|
|
155
225
|
`;
|
|
156
226
|
}
|
|
157
|
-
//
|
|
227
|
+
// Get device counts for conditional navigation
|
|
228
|
+
const { hasThreadDevices, hasWifiDevices } = this._getDeviceCounts();
|
|
229
|
+
|
|
230
|
+
// Check for Thread view (#thread or #thread/123)
|
|
231
|
+
if (this._route.prefix === "thread" || this._route.path[0] === "thread") {
|
|
232
|
+
return html`<matter-network-view
|
|
233
|
+
.client=${this.client}
|
|
234
|
+
.nodes=${this.client.nodes}
|
|
235
|
+
.activeView=${this._activeView}
|
|
236
|
+
.initialSelectedNodeId=${this._initialSelectedNodeId}
|
|
237
|
+
.hasThreadDevices=${hasThreadDevices}
|
|
238
|
+
.hasWifiDevices=${hasWifiDevices}
|
|
239
|
+
networkType="thread"
|
|
240
|
+
></matter-network-view>`;
|
|
241
|
+
}
|
|
242
|
+
// Check for WiFi view (#wifi or #wifi/123)
|
|
243
|
+
if (this._route.prefix === "wifi" || this._route.path[0] === "wifi") {
|
|
244
|
+
return html`<matter-network-view
|
|
245
|
+
.client=${this.client}
|
|
246
|
+
.nodes=${this.client.nodes}
|
|
247
|
+
.activeView=${this._activeView}
|
|
248
|
+
.initialSelectedNodeId=${this._initialSelectedNodeId}
|
|
249
|
+
.hasThreadDevices=${hasThreadDevices}
|
|
250
|
+
.hasWifiDevices=${hasWifiDevices}
|
|
251
|
+
networkType="wifi"
|
|
252
|
+
></matter-network-view>`;
|
|
253
|
+
}
|
|
254
|
+
// root level: server overview (nodes view)
|
|
158
255
|
return html`<matter-server-view
|
|
159
256
|
.client=${this.client}
|
|
160
257
|
.nodes=${this.client.nodes}
|
|
161
258
|
.route=${this._route}
|
|
259
|
+
.activeView=${this._activeView}
|
|
260
|
+
.hasThreadDevices=${hasThreadDevices}
|
|
261
|
+
.hasWifiDevices=${hasWifiDevices}
|
|
162
262
|
></matter-server-view>`;
|
|
163
263
|
}
|
|
164
264
|
|