mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 15:49:06 +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,730 +0,0 @@
|
||||
//go:build !(linux && 386)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
uptypes "github.com/netbirdio/netbird/upload-server/types"
|
||||
"github.com/netbirdio/netbird/version"
|
||||
)
|
||||
|
||||
// Initial state for the debug collection
|
||||
type debugInitialState struct {
|
||||
wasDown bool
|
||||
needsRestoreUp bool
|
||||
logLevel proto.LogLevel
|
||||
isLevelTrace bool
|
||||
}
|
||||
|
||||
// Debug collection parameters
|
||||
type debugCollectionParams struct {
|
||||
duration time.Duration
|
||||
anonymize bool
|
||||
systemInfo bool
|
||||
upload bool
|
||||
uploadURL string
|
||||
enablePersistence bool
|
||||
capture bool
|
||||
}
|
||||
|
||||
// UI components for progress tracking
|
||||
type progressUI struct {
|
||||
statusLabel *widget.Label
|
||||
progressBar *widget.ProgressBar
|
||||
uiControls []fyne.Disableable
|
||||
window fyne.Window
|
||||
}
|
||||
|
||||
func (s *serviceClient) showDebugUI() {
|
||||
w := s.app.NewWindow("NetBird Debug")
|
||||
w.SetOnClosed(s.cancel)
|
||||
w.Resize(fyne.NewSize(600, 500))
|
||||
w.SetFixedSize(true)
|
||||
|
||||
anonymizeCheck := widget.NewCheck("Anonymize sensitive information (public IPs, domains, ...)", nil)
|
||||
systemInfoCheck := widget.NewCheck("Include system information (routes, interfaces, ...)", nil)
|
||||
systemInfoCheck.SetChecked(true)
|
||||
captureCheck := widget.NewCheck("Include packet capture", nil)
|
||||
uploadCheck := widget.NewCheck("Upload bundle automatically after creation", nil)
|
||||
uploadCheck.SetChecked(true)
|
||||
|
||||
uploadURLContainer, uploadURL := s.buildUploadSection(uploadCheck)
|
||||
|
||||
debugModeContainer, runForDurationCheck, durationInput, noteLabel := s.buildDurationSection()
|
||||
|
||||
statusLabel := widget.NewLabel("")
|
||||
statusLabel.Hide()
|
||||
progressBar := widget.NewProgressBar()
|
||||
progressBar.Hide()
|
||||
createButton := widget.NewButton("Create Debug Bundle", nil)
|
||||
|
||||
uiControls := []fyne.Disableable{
|
||||
anonymizeCheck, systemInfoCheck, captureCheck,
|
||||
uploadCheck, uploadURL, runForDurationCheck, durationInput, createButton,
|
||||
}
|
||||
|
||||
createButton.OnTapped = s.getCreateHandler(
|
||||
statusLabel, progressBar, uploadCheck, uploadURL,
|
||||
anonymizeCheck, systemInfoCheck, captureCheck,
|
||||
runForDurationCheck, durationInput, uiControls, w,
|
||||
)
|
||||
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel("Create a debug bundle to help troubleshoot issues with NetBird"),
|
||||
widget.NewLabel(""),
|
||||
anonymizeCheck, systemInfoCheck, captureCheck,
|
||||
uploadCheck, uploadURLContainer,
|
||||
widget.NewLabel(""),
|
||||
debugModeContainer, noteLabel,
|
||||
widget.NewLabel(""),
|
||||
statusLabel, progressBar, createButton,
|
||||
)
|
||||
|
||||
w.SetContent(container.NewPadded(content))
|
||||
w.Show()
|
||||
}
|
||||
|
||||
func (s *serviceClient) buildUploadSection(uploadCheck *widget.Check) (*fyne.Container, *widget.Entry) {
|
||||
uploadURL := widget.NewEntry()
|
||||
uploadURL.SetText(uptypes.DefaultBundleURL)
|
||||
uploadURL.SetPlaceHolder("Enter upload URL")
|
||||
|
||||
uploadURLContainer := container.NewVBox(widget.NewLabel("Debug upload URL:"), uploadURL)
|
||||
|
||||
uploadCheck.OnChanged = func(checked bool) {
|
||||
if checked {
|
||||
uploadURLContainer.Show()
|
||||
} else {
|
||||
uploadURLContainer.Hide()
|
||||
}
|
||||
}
|
||||
return uploadURLContainer, uploadURL
|
||||
}
|
||||
|
||||
func (s *serviceClient) buildDurationSection() (*fyne.Container, *widget.Check, *widget.Entry, *widget.Label) {
|
||||
runForDurationCheck := widget.NewCheck("Run with trace logs before creating bundle", nil)
|
||||
runForDurationCheck.SetChecked(true)
|
||||
|
||||
forLabel := widget.NewLabel("for")
|
||||
durationInput := widget.NewEntry()
|
||||
durationInput.SetText("1")
|
||||
minutesLabel := widget.NewLabel("minute")
|
||||
durationInput.Validator = func(s string) error {
|
||||
return validateMinute(s, minutesLabel)
|
||||
}
|
||||
|
||||
noteLabel := widget.NewLabel("Note: NetBird will be brought up and down during collection")
|
||||
|
||||
runForDurationCheck.OnChanged = func(checked bool) {
|
||||
if checked {
|
||||
forLabel.Show()
|
||||
durationInput.Show()
|
||||
minutesLabel.Show()
|
||||
noteLabel.Show()
|
||||
} else {
|
||||
forLabel.Hide()
|
||||
durationInput.Hide()
|
||||
minutesLabel.Hide()
|
||||
noteLabel.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
modeContainer := container.NewHBox(runForDurationCheck, forLabel, durationInput, minutesLabel)
|
||||
return modeContainer, runForDurationCheck, durationInput, noteLabel
|
||||
}
|
||||
|
||||
func validateMinute(s string, minutesLabel *widget.Label) error {
|
||||
if val, err := strconv.Atoi(s); err != nil || val < 1 {
|
||||
return fmt.Errorf("must be a number ≥ 1")
|
||||
}
|
||||
if s == "1" {
|
||||
minutesLabel.SetText("minute")
|
||||
} else {
|
||||
minutesLabel.SetText("minutes")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// disableUIControls disables the provided UI controls
|
||||
func disableUIControls(controls []fyne.Disableable) {
|
||||
for _, control := range controls {
|
||||
control.Disable()
|
||||
}
|
||||
}
|
||||
|
||||
// enableUIControls enables the provided UI controls
|
||||
func enableUIControls(controls []fyne.Disableable) {
|
||||
for _, control := range controls {
|
||||
control.Enable()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceClient) getCreateHandler(
|
||||
statusLabel *widget.Label,
|
||||
progressBar *widget.ProgressBar,
|
||||
uploadCheck *widget.Check,
|
||||
uploadURL *widget.Entry,
|
||||
anonymizeCheck *widget.Check,
|
||||
systemInfoCheck *widget.Check,
|
||||
captureCheck *widget.Check,
|
||||
runForDurationCheck *widget.Check,
|
||||
duration *widget.Entry,
|
||||
uiControls []fyne.Disableable,
|
||||
w fyne.Window,
|
||||
) func() {
|
||||
return func() {
|
||||
disableUIControls(uiControls)
|
||||
statusLabel.Show()
|
||||
|
||||
var url string
|
||||
if uploadCheck.Checked {
|
||||
url = uploadURL.Text
|
||||
if url == "" {
|
||||
statusLabel.SetText("Error: Upload URL is required when upload is enabled")
|
||||
enableUIControls(uiControls)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
params := &debugCollectionParams{
|
||||
anonymize: anonymizeCheck.Checked,
|
||||
systemInfo: systemInfoCheck.Checked,
|
||||
capture: captureCheck.Checked,
|
||||
upload: uploadCheck.Checked,
|
||||
uploadURL: url,
|
||||
enablePersistence: true,
|
||||
}
|
||||
|
||||
runForDuration := runForDurationCheck.Checked
|
||||
if runForDuration {
|
||||
minutes, err := time.ParseDuration(duration.Text + "m")
|
||||
if err != nil {
|
||||
statusLabel.SetText(fmt.Sprintf("Error: Invalid duration: %v", err))
|
||||
enableUIControls(uiControls)
|
||||
return
|
||||
}
|
||||
params.duration = minutes
|
||||
|
||||
statusLabel.SetText(fmt.Sprintf("Running in debug mode for %d minutes...", int(minutes.Minutes())))
|
||||
progressBar.Show()
|
||||
progressBar.SetValue(0)
|
||||
|
||||
go s.handleRunForDuration(
|
||||
statusLabel,
|
||||
progressBar,
|
||||
uiControls,
|
||||
w,
|
||||
params,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
statusLabel.SetText("Creating debug bundle...")
|
||||
go s.handleDebugCreation(
|
||||
params,
|
||||
statusLabel,
|
||||
uiControls,
|
||||
w,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceClient) handleRunForDuration(
|
||||
statusLabel *widget.Label,
|
||||
progressBar *widget.ProgressBar,
|
||||
uiControls []fyne.Disableable,
|
||||
w fyne.Window,
|
||||
params *debugCollectionParams,
|
||||
) {
|
||||
progressUI := &progressUI{
|
||||
statusLabel: statusLabel,
|
||||
progressBar: progressBar,
|
||||
uiControls: uiControls,
|
||||
window: w,
|
||||
}
|
||||
|
||||
conn, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
handleError(progressUI, fmt.Sprintf("Failed to get client for debug: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
initialState, err := s.getInitialState(conn)
|
||||
if err != nil {
|
||||
handleError(progressUI, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
defer s.restoreServiceState(conn, initialState)
|
||||
|
||||
if err := s.collectDebugData(conn, initialState, params, progressUI); err != nil {
|
||||
handleError(progressUI, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.createDebugBundleFromCollection(conn, params, progressUI); err != nil {
|
||||
handleError(progressUI, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
progressUI.statusLabel.SetText("Bundle created successfully")
|
||||
}
|
||||
|
||||
// Get initial state of the service
|
||||
func (s *serviceClient) getInitialState(conn proto.DaemonServiceClient) (*debugInitialState, error) {
|
||||
statusResp, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(" get status: %v", err)
|
||||
}
|
||||
|
||||
logLevelResp, err := conn.GetLogLevel(s.ctx, &proto.GetLogLevelRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get log level: %v", err)
|
||||
}
|
||||
|
||||
wasDown := statusResp.Status != string(internal.StatusConnected) &&
|
||||
statusResp.Status != string(internal.StatusConnecting)
|
||||
|
||||
initialLogLevel := logLevelResp.GetLevel()
|
||||
initialLevelTrace := initialLogLevel >= proto.LogLevel_TRACE
|
||||
|
||||
return &debugInitialState{
|
||||
wasDown: wasDown,
|
||||
logLevel: initialLogLevel,
|
||||
isLevelTrace: initialLevelTrace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Handle progress tracking during collection
|
||||
func startProgressTracker(ctx context.Context, wg *sync.WaitGroup, duration time.Duration, progress *progressUI) {
|
||||
progress.progressBar.Show()
|
||||
progress.progressBar.SetValue(0)
|
||||
|
||||
startTime := time.Now()
|
||||
endTime := startTime.Add(duration)
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
remaining := time.Until(endTime)
|
||||
if remaining <= 0 {
|
||||
remaining = 0
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
progressVal := float64(elapsed) / float64(duration)
|
||||
if progressVal > 1.0 {
|
||||
progressVal = 1.0
|
||||
}
|
||||
|
||||
progress.progressBar.SetValue(progressVal)
|
||||
progress.statusLabel.SetText(fmt.Sprintf("Running with trace logs... %s remaining", formatDuration(remaining)))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func (s *serviceClient) configureServiceForDebug(
|
||||
conn proto.DaemonServiceClient,
|
||||
state *debugInitialState,
|
||||
params *debugCollectionParams,
|
||||
) {
|
||||
if state.wasDown {
|
||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||
log.Warnf("failed to bring service up: %v", err)
|
||||
} else {
|
||||
log.Info("Service brought up for debug")
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}
|
||||
|
||||
if !state.isLevelTrace {
|
||||
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
|
||||
log.Warnf("failed to set log level to TRACE: %v", err)
|
||||
} else {
|
||||
log.Info("Log level set to TRACE for debug")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
||||
log.Warnf("failed to bring service down: %v", err)
|
||||
} else {
|
||||
state.needsRestoreUp = !state.wasDown
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
if params.enablePersistence {
|
||||
if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{
|
||||
Enabled: true,
|
||||
}); err != nil {
|
||||
log.Warnf("failed to enable sync response persistence: %v", err)
|
||||
} else {
|
||||
log.Info("Sync response persistence enabled for debug")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||
log.Warnf("failed to bring service back up: %v", err)
|
||||
} else {
|
||||
state.needsRestoreUp = false
|
||||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil {
|
||||
log.Warnf("failed to start CPU profiling: %v", err)
|
||||
}
|
||||
|
||||
s.startBundleCaptureIfEnabled(conn, params)
|
||||
}
|
||||
|
||||
func (s *serviceClient) startBundleCaptureIfEnabled(conn proto.DaemonServiceClient, params *debugCollectionParams) {
|
||||
if !params.capture {
|
||||
return
|
||||
}
|
||||
|
||||
const maxCapture = 10 * time.Minute
|
||||
timeout := params.duration + 30*time.Second
|
||||
if timeout > maxCapture {
|
||||
timeout = maxCapture
|
||||
log.Warnf("packet capture clamped to %s (server maximum)", maxCapture)
|
||||
}
|
||||
if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{
|
||||
Timeout: durationpb.New(timeout),
|
||||
}); err != nil {
|
||||
log.Warnf("failed to start bundle capture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceClient) collectDebugData(
|
||||
conn proto.DaemonServiceClient,
|
||||
state *debugInitialState,
|
||||
params *debugCollectionParams,
|
||||
progress *progressUI,
|
||||
) error {
|
||||
ctx, cancel := context.WithTimeout(s.ctx, params.duration)
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
startProgressTracker(ctx, &wg, params.duration, progress)
|
||||
|
||||
s.configureServiceForDebug(conn, state, params)
|
||||
|
||||
wg.Wait()
|
||||
progress.progressBar.Hide()
|
||||
progress.statusLabel.SetText("Collecting debug data...")
|
||||
|
||||
if _, err := conn.StopCPUProfile(s.ctx, &proto.StopCPUProfileRequest{}); err != nil {
|
||||
log.Warnf("failed to stop CPU profiling: %v", err)
|
||||
}
|
||||
|
||||
if params.capture {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
||||
log.Warnf("failed to stop bundle capture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the debug bundle with collected data
|
||||
func (s *serviceClient) createDebugBundleFromCollection(
|
||||
conn proto.DaemonServiceClient,
|
||||
params *debugCollectionParams,
|
||||
progress *progressUI,
|
||||
) error {
|
||||
progress.statusLabel.SetText("Creating debug bundle with collected logs...")
|
||||
|
||||
request := &proto.DebugBundleRequest{
|
||||
Anonymize: params.anonymize,
|
||||
SystemInfo: params.systemInfo,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
}
|
||||
|
||||
if params.upload {
|
||||
request.UploadURL = params.uploadURL
|
||||
}
|
||||
|
||||
resp, err := conn.DebugBundle(s.ctx, request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create debug bundle: %v", err)
|
||||
}
|
||||
|
||||
// Show appropriate dialog based on upload status
|
||||
localPath := resp.GetPath()
|
||||
uploadFailureReason := resp.GetUploadFailureReason()
|
||||
uploadedKey := resp.GetUploadedKey()
|
||||
|
||||
if params.upload {
|
||||
if uploadFailureReason != "" {
|
||||
showUploadFailedDialog(progress.window, localPath, uploadFailureReason)
|
||||
} else {
|
||||
showUploadSuccessDialog(s.app, progress.window, localPath, uploadedKey)
|
||||
}
|
||||
} else {
|
||||
showBundleCreatedDialog(progress.window, localPath)
|
||||
}
|
||||
|
||||
enableUIControls(progress.uiControls)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restore service to original state
|
||||
func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, state *debugInitialState) {
|
||||
if state.needsRestoreUp {
|
||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||
log.Warnf("failed to restore up state: %v", err)
|
||||
} else {
|
||||
log.Info("Service state restored to up")
|
||||
}
|
||||
}
|
||||
|
||||
if state.wasDown {
|
||||
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
||||
log.Warnf("failed to restore down state: %v", err)
|
||||
} else {
|
||||
log.Info("Service state restored to down")
|
||||
}
|
||||
}
|
||||
|
||||
if !state.isLevelTrace {
|
||||
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: state.logLevel}); err != nil {
|
||||
log.Warnf("failed to restore log level: %v", err)
|
||||
} else {
|
||||
log.Info("Log level restored to original setting")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors during debug collection
|
||||
func handleError(progress *progressUI, errMsg string) {
|
||||
log.Errorf("%s", errMsg)
|
||||
progress.statusLabel.SetText(errMsg)
|
||||
progress.progressBar.Hide()
|
||||
enableUIControls(progress.uiControls)
|
||||
}
|
||||
|
||||
func (s *serviceClient) handleDebugCreation(
|
||||
params *debugCollectionParams,
|
||||
statusLabel *widget.Label,
|
||||
uiControls []fyne.Disableable,
|
||||
w fyne.Window,
|
||||
) {
|
||||
conn, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get client for debug: %v", err)
|
||||
statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
||||
enableUIControls(uiControls)
|
||||
return
|
||||
}
|
||||
|
||||
if params.capture {
|
||||
if _, err := conn.StartBundleCapture(s.ctx, &proto.StartBundleCaptureRequest{
|
||||
Timeout: durationpb.New(30 * time.Second),
|
||||
}); err != nil {
|
||||
log.Warnf("failed to start bundle capture: %v", err)
|
||||
} else {
|
||||
defer func() {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if _, err := conn.StopBundleCapture(stopCtx, &proto.StopBundleCaptureRequest{}); err != nil {
|
||||
log.Warnf("failed to stop bundle capture: %v", err)
|
||||
}
|
||||
}()
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := s.createDebugBundle(params.anonymize, params.systemInfo, params.uploadURL)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create debug bundle: %v", err)
|
||||
statusLabel.SetText(fmt.Sprintf("Error creating bundle: %v", err))
|
||||
enableUIControls(uiControls)
|
||||
return
|
||||
}
|
||||
|
||||
localPath := resp.GetPath()
|
||||
uploadFailureReason := resp.GetUploadFailureReason()
|
||||
uploadedKey := resp.GetUploadedKey()
|
||||
|
||||
if params.upload {
|
||||
if uploadFailureReason != "" {
|
||||
showUploadFailedDialog(w, localPath, uploadFailureReason)
|
||||
} else {
|
||||
showUploadSuccessDialog(s.app, w, localPath, uploadedKey)
|
||||
}
|
||||
} else {
|
||||
showBundleCreatedDialog(w, localPath)
|
||||
}
|
||||
|
||||
enableUIControls(uiControls)
|
||||
statusLabel.SetText("Bundle created successfully")
|
||||
}
|
||||
|
||||
func (s *serviceClient) createDebugBundle(anonymize bool, systemInfo bool, uploadURL string) (*proto.DebugBundleResponse, error) {
|
||||
conn, err := s.getSrvClient(failFastTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get client: %v", err)
|
||||
}
|
||||
|
||||
request := &proto.DebugBundleRequest{
|
||||
Anonymize: anonymize,
|
||||
SystemInfo: systemInfo,
|
||||
CliVersion: version.NetbirdVersion(),
|
||||
}
|
||||
|
||||
if uploadURL != "" {
|
||||
request.UploadURL = uploadURL
|
||||
}
|
||||
|
||||
resp, err := conn.DebugBundle(s.ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create debug bundle via daemon: %v", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// formatDuration formats a duration in HH:MM:SS format
|
||||
func formatDuration(d time.Duration) string {
|
||||
d = d.Round(time.Second)
|
||||
h := d / time.Hour
|
||||
d %= time.Hour
|
||||
m := d / time.Minute
|
||||
d %= time.Minute
|
||||
s := d / time.Second
|
||||
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||
}
|
||||
|
||||
// createButtonWithAction creates a button with the given label and action
|
||||
func createButtonWithAction(label string, action func()) *widget.Button {
|
||||
button := widget.NewButton(label, action)
|
||||
return button
|
||||
}
|
||||
|
||||
// showUploadFailedDialog displays a dialog when upload fails
|
||||
func showUploadFailedDialog(w fyne.Window, localPath, failureReason string) {
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel(fmt.Sprintf("Bundle upload failed:\n%s\n\n"+
|
||||
"A local copy was saved at:\n%s", failureReason, localPath)),
|
||||
)
|
||||
|
||||
customDialog := dialog.NewCustom("Upload Failed", "Cancel", content, w)
|
||||
|
||||
buttonBox := container.NewHBox(
|
||||
createButtonWithAction("Open file", func() {
|
||||
log.Infof("Attempting to open local file: %s", localPath)
|
||||
if openErr := open.Start(localPath); openErr != nil {
|
||||
log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
|
||||
dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
|
||||
}
|
||||
}),
|
||||
createButtonWithAction("Open folder", func() {
|
||||
folderPath := filepath.Dir(localPath)
|
||||
log.Infof("Attempting to open local folder: %s", folderPath)
|
||||
if openErr := open.Start(folderPath); openErr != nil {
|
||||
log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
|
||||
dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
content.Add(buttonBox)
|
||||
customDialog.Show()
|
||||
}
|
||||
|
||||
// showUploadSuccessDialog displays a dialog when upload succeeds
|
||||
func showUploadSuccessDialog(a fyne.App, w fyne.Window, localPath, uploadedKey string) {
|
||||
log.Infof("Upload key: %s", uploadedKey)
|
||||
keyEntry := widget.NewEntry()
|
||||
keyEntry.SetText(uploadedKey)
|
||||
keyEntry.Disable()
|
||||
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel("Bundle uploaded successfully!"),
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel("Upload key:"),
|
||||
keyEntry,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(fmt.Sprintf("Local copy saved at:\n%s", localPath)),
|
||||
)
|
||||
|
||||
customDialog := dialog.NewCustom("Upload Successful", "OK", content, w)
|
||||
|
||||
copyBtn := createButtonWithAction("Copy key", func() {
|
||||
a.Clipboard().SetContent(uploadedKey)
|
||||
log.Info("Upload key copied to clipboard")
|
||||
})
|
||||
|
||||
buttonBox := createButtonBox(localPath, w, copyBtn)
|
||||
content.Add(buttonBox)
|
||||
customDialog.Show()
|
||||
}
|
||||
|
||||
// showBundleCreatedDialog displays a dialog when bundle is created without upload
|
||||
func showBundleCreatedDialog(w fyne.Window, localPath string) {
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel(fmt.Sprintf("Bundle created locally at:\n%s\n\n"+
|
||||
"Administrator privileges may be required to access the file.", localPath)),
|
||||
)
|
||||
|
||||
customDialog := dialog.NewCustom("Debug Bundle Created", "Cancel", content, w)
|
||||
|
||||
buttonBox := createButtonBox(localPath, w, nil)
|
||||
content.Add(buttonBox)
|
||||
customDialog.Show()
|
||||
}
|
||||
|
||||
func createButtonBox(localPath string, w fyne.Window, elems ...fyne.Widget) *fyne.Container {
|
||||
box := container.NewHBox()
|
||||
for _, elem := range elems {
|
||||
box.Add(elem)
|
||||
}
|
||||
|
||||
fileBtn := createButtonWithAction("Open file", func() {
|
||||
log.Infof("Attempting to open local file: %s", localPath)
|
||||
if openErr := open.Start(localPath); openErr != nil {
|
||||
log.Errorf("Failed to open local file '%s': %v", localPath, openErr)
|
||||
dialog.ShowError(fmt.Errorf("open the local file:\n%s\n\nError: %v", localPath, openErr), w)
|
||||
}
|
||||
})
|
||||
|
||||
folderBtn := createButtonWithAction("Open folder", func() {
|
||||
folderPath := filepath.Dir(localPath)
|
||||
log.Infof("Attempting to open local folder: %s", folderPath)
|
||||
if openErr := open.Start(folderPath); openErr != nil {
|
||||
log.Errorf("Failed to open local folder '%s': %v", folderPath, openErr)
|
||||
dialog.ShowError(fmt.Errorf("open the local folder:\n%s\n\nError: %v", folderPath, openErr), w)
|
||||
}
|
||||
})
|
||||
|
||||
box.Add(fileBtn)
|
||||
box.Add(folderBtn)
|
||||
|
||||
return box
|
||||
}
|
||||
Reference in New Issue
Block a user