mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-12 19:59:56 +00:00
[client/ui] Add coloured status dot to tray menu
Show a small dot next to the first menu entry that reflects the daemon state: green for Connected, yellow for Connecting, blue for NeedsLogin/SessionExpired, red for LoginFailed/Error, grey for Idle/Disconnected and dark grey for DaemonUnavailable. PNGs are 24x24 with a pHYs chunk declaring 144 DPI so NSImage renders them at 12 pt while keeping retina-sharp pixel data; circles are supersampled 8x for smooth edges. Idle now surfaces as "Disconnected" in the menu label, daemon-status literals moved to status* constants, and Exit Node / Resources are gated on the Connected state instead of just daemon availability.
This commit is contained in:
BIN
client/ui/assets/netbird-menu-dot-connected.png
Normal file
BIN
client/ui/assets/netbird-menu-dot-connected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 452 B |
BIN
client/ui/assets/netbird-menu-dot-connecting.png
Normal file
BIN
client/ui/assets/netbird-menu-dot-connecting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 452 B |
BIN
client/ui/assets/netbird-menu-dot-error.png
Normal file
BIN
client/ui/assets/netbird-menu-dot-error.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 433 B |
BIN
client/ui/assets/netbird-menu-dot-idle.png
Normal file
BIN
client/ui/assets/netbird-menu-dot-idle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 483 B |
BIN
client/ui/assets/netbird-menu-dot-login.png
Normal file
BIN
client/ui/assets/netbird-menu-dot-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 475 B |
BIN
client/ui/assets/netbird-menu-dot-offline.png
Normal file
BIN
client/ui/assets/netbird-menu-dot-offline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 456 B |
@@ -58,3 +58,25 @@ var iconUpdateDisconnectedMacOS []byte
|
||||
|
||||
//go:embed assets/netbird.png
|
||||
var iconWindow []byte
|
||||
|
||||
// Small colored dots shown next to the status menu entry. Rendered as
|
||||
// regular NSImage/HBITMAP/GTK menu-item icons (not template), so the
|
||||
// colours stay intact on every platform.
|
||||
|
||||
//go:embed assets/netbird-menu-dot-connected.png
|
||||
var iconMenuDotConnected []byte
|
||||
|
||||
//go:embed assets/netbird-menu-dot-connecting.png
|
||||
var iconMenuDotConnecting []byte
|
||||
|
||||
//go:embed assets/netbird-menu-dot-login.png
|
||||
var iconMenuDotLogin []byte
|
||||
|
||||
//go:embed assets/netbird-menu-dot-error.png
|
||||
var iconMenuDotError []byte
|
||||
|
||||
//go:embed assets/netbird-menu-dot-idle.png
|
||||
var iconMenuDotIdle []byte
|
||||
|
||||
//go:embed assets/netbird-menu-dot-offline.png
|
||||
var iconMenuDotOffline []byte
|
||||
|
||||
@@ -26,14 +26,14 @@ const (
|
||||
trayTooltip = "NetBird"
|
||||
|
||||
// Top-level menu entries.
|
||||
menuStatusDisconnected = "Disconnected"
|
||||
menuStatusDisconnected = "Disconnected"
|
||||
menuStatusDaemonUnavailable = "Not running"
|
||||
menuOpenNetBird = "Open NetBird"
|
||||
menuConnect = "Connect"
|
||||
menuDisconnect = "Disconnect"
|
||||
menuExitNode = "Exit Node"
|
||||
menuNetworks = "Networks"
|
||||
menuQuit = "Quit"
|
||||
menuOpenNetBird = "Open NetBird"
|
||||
menuConnect = "Connect"
|
||||
menuDisconnect = "Disconnect"
|
||||
menuExitNode = "Exit Node"
|
||||
menuNetworks = "Resources"
|
||||
menuQuit = "Quit"
|
||||
|
||||
// Settings + diagnostics. The settings page replaces the Fyne tray's
|
||||
// Settings submenu (per-toggle checkboxes for SSH, auto-connect,
|
||||
@@ -68,11 +68,17 @@ const (
|
||||
notifySessionExpiredBody = "Your NetBird session has expired. Please log in again."
|
||||
|
||||
// Notification IDs (used to coalesce duplicate toasts).
|
||||
notifyIDUpdatePrefix = "netbird-update-"
|
||||
notifyIDEvent = "netbird-event-"
|
||||
notifyIDTrayError = "netbird-tray-error"
|
||||
notifyIDSessionExpired = "netbird-session-expired"
|
||||
notifyIDUpdatePrefix = "netbird-update-"
|
||||
notifyIDEvent = "netbird-event-"
|
||||
notifyIDTrayError = "netbird-tray-error"
|
||||
notifyIDSessionExpired = "netbird-session-expired"
|
||||
|
||||
// Daemon status strings mirroring internal.Status* — kept in sync
|
||||
// with client/internal/state.go.
|
||||
statusConnected = "Connected"
|
||||
statusConnecting = "Connecting"
|
||||
statusIdle = "Idle"
|
||||
statusError = "Error"
|
||||
// Daemon status string for an SSO session that has expired and needs
|
||||
// re-authentication. Mirrors internal.StatusSessionExpired.
|
||||
statusSessionExpired = "SessionExpired"
|
||||
@@ -189,7 +195,8 @@ func (t *Tray) buildMenu() *application.Menu {
|
||||
// up unconditionally rather than swapping items at runtime.
|
||||
t.statusItem = menu.Add(menuStatusDisconnected).
|
||||
OnClick(func(*application.Context) { t.openRoute("/login") }).
|
||||
SetEnabled(false)
|
||||
SetEnabled(false).
|
||||
SetBitmap(iconMenuDotIdle)
|
||||
|
||||
menu.AddSeparator()
|
||||
// The tray icon's left-click handler is intentionally unbound (see
|
||||
@@ -430,7 +437,7 @@ func (t *Tray) onUpdateProgress(ev *application.CustomEvent) {
|
||||
// otherwise spam Shell_NotifyIcon and the log.
|
||||
func (t *Tray) applyStatus(st services.Status) {
|
||||
t.mu.Lock()
|
||||
connected := strings.EqualFold(st.Status, "Connected")
|
||||
connected := strings.EqualFold(st.Status, statusConnected)
|
||||
iconChanged := connected != t.connected || st.Status != t.lastStatus
|
||||
// Detect the transition into SessionExpired: the daemon emits the
|
||||
// state on every Status snapshot for as long as the session stays
|
||||
@@ -463,11 +470,15 @@ func (t *Tray) applyStatus(st services.Status) {
|
||||
// When the daemon socket is unreachable, swap the label to make
|
||||
// the cause obvious; Connect/Disconnect would just fail.
|
||||
label := st.Status
|
||||
if daemonUnavailable {
|
||||
switch {
|
||||
case daemonUnavailable:
|
||||
label = menuStatusDaemonUnavailable
|
||||
case strings.EqualFold(st.Status, statusIdle):
|
||||
label = menuStatusDisconnected
|
||||
}
|
||||
t.statusItem.SetLabel(label)
|
||||
t.statusItem.SetEnabled(needsLogin)
|
||||
t.applyStatusIndicator(st.Status)
|
||||
}
|
||||
if t.upItem != nil {
|
||||
t.upItem.SetHidden(connected || needsLogin || daemonUnavailable)
|
||||
@@ -477,12 +488,14 @@ func (t *Tray) applyStatus(st services.Status) {
|
||||
t.downItem.SetHidden(!connected)
|
||||
t.downItem.SetEnabled(connected)
|
||||
}
|
||||
// Settings, Networks and Debug Bundle all drive daemon RPCs from
|
||||
// their respective frontend routes — disable them while the daemon
|
||||
// socket is unreachable so the user doesn't land on a page that
|
||||
// would only fail to load.
|
||||
// Exit Node and Resources surface tunnel-routed state, so only
|
||||
// expose them while the tunnel is up. Settings/Debug-Bundle just
|
||||
// need the daemon socket reachable.
|
||||
if t.exitNodeItem != nil {
|
||||
t.exitNodeItem.SetEnabled(connected)
|
||||
}
|
||||
if t.networksItem != nil {
|
||||
t.networksItem.SetEnabled(!daemonUnavailable)
|
||||
t.networksItem.SetEnabled(connected)
|
||||
}
|
||||
if t.settingsItem != nil {
|
||||
t.settingsItem.SetEnabled(!daemonUnavailable)
|
||||
@@ -519,18 +532,44 @@ func (t *Tray) handleSessionExpired() {
|
||||
}
|
||||
|
||||
func (t *Tray) rebuildExitNodes(nodes []string) {
|
||||
if t.exitNodeItem == nil {
|
||||
return
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
t.exitNodeItem.SetEnabled(false)
|
||||
if t.exitNodeItem == nil || len(nodes) == 0 {
|
||||
return
|
||||
}
|
||||
sub := application.NewMenu()
|
||||
for _, fqdn := range nodes {
|
||||
sub.AddCheckbox(fqdn, false)
|
||||
}
|
||||
t.exitNodeItem.SetEnabled(true)
|
||||
}
|
||||
|
||||
// applyStatusIndicator sets the small coloured dot shown on the status
|
||||
// menu entry. The dot mirrors the tray icon's state through a fixed
|
||||
// palette: green for Connected, yellow for Connecting, blue for the
|
||||
// login states, red for hard errors, grey for the idle/disconnected
|
||||
// pair and a darker grey when the daemon socket is unreachable.
|
||||
func (t *Tray) applyStatusIndicator(status string) {
|
||||
if t.statusItem == nil {
|
||||
return
|
||||
}
|
||||
t.statusItem.SetBitmap(statusIndicatorBitmap(status))
|
||||
}
|
||||
|
||||
func statusIndicatorBitmap(status string) []byte {
|
||||
switch {
|
||||
case strings.EqualFold(status, statusConnected):
|
||||
return iconMenuDotConnected
|
||||
case strings.EqualFold(status, statusConnecting):
|
||||
return iconMenuDotConnecting
|
||||
case strings.EqualFold(status, statusNeedsLogin),
|
||||
strings.EqualFold(status, statusSessionExpired):
|
||||
return iconMenuDotLogin
|
||||
case strings.EqualFold(status, statusLoginFailed),
|
||||
strings.EqualFold(status, statusError):
|
||||
return iconMenuDotError
|
||||
case strings.EqualFold(status, services.StatusDaemonUnavailable):
|
||||
return iconMenuDotOffline
|
||||
default:
|
||||
return iconMenuDotIdle
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tray) applyIcon() {
|
||||
@@ -561,8 +600,8 @@ func (t *Tray) iconForState() (icon, dark []byte) {
|
||||
statusLabel := t.lastStatus
|
||||
t.mu.Unlock()
|
||||
|
||||
connecting := strings.EqualFold(statusLabel, "Connecting")
|
||||
errored := strings.EqualFold(statusLabel, "Error") ||
|
||||
connecting := strings.EqualFold(statusLabel, statusConnecting)
|
||||
errored := strings.EqualFold(statusLabel, statusError) ||
|
||||
strings.EqualFold(statusLabel, services.StatusDaemonUnavailable)
|
||||
needsLogin := strings.EqualFold(statusLabel, statusNeedsLogin) ||
|
||||
strings.EqualFold(statusLabel, statusSessionExpired) ||
|
||||
|
||||
Reference in New Issue
Block a user