mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-23 22:09:09 +02:00
[management,client] 0.75.0 release with new desktop UI (#6473)
- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow. - **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend. - **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher. - **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements. - **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms. - **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows). - **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow. Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com> Co-authored-by: Eduard Gert <kontakt@eduardgert.de> Co-authored-by: braginini <bangvalo@gmail.com> Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Co-authored-by: riccardom <riccardomanfrin@gmail.com>
This commit is contained in:
co-authored by
Zoltan Papp
Eduard Gert
braginini
Pascal Fischer
riccardom
parent
c9d387bd0d
commit
91acb8147c
@@ -0,0 +1,197 @@
|
||||
//go:build !android && !ios && !freebsd && !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
|
||||
"github.com/netbirdio/netbird/client/ui/services"
|
||||
"github.com/netbirdio/netbird/client/ui/updater"
|
||||
)
|
||||
|
||||
// trayUpdater owns the tray UI that reacts to auto-update. Composed inside Tray.
|
||||
type trayUpdater struct {
|
||||
app *application.App
|
||||
window *application.WebviewWindow
|
||||
update *services.Update
|
||||
notifier *notifications.NotificationService
|
||||
loc *Localizer
|
||||
onIconChange func()
|
||||
// onMenuChange drives a full tray relayout: the update row lives in the
|
||||
// About submenu, which KDE/Plasma caches on first open and never re-fetches
|
||||
// on a plain SetLabel/SetHidden — only a relayout (fresh submenu ids) repaints.
|
||||
onMenuChange func()
|
||||
|
||||
mu sync.Mutex
|
||||
item *application.MenuItem
|
||||
state updater.State
|
||||
notifiedVersion string
|
||||
progressWindowOpen bool
|
||||
}
|
||||
|
||||
func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *notifications.NotificationService, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater {
|
||||
u := &trayUpdater{
|
||||
app: app,
|
||||
window: window,
|
||||
update: update,
|
||||
notifier: notifier,
|
||||
loc: loc,
|
||||
onIconChange: onIconChange,
|
||||
onMenuChange: onMenuChange,
|
||||
}
|
||||
app.Event.On(updater.EventStateChanged, u.onStateEvent)
|
||||
// Seed from cached state to cover an event that fired before wiring completed.
|
||||
u.state = update.GetState()
|
||||
return u
|
||||
}
|
||||
|
||||
// attach (re)binds the menu item on each Tray.buildMenu run. The caller owns the
|
||||
// item's OnClick handler.
|
||||
func (u *trayUpdater) attach(item *application.MenuItem) {
|
||||
u.mu.Lock()
|
||||
u.item = item
|
||||
state := u.state
|
||||
u.mu.Unlock()
|
||||
u.refreshMenuItem(state)
|
||||
}
|
||||
|
||||
// hasUpdate reports whether the tray should paint the "update available" icon.
|
||||
func (u *trayUpdater) hasUpdate() bool {
|
||||
u.mu.Lock()
|
||||
defer u.mu.Unlock()
|
||||
return u.state.Available
|
||||
}
|
||||
|
||||
// applyLanguage re-renders the menu item label after a locale switch.
|
||||
func (u *trayUpdater) applyLanguage() {
|
||||
u.mu.Lock()
|
||||
state := u.state
|
||||
u.mu.Unlock()
|
||||
u.refreshMenuItem(state)
|
||||
}
|
||||
|
||||
// handleClick opens the GitHub releases page when not Enforced, otherwise shows
|
||||
// the progress page and asks the daemon to start the installer.
|
||||
func (u *trayUpdater) handleClick() {
|
||||
u.mu.Lock()
|
||||
state := u.state
|
||||
u.mu.Unlock()
|
||||
|
||||
if !state.Enforced {
|
||||
_ = u.app.Browser.OpenURL(urlGitHubReleases)
|
||||
return
|
||||
}
|
||||
|
||||
u.openProgressWindow(state.Version)
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if _, err := u.update.Trigger(ctx); err != nil {
|
||||
log.Errorf("trigger update: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (u *trayUpdater) onStateEvent(ev *application.CustomEvent) {
|
||||
st, ok := ev.Data.(updater.State)
|
||||
if !ok {
|
||||
log.Warnf("update state event payload not UpdateState: %T", ev.Data)
|
||||
return
|
||||
}
|
||||
u.applyState(st)
|
||||
}
|
||||
|
||||
// applyState diffs st against the cached state and drives the resulting side
|
||||
// effects: icon repaint, menu refresh, new-version notification, progress window.
|
||||
func (u *trayUpdater) applyState(st updater.State) {
|
||||
u.mu.Lock()
|
||||
prev := u.state
|
||||
u.state = st
|
||||
|
||||
sendNotify := st.Available && st.Version != "" && st.Version != u.notifiedVersion
|
||||
if sendNotify {
|
||||
u.notifiedVersion = st.Version
|
||||
}
|
||||
|
||||
showWindow := st.Installing && !u.progressWindowOpen
|
||||
if showWindow {
|
||||
u.progressWindowOpen = true
|
||||
} else if !st.Installing {
|
||||
u.progressWindowOpen = false
|
||||
}
|
||||
u.mu.Unlock()
|
||||
|
||||
// Full relayout rather than in-place: KDE layout-caches the About submenu, so
|
||||
// a direct SetLabel/SetHidden wouldn't paint. Fall back if no hook was wired.
|
||||
if u.onMenuChange != nil {
|
||||
u.onMenuChange()
|
||||
} else {
|
||||
u.refreshMenuItem(st)
|
||||
}
|
||||
if prev.Available != st.Available && u.onIconChange != nil {
|
||||
u.onIconChange()
|
||||
}
|
||||
if sendNotify {
|
||||
u.sendUpdateNotification(st)
|
||||
}
|
||||
if showWindow {
|
||||
u.openProgressWindow(st.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *trayUpdater) refreshMenuItem(st updater.State) {
|
||||
u.mu.Lock()
|
||||
item := u.item
|
||||
u.mu.Unlock()
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !st.Available {
|
||||
item.SetHidden(true)
|
||||
return
|
||||
}
|
||||
if st.Enforced {
|
||||
item.SetLabel(u.loc.T("tray.menu.installVersion", "version", st.Version))
|
||||
} else {
|
||||
item.SetLabel(u.loc.T("tray.menu.downloadLatest"))
|
||||
}
|
||||
item.SetHidden(false)
|
||||
}
|
||||
|
||||
func (u *trayUpdater) sendUpdateNotification(st updater.State) {
|
||||
if u.notifier == nil {
|
||||
return
|
||||
}
|
||||
body := u.loc.T("notify.update.body", "version", st.Version)
|
||||
if st.Enforced {
|
||||
body += u.loc.T("notify.update.enforcedSuffix")
|
||||
}
|
||||
_ = safeSendNotification(u.notifier.SendNotification, "update", notifications.NotificationOptions{
|
||||
ID: notifyIDUpdatePrefix + st.Version,
|
||||
Title: u.loc.T("notify.update.title"),
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
// openProgressWindow points the main window at the /update progress page and
|
||||
// brings it forward.
|
||||
func (u *trayUpdater) openProgressWindow(version string) {
|
||||
if u.window == nil {
|
||||
return
|
||||
}
|
||||
url := "/#/update"
|
||||
if version != "" {
|
||||
url += "?version=" + version
|
||||
}
|
||||
u.window.SetURL(url)
|
||||
u.window.Show()
|
||||
u.window.Focus()
|
||||
}
|
||||
Reference in New Issue
Block a user