Add multiple tabs for route selection (#2198)

Add all routes, overlapping and exit routes tabs
This commit is contained in:
Maycon Santos 2024-06-27 14:32:30 +02:00 committed by GitHub
parent d2157bda66
commit 4cd4f88666
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -17,28 +17,56 @@ import (
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
) )
const (
allRoutesText = "All routes"
overlappingRoutesText = "Overlapping routes"
exitNodeRoutesText = "Exit-node routes"
allRoutes filter = "all"
overlappingRoutes filter = "overlapping"
exitNodeRoutes filter = "exit-node"
)
type filter string
func (s *serviceClient) showRoutesUI() { func (s *serviceClient) showRoutesUI() {
s.wRoutes = s.app.NewWindow("NetBird Routes") s.wRoutes = s.app.NewWindow("NetBird Routes")
grid := container.New(layout.NewGridLayout(3)) allGrid := container.New(layout.NewGridLayout(3))
go s.updateRoutes(grid) go s.updateRoutes(allGrid, allRoutes)
overlappingGrid := container.New(layout.NewGridLayout(3))
exitNodeGrid := container.New(layout.NewGridLayout(3))
routeCheckContainer := container.NewVBox() routeCheckContainer := container.NewVBox()
routeCheckContainer.Add(grid) tabs := container.NewAppTabs(
container.NewTabItem(allRoutesText, allGrid),
container.NewTabItem(overlappingRoutesText, overlappingGrid),
container.NewTabItem(exitNodeRoutesText, exitNodeGrid),
)
tabs.OnSelected = func(item *container.TabItem) {
s.updateRoutesBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
}
tabs.OnUnselected = func(item *container.TabItem) {
grid, _ := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
grid.Objects = nil
}
routeCheckContainer.Add(tabs)
scrollContainer := container.NewVScroll(routeCheckContainer) scrollContainer := container.NewVScroll(routeCheckContainer)
scrollContainer.SetMinSize(fyne.NewSize(200, 300)) scrollContainer.SetMinSize(fyne.NewSize(200, 300))
buttonBox := container.NewHBox( buttonBox := container.NewHBox(
layout.NewSpacer(), layout.NewSpacer(),
widget.NewButton("Refresh", func() { widget.NewButton("Refresh", func() {
s.updateRoutes(grid) s.updateRoutesBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
}), }),
widget.NewButton("Select all", func() { widget.NewButton("Select all", func() {
s.selectAllRoutes() _, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
s.updateRoutes(grid) s.selectAllFilteredRoutes(f)
s.updateRoutesBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
}), }),
widget.NewButton("Deselect All", func() { widget.NewButton("Deselect All", func() {
s.deselectAllRoutes() _, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
s.updateRoutes(grid) s.deselectAllFilteredRoutes(f)
s.updateRoutesBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodeGrid)
}), }),
layout.NewSpacer(), layout.NewSpacer(),
) )
@ -48,18 +76,12 @@ func (s *serviceClient) showRoutesUI() {
s.wRoutes.SetContent(content) s.wRoutes.SetContent(content)
s.wRoutes.Show() s.wRoutes.Show()
s.startAutoRefresh(5*time.Second, grid) s.startAutoRefresh(10*time.Second, tabs, allGrid, overlappingGrid, exitNodeGrid)
}
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
} }
func (s *serviceClient) updateRoutes(grid *fyne.Container, f filter) {
grid.Objects = nil grid.Objects = nil
grid.Refresh()
idHeader := widget.NewLabelWithStyle(" ID", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) idHeader := widget.NewLabelWithStyle(" ID", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
networkHeader := widget.NewLabelWithStyle("Network/Domains", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) networkHeader := widget.NewLabelWithStyle("Network/Domains", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
resolvedIPsHeader := widget.NewLabelWithStyle("Resolved IPs", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) resolvedIPsHeader := widget.NewLabelWithStyle("Resolved IPs", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
@ -67,7 +89,13 @@ func (s *serviceClient) updateRoutes(grid *fyne.Container) {
grid.Add(idHeader) grid.Add(idHeader)
grid.Add(networkHeader) grid.Add(networkHeader)
grid.Add(resolvedIPsHeader) grid.Add(resolvedIPsHeader)
for _, route := range routes {
filteredRoutes, err := s.getFilteredRoutes(f)
if err != nil {
return
}
for _, route := range filteredRoutes {
r := route r := route
checkBox := widget.NewCheck(r.GetID(), func(checked bool) { checkBox := widget.NewCheck(r.GetID(), func(checked bool) {
@ -119,6 +147,51 @@ func (s *serviceClient) updateRoutes(grid *fyne.Container) {
grid.Refresh() grid.Refresh()
} }
func (s *serviceClient) getFilteredRoutes(f filter) ([]*proto.Route, error) {
routes, err := s.fetchRoutes()
if err != nil {
log.Errorf("get client: %v", err)
s.showError(fmt.Errorf("get client: %v", err))
return nil, err
}
switch f {
case overlappingRoutes:
return getOverlappingRoutes(routes), nil
case exitNodeRoutes:
return getExitNodeRoutes(routes), nil
default:
}
return routes, nil
}
func getOverlappingRoutes(routes []*proto.Route) []*proto.Route {
var filteredRoutes []*proto.Route
existingRange := make(map[string][]*proto.Route)
for _, route := range routes {
if r, exists := existingRange[route.GetNetwork()]; exists {
r = append(r, route)
existingRange[route.GetNetwork()] = r
} else {
existingRange[route.GetNetwork()] = []*proto.Route{route}
}
}
for _, r := range existingRange {
if len(r) > 1 {
filteredRoutes = append(filteredRoutes, r...)
}
}
return filteredRoutes
}
func getExitNodeRoutes(routes []*proto.Route) []*proto.Route {
var filteredRoutes []*proto.Route
for _, route := range routes {
if route.Network == "0.0.0.0/0" {
filteredRoutes = append(filteredRoutes, route)
}
}
return filteredRoutes
}
func (s *serviceClient) fetchRoutes() ([]*proto.Route, error) { func (s *serviceClient) fetchRoutes() ([]*proto.Route, error) {
conn, err := s.getSrvClient(defaultFailTimeout) conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil { if err != nil {
@ -163,16 +236,14 @@ func (s *serviceClient) selectRoute(id string, checked bool) {
} }
} }
func (s *serviceClient) selectAllRoutes() { func (s *serviceClient) selectAllFilteredRoutes(f filter) {
conn, err := s.getSrvClient(defaultFailTimeout) conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil { if err != nil {
log.Errorf("get client: %v", err) log.Errorf("get client: %v", err)
return return
} }
req := &proto.SelectRoutesRequest{ req := s.getRoutesRequest(f, true)
All: true,
}
if _, err := conn.SelectRoutes(s.ctx, req); err != nil { if _, err := conn.SelectRoutes(s.ctx, req); err != nil {
log.Errorf("failed to select all routes: %v", err) log.Errorf("failed to select all routes: %v", err)
s.showError(fmt.Errorf("failed to select all routes: %v", err)) s.showError(fmt.Errorf("failed to select all routes: %v", err))
@ -182,16 +253,14 @@ func (s *serviceClient) selectAllRoutes() {
log.Debug("All routes selected") log.Debug("All routes selected")
} }
func (s *serviceClient) deselectAllRoutes() { func (s *serviceClient) deselectAllFilteredRoutes(f filter) {
conn, err := s.getSrvClient(defaultFailTimeout) conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil { if err != nil {
log.Errorf("get client: %v", err) log.Errorf("get client: %v", err)
return return
} }
req := &proto.SelectRoutesRequest{ req := s.getRoutesRequest(f, false)
All: true,
}
if _, err := conn.DeselectRoutes(s.ctx, req); err != nil { if _, err := conn.DeselectRoutes(s.ctx, req); err != nil {
log.Errorf("failed to deselect all routes: %v", err) log.Errorf("failed to deselect all routes: %v", err)
s.showError(fmt.Errorf("failed to deselect all routes: %v", err)) s.showError(fmt.Errorf("failed to deselect all routes: %v", err))
@ -201,17 +270,34 @@ func (s *serviceClient) deselectAllRoutes() {
log.Debug("All routes deselected") log.Debug("All routes deselected")
} }
func (s *serviceClient) getRoutesRequest(f filter, appendRoute bool) *proto.SelectRoutesRequest {
req := &proto.SelectRoutesRequest{}
if f == allRoutes {
req.All = true
} else {
routes, err := s.getFilteredRoutes(f)
if err != nil {
return nil
}
for _, route := range routes {
req.RouteIDs = append(req.RouteIDs, route.GetID())
}
req.Append = appendRoute
}
return req
}
func (s *serviceClient) showError(err error) { func (s *serviceClient) showError(err error) {
wrappedMessage := wrapText(err.Error(), 50) wrappedMessage := wrapText(err.Error(), 50)
dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wRoutes) dialog.ShowError(fmt.Errorf("%s", wrappedMessage), s.wRoutes)
} }
func (s *serviceClient) startAutoRefresh(interval time.Duration, grid *fyne.Container) { func (s *serviceClient) startAutoRefresh(interval time.Duration, tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) {
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
go func() { go func() {
for range ticker.C { for range ticker.C {
s.updateRoutes(grid) s.updateRoutesBasedOnDisplayTab(tabs, allGrid, overlappingGrid, exitNodesGrid)
} }
}() }()
@ -220,6 +306,23 @@ func (s *serviceClient) startAutoRefresh(interval time.Duration, grid *fyne.Cont
}) })
} }
func (s *serviceClient) updateRoutesBasedOnDisplayTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) {
grid, f := getGridAndFilterFromTab(tabs, allGrid, overlappingGrid, exitNodesGrid)
s.wRoutes.Content().Refresh()
s.updateRoutes(grid, f)
}
func getGridAndFilterFromTab(tabs *container.AppTabs, allGrid, overlappingGrid, exitNodesGrid *fyne.Container) (*fyne.Container, filter) {
switch tabs.Selected().Text {
case overlappingRoutesText:
return overlappingGrid, overlappingRoutes
case exitNodeRoutesText:
return exitNodesGrid, exitNodeRoutes
default:
return allGrid, allRoutes
}
}
// wrapText inserts newlines into the text to ensure that each line is // wrapText inserts newlines into the text to ensure that each line is
// no longer than 'lineLength' runes. // no longer than 'lineLength' runes.
func wrapText(text string, lineLength int) string { func wrapText(text string, lineLength int) string {