Add route selection functionality for CLI and GUI (#1865)

This commit is contained in:
Viktor Liu
2024-04-23 14:42:53 +02:00
committed by GitHub
parent 3477108ce7
commit f51dc13f8c
20 changed files with 1650 additions and 146 deletions

View File

@@ -1,7 +1,5 @@
//go:build !(linux && 386)
// +build !linux !386
// skipping linux 32 bits build and tests
package main
import (
@@ -58,6 +56,8 @@ func main() {
var showSettings bool
flag.BoolVar(&showSettings, "settings", false, "run settings windows")
var showRoutes bool
flag.BoolVar(&showRoutes, "routes", false, "run routes windows")
var errorMSG string
flag.StringVar(&errorMSG, "error-msg", "", "displays a error message window")
@@ -71,8 +71,8 @@ func main() {
return
}
client := newServiceClient(daemonAddr, a, showSettings)
if showSettings {
client := newServiceClient(daemonAddr, a, showSettings, showRoutes)
if showSettings || showRoutes {
a.Run()
} else {
if err := checkPIDFile(); err != nil {
@@ -135,6 +135,7 @@ type serviceClient struct {
mVersionDaemon *systray.MenuItem
mUpdate *systray.MenuItem
mQuit *systray.MenuItem
mRoutes *systray.MenuItem
// application with main windows.
app fyne.App
@@ -159,12 +160,15 @@ type serviceClient struct {
daemonVersion string
updateIndicationLock sync.Mutex
isUpdateIconActive bool
showRoutes bool
wRoutes fyne.Window
}
// newServiceClient instance constructor
//
// This constructor also builds the UI elements for the settings window.
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes bool) *serviceClient {
s := &serviceClient{
ctx: context.Background(),
addr: addr,
@@ -172,6 +176,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient
sendNotification: false,
showSettings: showSettings,
showRoutes: showRoutes,
update: version.NewUpdate(),
}
@@ -191,14 +196,16 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient
}
if showSettings {
s.showUIElements()
s.showSettingsUI()
return s
} else if showRoutes {
s.showRoutesUI()
}
return s
}
func (s *serviceClient) showUIElements() {
func (s *serviceClient) showSettingsUI() {
// add settings window UI elements.
s.wSettings = s.app.NewWindow("NetBird Settings")
s.iMngURL = widget.NewEntry()
@@ -416,6 +423,7 @@ func (s *serviceClient) updateStatus() error {
s.mStatus.SetTitle("Connected")
s.mUp.Disable()
s.mDown.Enable()
s.mRoutes.Enable()
systrayIconState = true
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
s.connected = false
@@ -428,6 +436,7 @@ func (s *serviceClient) updateStatus() error {
s.mStatus.SetTitle("Disconnected")
s.mDown.Disable()
s.mUp.Enable()
s.mRoutes.Disable()
systrayIconState = false
}
@@ -483,9 +492,11 @@ func (s *serviceClient) onTrayReady() {
s.mUp = systray.AddMenuItem("Connect", "Connect")
s.mDown = systray.AddMenuItem("Disconnect", "Disconnect")
s.mDown.Disable()
s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Wiretrustee Admin Panel")
s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Netbird Admin Panel")
systray.AddSeparator()
s.mSettings = systray.AddMenuItem("Settings", "Settings of the application")
s.mRoutes = systray.AddMenuItem("Network Routes", "Open the routes management window")
s.mRoutes.Disable()
systray.AddSeparator()
s.mAbout = systray.AddMenuItem("About", "About")
@@ -557,6 +568,12 @@ func (s *serviceClient) onTrayReady() {
if err != nil {
log.Errorf("%s", err)
}
case <-s.mRoutes.ClickedCh:
s.mRoutes.Disable()
go func() {
defer s.mRoutes.Enable()
s.runSelfCommand("routes", "true")
}()
}
if err != nil {
log.Errorf("process connection: %v", err)

203
client/ui/route.go Normal file
View File

@@ -0,0 +1,203 @@
//go:build !(linux && 386)
package main
import (
"fmt"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/proto"
)
func (s *serviceClient) showRoutesUI() {
s.wRoutes = s.app.NewWindow("NetBird Routes")
grid := container.New(layout.NewGridLayout(2))
go s.updateRoutes(grid)
routeCheckContainer := container.NewVBox()
routeCheckContainer.Add(grid)
scrollContainer := container.NewVScroll(routeCheckContainer)
scrollContainer.SetMinSize(fyne.NewSize(200, 300))
buttonBox := container.NewHBox(
layout.NewSpacer(),
widget.NewButton("Refresh", func() {
s.updateRoutes(grid)
}),
widget.NewButton("Select all", func() {
s.selectAllRoutes()
s.updateRoutes(grid)
}),
widget.NewButton("Deselect All", func() {
s.deselectAllRoutes()
s.updateRoutes(grid)
}),
layout.NewSpacer(),
)
content := container.NewBorder(nil, buttonBox, nil, nil, scrollContainer)
s.wRoutes.SetContent(content)
s.wRoutes.Show()
s.startAutoRefresh(5*time.Second, grid)
}
func (s *serviceClient) updateRoutes(grid *fyne.Container) {
routes, err := s.fetchRoutes()
if err != nil {
log.Errorf("get client: %v", err)
s.showError(fmt.Errorf("get client: %v", err))
return
}
grid.Objects = nil
idHeader := widget.NewLabelWithStyle(" ID", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
networkHeader := widget.NewLabelWithStyle("Network", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
grid.Add(idHeader)
grid.Add(networkHeader)
for _, route := range routes {
r := route
checkBox := widget.NewCheck(r.ID, func(checked bool) {
s.selectRoute(r.ID, checked)
})
checkBox.Checked = route.Selected
checkBox.Resize(fyne.NewSize(20, 20))
checkBox.Refresh()
grid.Add(checkBox)
grid.Add(widget.NewLabel(r.Network))
}
s.wRoutes.Content().Refresh()
}
func (s *serviceClient) fetchRoutes() ([]*proto.Route, error) {
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
return nil, fmt.Errorf("get client: %v", err)
}
resp, err := conn.ListRoutes(s.ctx, &proto.ListRoutesRequest{})
if err != nil {
return nil, fmt.Errorf("failed to list routes: %v", err)
}
return resp.Routes, nil
}
func (s *serviceClient) selectRoute(id string, checked bool) {
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
log.Errorf("get client: %v", err)
s.showError(fmt.Errorf("get client: %v", err))
return
}
req := &proto.SelectRoutesRequest{
RouteIDs: []string{id},
Append: checked,
}
if checked {
if _, err := conn.SelectRoutes(s.ctx, req); err != nil {
log.Errorf("failed to select route: %v", err)
s.showError(fmt.Errorf("failed to select route: %v", err))
return
}
log.Infof("Route %s selected", id)
} else {
if _, err := conn.DeselectRoutes(s.ctx, req); err != nil {
log.Errorf("failed to deselect route: %v", err)
s.showError(fmt.Errorf("failed to deselect route: %v", err))
return
}
log.Infof("Route %s deselected", id)
}
}
func (s *serviceClient) selectAllRoutes() {
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
log.Errorf("get client: %v", err)
return
}
req := &proto.SelectRoutesRequest{
All: true,
}
if _, err := conn.SelectRoutes(s.ctx, req); err != nil {
log.Errorf("failed to select all routes: %v", err)
s.showError(fmt.Errorf("failed to select all routes: %v", err))
return
}
log.Debug("All routes selected")
}
func (s *serviceClient) deselectAllRoutes() {
conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil {
log.Errorf("get client: %v", err)
return
}
req := &proto.SelectRoutesRequest{
All: true,
}
if _, err := conn.DeselectRoutes(s.ctx, req); err != nil {
log.Errorf("failed to deselect all routes: %v", err)
s.showError(fmt.Errorf("failed to deselect all routes: %v", err))
return
}
log.Debug("All routes deselected")
}
func (s *serviceClient) showError(err error) {
wrappedMessage := wrapText(err.Error(), 50)
dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wRoutes)
}
func (s *serviceClient) startAutoRefresh(interval time.Duration, grid *fyne.Container) {
ticker := time.NewTicker(interval)
go func() {
for range ticker.C {
s.updateRoutes(grid)
}
}()
s.wRoutes.SetOnClosed(func() {
ticker.Stop()
})
}
// wrapText inserts newlines into the text to ensure that each line is
// no longer than 'lineLength' runes.
func wrapText(text string, lineLength int) string {
var sb strings.Builder
var currentLineLength int
for _, runeValue := range text {
sb.WriteRune(runeValue)
currentLineLength++
if currentLineLength >= lineLength || runeValue == '\n' {
sb.WriteRune('\n')
currentLineLength = 0
}
}
return sb.String()
}