mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-15 01:49:08 +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
@@ -1,349 +0,0 @@
|
||||
//go:build !(linux && 386)
|
||||
|
||||
//go:generate fyne bundle -o quickactions_assets.go assets/connected.png
|
||||
//go:generate fyne bundle -o quickactions_assets.go -append assets/disconnected.png
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
type quickActionsUiState struct {
|
||||
connectionStatus string
|
||||
isToggleButtonEnabled bool
|
||||
isConnectionChanged bool
|
||||
toggleAction func()
|
||||
}
|
||||
|
||||
func newQuickActionsUiState() quickActionsUiState {
|
||||
return quickActionsUiState{
|
||||
connectionStatus: string(internal.StatusIdle),
|
||||
isToggleButtonEnabled: false,
|
||||
isConnectionChanged: false,
|
||||
}
|
||||
}
|
||||
|
||||
type clientConnectionStatusProvider interface {
|
||||
connectionStatus(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type daemonClientConnectionStatusProvider struct {
|
||||
client proto.DaemonServiceClient
|
||||
}
|
||||
|
||||
func (d daemonClientConnectionStatusProvider) connectionStatus(ctx context.Context) (string, error) {
|
||||
childCtx, cancel := context.WithTimeout(ctx, 400*time.Millisecond)
|
||||
defer cancel()
|
||||
status, err := d.client.Status(childCtx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return status.Status, nil
|
||||
}
|
||||
|
||||
type clientCommand interface {
|
||||
execute() error
|
||||
}
|
||||
|
||||
type connectCommand struct {
|
||||
connectClient func() error
|
||||
}
|
||||
|
||||
func (c connectCommand) execute() error {
|
||||
return c.connectClient()
|
||||
}
|
||||
|
||||
type disconnectCommand struct {
|
||||
disconnectClient func() error
|
||||
}
|
||||
|
||||
func (c disconnectCommand) execute() error {
|
||||
return c.disconnectClient()
|
||||
}
|
||||
|
||||
type quickActionsViewModel struct {
|
||||
provider clientConnectionStatusProvider
|
||||
connect clientCommand
|
||||
disconnect clientCommand
|
||||
uiChan chan quickActionsUiState
|
||||
isWatchingConnectionStatus atomic.Bool
|
||||
}
|
||||
|
||||
func newQuickActionsViewModel(ctx context.Context, provider clientConnectionStatusProvider, connect, disconnect clientCommand, uiChan chan quickActionsUiState) {
|
||||
viewModel := quickActionsViewModel{
|
||||
provider: provider,
|
||||
connect: connect,
|
||||
disconnect: disconnect,
|
||||
uiChan: uiChan,
|
||||
}
|
||||
|
||||
viewModel.isWatchingConnectionStatus.Store(true)
|
||||
|
||||
// base UI status
|
||||
uiChan <- newQuickActionsUiState()
|
||||
|
||||
// this retrieves the current connection status
|
||||
// and pushes the UI state that reflects it via uiChan
|
||||
go viewModel.watchConnectionStatus(ctx)
|
||||
}
|
||||
|
||||
func (q *quickActionsViewModel) updateUiState(ctx context.Context) {
|
||||
uiState := newQuickActionsUiState()
|
||||
connectionStatus, err := q.provider.connectionStatus(ctx)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Status: Error - %v", err)
|
||||
q.uiChan <- uiState
|
||||
return
|
||||
}
|
||||
|
||||
if connectionStatus == string(internal.StatusConnected) {
|
||||
uiState.toggleAction = func() {
|
||||
q.executeCommand(q.disconnect)
|
||||
}
|
||||
} else {
|
||||
uiState.toggleAction = func() {
|
||||
q.executeCommand(q.connect)
|
||||
}
|
||||
}
|
||||
|
||||
uiState.isToggleButtonEnabled = true
|
||||
uiState.connectionStatus = connectionStatus
|
||||
q.uiChan <- uiState
|
||||
}
|
||||
|
||||
func (q *quickActionsViewModel) watchConnectionStatus(ctx context.Context) {
|
||||
ticker := time.NewTicker(1000 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if q.isWatchingConnectionStatus.Load() {
|
||||
q.updateUiState(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *quickActionsViewModel) executeCommand(command clientCommand) {
|
||||
uiState := newQuickActionsUiState()
|
||||
// newQuickActionsUiState starts with Idle connection status,
|
||||
// and all that's necessary here is to just disable the toggle button.
|
||||
uiState.connectionStatus = ""
|
||||
|
||||
q.uiChan <- uiState
|
||||
|
||||
q.isWatchingConnectionStatus.Store(false)
|
||||
|
||||
err := command.execute()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Status: Error - %v", err)
|
||||
q.isWatchingConnectionStatus.Store(true)
|
||||
} else {
|
||||
uiState = newQuickActionsUiState()
|
||||
uiState.isConnectionChanged = true
|
||||
q.uiChan <- uiState
|
||||
}
|
||||
}
|
||||
|
||||
func getSystemTrayName() string {
|
||||
os := runtime.GOOS
|
||||
switch os {
|
||||
case "darwin":
|
||||
return "menu bar"
|
||||
default:
|
||||
return "system tray"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceClient) getNetBirdImage(name string, content []byte) *canvas.Image {
|
||||
imageSize := fyne.NewSize(64, 64)
|
||||
|
||||
resource := fyne.NewStaticResource(name, content)
|
||||
image := canvas.NewImageFromResource(resource)
|
||||
image.FillMode = canvas.ImageFillContain
|
||||
image.SetMinSize(imageSize)
|
||||
image.Resize(imageSize)
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
type quickActionsUiComponents struct {
|
||||
content *fyne.Container
|
||||
toggleConnectionButton *widget.Button
|
||||
connectedLabelText, disconnectedLabelText string
|
||||
connectedImage, disconnectedImage *canvas.Image
|
||||
connectedCircleRes, disconnectedCircleRes fyne.Resource
|
||||
}
|
||||
|
||||
// applyQuickActionsUiState applies a single UI state to the quick actions window.
|
||||
// It closes the window and returns true if the connection status has changed,
|
||||
// in which case the caller should stop processing further states.
|
||||
func (s *serviceClient) applyQuickActionsUiState(
|
||||
uiState quickActionsUiState,
|
||||
components quickActionsUiComponents,
|
||||
) bool {
|
||||
if uiState.isConnectionChanged {
|
||||
fyne.DoAndWait(func() {
|
||||
s.wQuickActions.Close()
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
var logo *canvas.Image
|
||||
var buttonText string
|
||||
var buttonIcon fyne.Resource
|
||||
|
||||
if uiState.connectionStatus == string(internal.StatusConnected) {
|
||||
buttonText = components.connectedLabelText
|
||||
buttonIcon = components.connectedCircleRes
|
||||
logo = components.connectedImage
|
||||
} else if uiState.connectionStatus == string(internal.StatusIdle) {
|
||||
buttonText = components.disconnectedLabelText
|
||||
buttonIcon = components.disconnectedCircleRes
|
||||
logo = components.disconnectedImage
|
||||
}
|
||||
|
||||
fyne.DoAndWait(func() {
|
||||
if buttonText != "" {
|
||||
components.toggleConnectionButton.SetText(buttonText)
|
||||
}
|
||||
|
||||
if buttonIcon != nil {
|
||||
components.toggleConnectionButton.SetIcon(buttonIcon)
|
||||
}
|
||||
|
||||
if uiState.isToggleButtonEnabled {
|
||||
components.toggleConnectionButton.Enable()
|
||||
} else {
|
||||
components.toggleConnectionButton.Disable()
|
||||
}
|
||||
|
||||
components.toggleConnectionButton.OnTapped = func() {
|
||||
if uiState.toggleAction != nil {
|
||||
go uiState.toggleAction()
|
||||
}
|
||||
}
|
||||
|
||||
components.toggleConnectionButton.Refresh()
|
||||
|
||||
// the second position in the content's object array is the NetBird logo.
|
||||
if logo != nil {
|
||||
components.content.Objects[1] = logo
|
||||
components.content.Refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// showQuickActionsUI displays a simple window with the NetBird logo and a connection toggle button.
|
||||
func (s *serviceClient) showQuickActionsUI() {
|
||||
s.wQuickActions = s.app.NewWindow("NetBird")
|
||||
vmCtx, vmCancel := context.WithCancel(s.ctx)
|
||||
s.wQuickActions.SetOnClosed(vmCancel)
|
||||
|
||||
client, err := s.getSrvClient(defaultFailTimeout)
|
||||
|
||||
connCmd := connectCommand{
|
||||
connectClient: func() error {
|
||||
return s.menuUpClick(s.ctx)
|
||||
},
|
||||
}
|
||||
|
||||
disConnCmd := disconnectCommand{
|
||||
disconnectClient: func() error {
|
||||
return s.menuDownClick()
|
||||
},
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("get service client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
uiChan := make(chan quickActionsUiState, 1)
|
||||
newQuickActionsViewModel(vmCtx, daemonClientConnectionStatusProvider{client: client}, connCmd, disConnCmd, uiChan)
|
||||
|
||||
connectedImage := s.getNetBirdImage("netbird.png", iconAbout)
|
||||
disconnectedImage := s.getNetBirdImage("netbird-disconnected.png", iconAboutDisconnected)
|
||||
|
||||
connectedCircle := canvas.NewImageFromResource(resourceConnectedPng)
|
||||
disconnectedCircle := canvas.NewImageFromResource(resourceDisconnectedPng)
|
||||
|
||||
connectedLabelText := "Disconnect"
|
||||
disconnectedLabelText := "Connect"
|
||||
|
||||
toggleConnectionButton := widget.NewButtonWithIcon(disconnectedLabelText, disconnectedCircle.Resource, func() {
|
||||
// This button's tap function will be set when an ui state arrives via the uiChan channel.
|
||||
})
|
||||
|
||||
// Button starts disabled until the first ui state arrives.
|
||||
toggleConnectionButton.Disable()
|
||||
|
||||
hintLabelText := fmt.Sprintf("You can always access NetBird from your %s.", getSystemTrayName())
|
||||
hintLabel := widget.NewLabel(hintLabelText)
|
||||
|
||||
content := container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
disconnectedImage,
|
||||
layout.NewSpacer(),
|
||||
container.NewCenter(toggleConnectionButton),
|
||||
layout.NewSpacer(),
|
||||
container.NewCenter(hintLabel),
|
||||
)
|
||||
|
||||
// this watches for ui state updates.
|
||||
go func() {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-vmCtx.Done():
|
||||
return
|
||||
case uiState, ok := <-uiChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
closed := s.applyQuickActionsUiState(
|
||||
uiState,
|
||||
quickActionsUiComponents{
|
||||
content,
|
||||
toggleConnectionButton,
|
||||
connectedLabelText, disconnectedLabelText,
|
||||
connectedImage, disconnectedImage,
|
||||
connectedCircle.Resource, disconnectedCircle.Resource,
|
||||
},
|
||||
)
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.wQuickActions.SetContent(content)
|
||||
s.wQuickActions.Resize(fyne.NewSize(400, 200))
|
||||
s.wQuickActions.SetFixedSize(true)
|
||||
s.wQuickActions.Show()
|
||||
}
|
||||
Reference in New Issue
Block a user