Skip to content

Commit

Permalink
Add network status dialog (#1829)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
jotaen4tinypilot and jotaen authored Jul 26, 2024
1 parent 3e4df95 commit 3d33c4d
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 0 deletions.
7 changes: 7 additions & 0 deletions app/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,13 @@ menuBar.addEventListener("wifi-dialog-requested", () => {
document.getElementById("wifi-overlay").show();
document.getElementById("wifi-dialog").initialize();
});
menuBar.addEventListener("network-status-dialog-requested", () => {
// Note: we have to call `initialize()` after `show()`, to ensure that the
// dialog is able to focus the main input element.
// See https://github.com/tiny-pilot/tinypilot/issues/1770
document.getElementById("network-status-overlay").show();
document.getElementById("network-status-dialog").initialize();
});
menuBar.addEventListener("fullscreen-requested", () => {
document.getElementById("remote-screen").fullscreen = true;
});
Expand Down
7 changes: 7 additions & 0 deletions app/templates/custom-elements/menu-bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@
>Static IP</a
>
</li>
<li class="item" role="presentation">
<a
data-onclick-event="network-status-dialog-requested"
role="menuitem"
>Status</a
>
</li>
</ul>
</li>
<li class="item" role="presentation">
Expand Down
143 changes: 143 additions & 0 deletions app/templates/custom-elements/network-status-dialog.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template id="network-status-dialog-template">
<style>
@import "css/style.css";
@import "css/button.css";

#initializing,
#display {
display: none;
}

:host([state="initializing"]) #initializing,
:host([state="display"]) #display {
display: block;
}

.info-container {
margin-bottom: 1em;
}
</style>

<div id="initializing">
<h3>Determining Network Status</h3>
<div>
<progress-spinner></progress-spinner>
</div>
</div>

<div id="display">
<h3>Network Status</h3>
<div class="info-container">
<network-status-interface id="status-ethernet"></network-status-interface>
<network-status-interface id="status-wifi"></network-status-interface>
</div>
<div class="button-container">
<button id="close-button" type="button">Close</button>
</div>
</div>
</template>

<script type="module">
import {
DialogClosedEvent,
DialogCloseStateChangedEvent,
DialogFailedEvent,
} from "/js/events.js";
import { getNetworkStatus } from "/js/controllers.js";

(function () {
const template = document.querySelector("#network-status-dialog-template");

customElements.define(
"network-status-dialog",
class extends HTMLElement {
_states = {
INITIALIZING: "initializing",
DISPLAY: "display",
};
_statesWithoutDialogClose = new Set([this._states.INITIALIZING]);

connectedCallback() {
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
);
this._elements = {
statusEthernet: this.shadowRoot.querySelector("#status-ethernet"),
statusWifi: this.shadowRoot.querySelector("#status-wifi"),
};
this._shouldAutoUpdate = false;
this._updateTicker = null;
this.shadowRoot
.querySelector("#close-button")
.addEventListener("click", () => {
this.dispatchEvent(new DialogClosedEvent());
});

// For all events that terminate the dialog, make sure to stop the
// update ticker, otherwise the status requests would continue to be
// fired even when the dialog is not visible anymore.
["dialog-closed", "dialog-failed"].forEach((evtName) => {
this.addEventListener(evtName, () => {
this._shouldAutoUpdate = false;
clearTimeout(this._updateTicker);
});
});
}

get _state() {
return this.getAttribute("state");
}

set _state(newValue) {
this.setAttribute("state", newValue);
this.dispatchEvent(
new DialogCloseStateChangedEvent(
!this._statesWithoutDialogClose.has(newValue)
)
);
}

async initialize() {
this._state = this._states.INITIALIZING;
await this._update();
this._state = this._states.DISPLAY;
this._shouldAutoUpdate = true;
this._startUpdateLoop();
}

_startUpdateLoop() {
// The update loop is based on `setTimeout`, not `setInterval`,
// because the latter would continue to trigger even if the
// update function lags and happens to be slower than the interval.
// That would result in a lot of parallel, pending requests.
this._updateTicker = setTimeout(async () => {
await this._update();
if (this._shouldAutoUpdate) {
this._startUpdateLoop();
}
}, 2500);
}

async _update() {
let networkStatus;
try {
networkStatus = await getNetworkStatus();
} catch (error) {
this.dispatchEvent(
new DialogFailedEvent({
title: "Failed to Determine Network Status",
details: error,
})
);
return;
}
this._elements.statusEthernet.update(
"Ethernet",
networkStatus.ethernet
);
this._elements.statusWifi.update("Wi-Fi", networkStatus.wifi);
}
}
);
})();
</script>
124 changes: 124 additions & 0 deletions app/templates/custom-elements/network-status-interface.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<template id="network-status-interface-template">
<style>
@import "css/style.css";

:host {
display: flex;
margin-bottom: 0.75em;
}

#name {
width: 39%;
text-align: right;
font-weight: bold;
}

#data {
display: flex;
flex-direction: column;
flex: 1;
align-items: start;
margin-left: 1em;
}

#data div {
margin-bottom: 0.3em;
}

.connection-indicator {
display: flex;
align-items: center;
}

.status-dot {
vertical-align: middle;
height: 0.9em;
width: 0.9em;
margin-right: 0.5rem;
border-radius: 50%;
display: inline-block;
}

:host([is-connected=""]) .status-dot {
background-color: var(--brand-green-bright);
}

:host(:not([is-connected])) .status-dot {
background-color: var(--brand-red-bright);
}

.label-connected,
.label-disconnected {
display: none;
}

:host([is-connected=""]) .label-connected,
:host(:not([is-connected])) .label-disconnected {
display: inline;
}

#ip-address,
#mac-address {
margin-left: 0.3em;
}
</style>

<div id="name"><!-- Filled programmatically --></div>
<div id="data">
<div class="connection-indicator">
<span class="status-dot status-dot-connected"></span>
<span class="label-connected">Connected</span>
<span class="label-disconnected">Disconnected</span>
</div>
<div>
IP Address:
<span id="ip-address" class="monospace"
><!-- Filled programmatically --></span
>
</div>
<div>
MAC Address:
<span id="mac-address" class="monospace"
><!-- Filled programmatically --></span
>
</div>
</div>
</template>

<script type="module">
(function () {
const template = document.querySelector(
"#network-status-interface-template"
);

customElements.define(
"network-status-interface",
class extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" }).appendChild(
template.content.cloneNode(true)
);
this._elements = {
name: this.shadowRoot.querySelector("#name"),
ipAddress: this.shadowRoot.querySelector("#ip-address"),
macAddress: this.shadowRoot.querySelector("#mac-address"),
};
}

/**
* @param {string} name - The display name of the interface
* @param {Object} status
* @param {boolean} status.isConnected
* @param {string} [status.ipAddress]
* @param {string} [status.macAddress]
*/
update(name, status) {
this._elements.name.innerText = name;
this.toggleAttribute("is-connected", status.isConnected);
this._elements.ipAddress.innerText = status.ipAddress || "n/a";
this._elements.macAddress.innerText = status.macAddress || "n/a";
}
}
);
})();
</script>
5 changes: 5 additions & 0 deletions app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
<overlay-panel id="wifi-overlay">
<wifi-dialog id="wifi-dialog"></wifi-dialog>
</overlay-panel>
<overlay-panel id="network-status-overlay">
<network-status-dialog
id="network-status-dialog"
></network-status-dialog>
</overlay-panel>
</div>
<script src="/third-party/socket.io/4.7.1/socket.io.min.js"></script>
<script type="module" src="/js/app.js"></script>
Expand Down

0 comments on commit 3d33c4d

Please sign in to comment.