mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-18 14:49:57 +00:00
[client/ui] Add Profiles submenu to the tray
Mirror the main branch's profile list: a Profiles submenu populated from the daemon's ListProfiles RPC, with the active profile shown as a checked entry and a click on any other entry switching to it via SwitchProfile. The initial fill is deferred to the Common.ApplicationStarted hook because Menu.Update() short-circuits while app.running is false and the Wails3 macOS impl would nil-deref on early-startup InvokeSync.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/events"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/ui/services"
|
"github.com/netbirdio/netbird/client/ui/services"
|
||||||
@@ -33,6 +34,7 @@ const (
|
|||||||
menuDisconnect = "Disconnect"
|
menuDisconnect = "Disconnect"
|
||||||
menuExitNode = "Exit Node"
|
menuExitNode = "Exit Node"
|
||||||
menuNetworks = "Resources"
|
menuNetworks = "Resources"
|
||||||
|
menuProfiles = "Profiles"
|
||||||
menuQuit = "Quit"
|
menuQuit = "Quit"
|
||||||
|
|
||||||
// Settings + diagnostics. The settings page replaces the Fyne tray's
|
// Settings + diagnostics. The settings page replaces the Fyne tray's
|
||||||
@@ -125,6 +127,7 @@ type Tray struct {
|
|||||||
downItem *application.MenuItem
|
downItem *application.MenuItem
|
||||||
exitNodeItem *application.MenuItem
|
exitNodeItem *application.MenuItem
|
||||||
networksItem *application.MenuItem
|
networksItem *application.MenuItem
|
||||||
|
profileSubmenu *application.Menu
|
||||||
settingsItem *application.MenuItem
|
settingsItem *application.MenuItem
|
||||||
debugItem *application.MenuItem
|
debugItem *application.MenuItem
|
||||||
updateItem *application.MenuItem
|
updateItem *application.MenuItem
|
||||||
@@ -168,6 +171,13 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe
|
|||||||
app.Event.On(services.EventSystem, t.onSystemEvent)
|
app.Event.On(services.EventSystem, t.onSystemEvent)
|
||||||
app.Event.On(services.EventUpdateAvailable, t.onUpdateAvailable)
|
app.Event.On(services.EventUpdateAvailable, t.onUpdateAvailable)
|
||||||
app.Event.On(services.EventUpdateProgress, t.onUpdateProgress)
|
app.Event.On(services.EventUpdateProgress, t.onUpdateProgress)
|
||||||
|
// Defer the first profile load until the macOS/GTK/Win32 menu impl is
|
||||||
|
// live — Menu.Update() short-circuits while app.running is false, and
|
||||||
|
// AppKit's main queue isn't ready earlier either (see d23ef34 InvokeSync
|
||||||
|
// nil-deref).
|
||||||
|
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
|
||||||
|
go t.loadProfiles()
|
||||||
|
})
|
||||||
|
|
||||||
go t.loadConfig()
|
go t.loadConfig()
|
||||||
return t
|
return t
|
||||||
@@ -204,6 +214,11 @@ func (t *Tray) buildMenu() *application.Menu {
|
|||||||
// menu entry on every platform.
|
// menu entry on every platform.
|
||||||
menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() })
|
menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() })
|
||||||
menu.AddSeparator()
|
menu.AddSeparator()
|
||||||
|
// Profiles submenu is populated asynchronously once the application
|
||||||
|
// has started — Menu.Update() is a no-op before app.running is true,
|
||||||
|
// so the initial fill is gated on the ApplicationStarted hook.
|
||||||
|
t.profileSubmenu = menu.AddSubmenu(menuProfiles)
|
||||||
|
menu.AddSeparator()
|
||||||
// Only the action that applies to the current state is visible: Connect
|
// Only the action that applies to the current state is visible: Connect
|
||||||
// when disconnected, Disconnect when connected. applyStatus swaps them on
|
// when disconnected, Disconnect when connected. applyStatus swaps them on
|
||||||
// each daemon status change.
|
// each daemon status change.
|
||||||
@@ -675,6 +690,68 @@ func (t *Tray) loadConfig() {
|
|||||||
t.mu.Unlock()
|
t.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadProfiles refreshes the Profiles submenu from the daemon. Each
|
||||||
|
// entry is a checkbox showing the active profile and switches on click.
|
||||||
|
// Called once on ApplicationStarted and again after a successful switch
|
||||||
|
// so the checkmark moves to the new active profile.
|
||||||
|
func (t *Tray) loadProfiles() {
|
||||||
|
if t.profileSubmenu == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
username, err := t.svc.Profiles.Username()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("get current user: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profiles, err := t.svc.Profiles.List(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("list profiles: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(profiles, func(i, j int) bool { return profiles[i].Name < profiles[j].Name })
|
||||||
|
|
||||||
|
t.profileSubmenu.Clear()
|
||||||
|
for _, p := range profiles {
|
||||||
|
name := p.Name
|
||||||
|
active := p.IsActive
|
||||||
|
item := t.profileSubmenu.AddCheckbox(name, active)
|
||||||
|
item.OnClick(func(*application.Context) {
|
||||||
|
if active {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.switchProfile(name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.profileSubmenu.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
// switchProfile runs the daemon RPC in a goroutine so the menu click
|
||||||
|
// returns immediately, then reloads the submenu to move the checkmark.
|
||||||
|
func (t *Tray) switchProfile(name string) {
|
||||||
|
go func() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
username, err := t.svc.Profiles.Username()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get current user: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := t.svc.Profiles.Switch(ctx, services.ProfileRef{
|
||||||
|
ProfileName: name,
|
||||||
|
Username: username,
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("switch profile to %s: %v", name, err)
|
||||||
|
t.notifyError(fmt.Sprintf("Failed to switch to %s", name))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.loadProfiles()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// notify wraps the Wails notification service with the tray's standard
|
// notify wraps the Wails notification service with the tray's standard
|
||||||
// id-prefix scheme and swallows errors (notifications are best-effort).
|
// id-prefix scheme and swallows errors (notifications are best-effort).
|
||||||
func (t *Tray) notify(title, body, id string) {
|
func (t *Tray) notify(title, body, id string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user