mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-13 12:19:54 +00:00
[client/ui] Show active profile name and account email in tray menu
The Profiles submenu label now reflects the active profile name instead of the static "Profiles" text. A disabled email item appears directly below it in the main menu, matching the legacy Fyne/systray behaviour. Email is read from the per-profile state file via profilemanager in the UI process — not through the daemon RPC — because the daemon runs as root and its getConfigDir() resolves to the root home directory, making the user-owned state file inaccessible from the daemon side.
This commit is contained in:
@@ -755,6 +755,18 @@ export class Profile {
|
||||
"name": string;
|
||||
"isActive": boolean;
|
||||
|
||||
/**
|
||||
* Email is the account address associated with this profile, sourced from
|
||||
* the per-profile state file written by the CLI after a successful SSO
|
||||
* login (e.g. ~/Library/Application Support/netbird/default.state.json on
|
||||
* macOS). The daemon always runs as root, so its getConfigDir() resolves to
|
||||
* the root home directory and cannot reach the user-owned state file. The
|
||||
* UI process runs as the logged-in user and can read it directly via
|
||||
* profilemanager.ProfileManager, which is why the email is fetched here
|
||||
* instead of being returned by the ListProfiles RPC.
|
||||
*/
|
||||
"email": string;
|
||||
|
||||
/** Creates a new Profile instance. */
|
||||
constructor($$source: Partial<Profile> = {}) {
|
||||
if (!("name" in $$source)) {
|
||||
@@ -763,6 +775,9 @@ export class Profile {
|
||||
if (!("isActive" in $$source)) {
|
||||
this["isActive"] = false;
|
||||
}
|
||||
if (!("email" in $$source)) {
|
||||
this["email"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"os/user"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
@@ -13,6 +14,15 @@ import (
|
||||
type Profile struct {
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"isActive"`
|
||||
// Email is the account address associated with this profile, sourced from
|
||||
// the per-profile state file written by the CLI after a successful SSO
|
||||
// login (e.g. ~/Library/Application Support/netbird/default.state.json on
|
||||
// macOS). The daemon always runs as root, so its getConfigDir() resolves to
|
||||
// the root home directory and cannot reach the user-owned state file. The
|
||||
// UI process runs as the logged-in user and can read it directly via
|
||||
// profilemanager.ProfileManager, which is why the email is fetched here
|
||||
// instead of being returned by the ListProfiles RPC.
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// ProfileRef identifies a profile by name+username.
|
||||
@@ -55,9 +65,14 @@ func (s *Profiles) List(ctx context.Context, username string) ([]Profile, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pm := profilemanager.NewProfileManager()
|
||||
out := make([]Profile, 0, len(resp.GetProfiles()))
|
||||
for _, p := range resp.GetProfiles() {
|
||||
out = append(out, Profile{Name: p.GetName(), IsActive: p.GetIsActive()})
|
||||
prof := Profile{Name: p.GetName(), IsActive: p.GetIsActive()}
|
||||
if state, err := pm.GetProfileState(p.GetName()); err == nil {
|
||||
prof.Email = state.Email
|
||||
}
|
||||
out = append(out, prof)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -128,7 +128,9 @@ type Tray struct {
|
||||
downItem *application.MenuItem
|
||||
exitNodeItem *application.MenuItem
|
||||
networksItem *application.MenuItem
|
||||
profileSubmenu *application.Menu
|
||||
profileSubmenu *application.Menu
|
||||
profileSubmenuItem *application.MenuItem
|
||||
profileEmailItem *application.MenuItem
|
||||
settingsItem *application.MenuItem
|
||||
debugItem *application.MenuItem
|
||||
updateItem *application.MenuItem
|
||||
@@ -220,6 +222,16 @@ func (t *Tray) buildMenu() *application.Menu {
|
||||
// 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)
|
||||
// profileSubmenuItem is the parent MenuItem whose label is the active
|
||||
// profile name. AddSubmenu returns the child *Menu, so we retrieve the
|
||||
// parent *MenuItem via FindByLabel immediately after insertion.
|
||||
t.profileSubmenuItem = menu.FindByLabel(menuProfiles)
|
||||
// profileEmailItem shows the account email of the active profile directly
|
||||
// in the main menu, below the Profiles submenu — matching the behaviour of
|
||||
// the legacy Fyne/systray UI. It is hidden until loadProfiles resolves a
|
||||
// non-empty email for the active profile.
|
||||
t.profileEmailItem = menu.Add("").SetEnabled(false)
|
||||
t.profileEmailItem.SetHidden(true)
|
||||
menu.AddSeparator()
|
||||
// Only the action that applies to the current state is visible: Connect
|
||||
// when disconnected, Disconnect when connected. applyStatus swaps them on
|
||||
@@ -736,6 +748,7 @@ func (t *Tray) loadProfiles() {
|
||||
|
||||
log.Infof("tray loadProfiles: received %d profile(s) for user %q", len(profiles), username)
|
||||
t.profileSubmenu.Clear()
|
||||
var activeName, activeEmail string
|
||||
for _, p := range profiles {
|
||||
name := p.Name
|
||||
active := p.IsActive
|
||||
@@ -748,6 +761,21 @@ func (t *Tray) loadProfiles() {
|
||||
}
|
||||
t.switchProfile(name)
|
||||
})
|
||||
if active {
|
||||
activeName = name
|
||||
activeEmail = p.Email
|
||||
}
|
||||
}
|
||||
if t.profileSubmenuItem != nil && activeName != "" {
|
||||
t.profileSubmenuItem.SetLabel(activeName)
|
||||
}
|
||||
if t.profileEmailItem != nil {
|
||||
if activeEmail != "" {
|
||||
t.profileEmailItem.SetLabel(fmt.Sprintf("(%s)", activeEmail))
|
||||
t.profileEmailItem.SetHidden(false)
|
||||
} else {
|
||||
t.profileEmailItem.SetHidden(true)
|
||||
}
|
||||
}
|
||||
// Wails v3 alpha's submenu.Update() builds a fresh, detached NSMenu on
|
||||
// darwin that never replaces the empty NSMenu attached to the parent
|
||||
|
||||
Reference in New Issue
Block a user