Skip to content

Commit 3d33c4d

Browse files
Add network status dialog (#1829)
Resolves #1814. Stacked on #1828. This PR adds a new “Status” dialog to the “Network” submenu, which displays basic network connectivity information in (almost) realtime. The screen recording below shows two browser windows side by side, and how the Network Status dialog updates itself automatically when there are changes. https://github.com/user-attachments/assets/b11081cc-364a-4816-a8c2-474750d8def4 Notes: - I’ve set the update frequency to `2,5s`, which seemed like a reasonable compromise between “realtime enough” and not overloading the backend on lower-latency connections. - For the case when the IP address or MAC address are absent, I’ve debated whether to show `n/a` or whether to hide the row altogether. I’m not strongly attached to the `n/a` placeholder, but to me it felt better to show the row consistently, instead of having the UI jump around as fields are inserted and removed. <a data-ca-tag href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1829"><img src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review on CodeApprove" /></a> --------- Co-authored-by: Jan Heuermann <[email protected]>
1 parent 3e4df95 commit 3d33c4d

File tree

5 files changed

+286
-0
lines changed

5 files changed

+286
-0
lines changed

app/static/js/app.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,13 @@ menuBar.addEventListener("wifi-dialog-requested", () => {
327327
document.getElementById("wifi-overlay").show();
328328
document.getElementById("wifi-dialog").initialize();
329329
});
330+
menuBar.addEventListener("network-status-dialog-requested", () => {
331+
// Note: we have to call `initialize()` after `show()`, to ensure that the
332+
// dialog is able to focus the main input element.
333+
// See https://github.com/tiny-pilot/tinypilot/issues/1770
334+
document.getElementById("network-status-overlay").show();
335+
document.getElementById("network-status-dialog").initialize();
336+
});
330337
menuBar.addEventListener("fullscreen-requested", () => {
331338
document.getElementById("remote-screen").fullscreen = true;
332339
});

app/templates/custom-elements/menu-bar.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,13 @@
239239
>Static IP</a
240240
>
241241
</li>
242+
<li class="item" role="presentation">
243+
<a
244+
data-onclick-event="network-status-dialog-requested"
245+
role="menuitem"
246+
>Status</a
247+
>
248+
</li>
242249
</ul>
243250
</li>
244251
<li class="item" role="presentation">
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<template id="network-status-dialog-template">
2+
<style>
3+
@import "css/style.css";
4+
@import "css/button.css";
5+
6+
#initializing,
7+
#display {
8+
display: none;
9+
}
10+
11+
:host([state="initializing"]) #initializing,
12+
:host([state="display"]) #display {
13+
display: block;
14+
}
15+
16+
.info-container {
17+
margin-bottom: 1em;
18+
}
19+
</style>
20+
21+
<div id="initializing">
22+
<h3>Determining Network Status</h3>
23+
<div>
24+
<progress-spinner></progress-spinner>
25+
</div>
26+
</div>
27+
28+
<div id="display">
29+
<h3>Network Status</h3>
30+
<div class="info-container">
31+
<network-status-interface id="status-ethernet"></network-status-interface>
32+
<network-status-interface id="status-wifi"></network-status-interface>
33+
</div>
34+
<div class="button-container">
35+
<button id="close-button" type="button">Close</button>
36+
</div>
37+
</div>
38+
</template>
39+
40+
<script type="module">
41+
import {
42+
DialogClosedEvent,
43+
DialogCloseStateChangedEvent,
44+
DialogFailedEvent,
45+
} from "/js/events.js";
46+
import { getNetworkStatus } from "/js/controllers.js";
47+
48+
(function () {
49+
const template = document.querySelector("#network-status-dialog-template");
50+
51+
customElements.define(
52+
"network-status-dialog",
53+
class extends HTMLElement {
54+
_states = {
55+
INITIALIZING: "initializing",
56+
DISPLAY: "display",
57+
};
58+
_statesWithoutDialogClose = new Set([this._states.INITIALIZING]);
59+
60+
connectedCallback() {
61+
this.attachShadow({ mode: "open" }).appendChild(
62+
template.content.cloneNode(true)
63+
);
64+
this._elements = {
65+
statusEthernet: this.shadowRoot.querySelector("#status-ethernet"),
66+
statusWifi: this.shadowRoot.querySelector("#status-wifi"),
67+
};
68+
this._shouldAutoUpdate = false;
69+
this._updateTicker = null;
70+
this.shadowRoot
71+
.querySelector("#close-button")
72+
.addEventListener("click", () => {
73+
this.dispatchEvent(new DialogClosedEvent());
74+
});
75+
76+
// For all events that terminate the dialog, make sure to stop the
77+
// update ticker, otherwise the status requests would continue to be
78+
// fired even when the dialog is not visible anymore.
79+
["dialog-closed", "dialog-failed"].forEach((evtName) => {
80+
this.addEventListener(evtName, () => {
81+
this._shouldAutoUpdate = false;
82+
clearTimeout(this._updateTicker);
83+
});
84+
});
85+
}
86+
87+
get _state() {
88+
return this.getAttribute("state");
89+
}
90+
91+
set _state(newValue) {
92+
this.setAttribute("state", newValue);
93+
this.dispatchEvent(
94+
new DialogCloseStateChangedEvent(
95+
!this._statesWithoutDialogClose.has(newValue)
96+
)
97+
);
98+
}
99+
100+
async initialize() {
101+
this._state = this._states.INITIALIZING;
102+
await this._update();
103+
this._state = this._states.DISPLAY;
104+
this._shouldAutoUpdate = true;
105+
this._startUpdateLoop();
106+
}
107+
108+
_startUpdateLoop() {
109+
// The update loop is based on `setTimeout`, not `setInterval`,
110+
// because the latter would continue to trigger even if the
111+
// update function lags and happens to be slower than the interval.
112+
// That would result in a lot of parallel, pending requests.
113+
this._updateTicker = setTimeout(async () => {
114+
await this._update();
115+
if (this._shouldAutoUpdate) {
116+
this._startUpdateLoop();
117+
}
118+
}, 2500);
119+
}
120+
121+
async _update() {
122+
let networkStatus;
123+
try {
124+
networkStatus = await getNetworkStatus();
125+
} catch (error) {
126+
this.dispatchEvent(
127+
new DialogFailedEvent({
128+
title: "Failed to Determine Network Status",
129+
details: error,
130+
})
131+
);
132+
return;
133+
}
134+
this._elements.statusEthernet.update(
135+
"Ethernet",
136+
networkStatus.ethernet
137+
);
138+
this._elements.statusWifi.update("Wi-Fi", networkStatus.wifi);
139+
}
140+
}
141+
);
142+
})();
143+
</script>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<template id="network-status-interface-template">
2+
<style>
3+
@import "css/style.css";
4+
5+
:host {
6+
display: flex;
7+
margin-bottom: 0.75em;
8+
}
9+
10+
#name {
11+
width: 39%;
12+
text-align: right;
13+
font-weight: bold;
14+
}
15+
16+
#data {
17+
display: flex;
18+
flex-direction: column;
19+
flex: 1;
20+
align-items: start;
21+
margin-left: 1em;
22+
}
23+
24+
#data div {
25+
margin-bottom: 0.3em;
26+
}
27+
28+
.connection-indicator {
29+
display: flex;
30+
align-items: center;
31+
}
32+
33+
.status-dot {
34+
vertical-align: middle;
35+
height: 0.9em;
36+
width: 0.9em;
37+
margin-right: 0.5rem;
38+
border-radius: 50%;
39+
display: inline-block;
40+
}
41+
42+
:host([is-connected=""]) .status-dot {
43+
background-color: var(--brand-green-bright);
44+
}
45+
46+
:host(:not([is-connected])) .status-dot {
47+
background-color: var(--brand-red-bright);
48+
}
49+
50+
.label-connected,
51+
.label-disconnected {
52+
display: none;
53+
}
54+
55+
:host([is-connected=""]) .label-connected,
56+
:host(:not([is-connected])) .label-disconnected {
57+
display: inline;
58+
}
59+
60+
#ip-address,
61+
#mac-address {
62+
margin-left: 0.3em;
63+
}
64+
</style>
65+
66+
<div id="name"><!-- Filled programmatically --></div>
67+
<div id="data">
68+
<div class="connection-indicator">
69+
<span class="status-dot status-dot-connected"></span>
70+
<span class="label-connected">Connected</span>
71+
<span class="label-disconnected">Disconnected</span>
72+
</div>
73+
<div>
74+
IP Address:
75+
<span id="ip-address" class="monospace"
76+
><!-- Filled programmatically --></span
77+
>
78+
</div>
79+
<div>
80+
MAC Address:
81+
<span id="mac-address" class="monospace"
82+
><!-- Filled programmatically --></span
83+
>
84+
</div>
85+
</div>
86+
</template>
87+
88+
<script type="module">
89+
(function () {
90+
const template = document.querySelector(
91+
"#network-status-interface-template"
92+
);
93+
94+
customElements.define(
95+
"network-status-interface",
96+
class extends HTMLElement {
97+
connectedCallback() {
98+
this.attachShadow({ mode: "open" }).appendChild(
99+
template.content.cloneNode(true)
100+
);
101+
this._elements = {
102+
name: this.shadowRoot.querySelector("#name"),
103+
ipAddress: this.shadowRoot.querySelector("#ip-address"),
104+
macAddress: this.shadowRoot.querySelector("#mac-address"),
105+
};
106+
}
107+
108+
/**
109+
* @param {string} name - The display name of the interface
110+
* @param {Object} status
111+
* @param {boolean} status.isConnected
112+
* @param {string} [status.ipAddress]
113+
* @param {string} [status.macAddress]
114+
*/
115+
update(name, status) {
116+
this._elements.name.innerText = name;
117+
this.toggleAttribute("is-connected", status.isConnected);
118+
this._elements.ipAddress.innerText = status.ipAddress || "n/a";
119+
this._elements.macAddress.innerText = status.macAddress || "n/a";
120+
}
121+
}
122+
);
123+
})();
124+
</script>

app/templates/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979
<overlay-panel id="wifi-overlay">
8080
<wifi-dialog id="wifi-dialog"></wifi-dialog>
8181
</overlay-panel>
82+
<overlay-panel id="network-status-overlay">
83+
<network-status-dialog
84+
id="network-status-dialog"
85+
></network-status-dialog>
86+
</overlay-panel>
8287
</div>
8388
<script src="/third-party/socket.io/4.7.1/socket.io.min.js"></script>
8489
<script type="module" src="/js/app.js"></script>

0 commit comments

Comments
 (0)