Skip to content

Commit d604dbd

Browse files
committed
✨ feat: enhance profile application and VNC connection handling
- Added legacy compatibility comment for profile settings application in Config. - Introduced DefaultProfile assignment to ensure getters resolve to the current profile. - Improved VNC connection handling by centralizing UI feedback for connection outcomes. - Added success dialog for VNC connections with URL information for better user guidance. - Enhanced logging for profile switching and VNC connection processes to aid in debugging.
1 parent a2593b3 commit d604dbd

4 files changed

Lines changed: 124 additions & 58 deletions

File tree

internal/config/profiles.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (c *Config) ApplyProfile(profileName string) error {
3333
return fmt.Errorf("profile '%s' not found", profileName)
3434
}
3535

36-
// Apply profile settings to main config
36+
// Apply profile settings to main config (legacy compatibility)
3737
c.Addr = profile.Addr
3838
c.User = profile.User
3939
c.Password = profile.Password
@@ -44,6 +44,9 @@ func (c *Config) ApplyProfile(profileName string) error {
4444
c.Insecure = profile.Insecure
4545
c.SSHUser = profile.SSHUser
4646

47+
// Also set DefaultProfile so getters (GetAddr, GetUser, etc.) resolve to this profile at runtime
48+
c.DefaultProfile = profileName
49+
4750
return nil
4851
}
4952

internal/ui/components/connection_profiles_operations.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,49 @@ func (a *App) applyConnectionProfile(profileName string) {
1818

1919
// Run profile switching in goroutine to avoid blocking UI
2020
go func() {
21+
uiLogger := models.GetUILogger()
22+
uiLogger.Debug("Starting profile switch to: %s", profileName)
23+
2124
err := a.config.ApplyProfile(profileName)
2225
if err != nil {
26+
uiLogger.Error("Failed to apply profile %s: %v", profileName, err)
2327
a.QueueUpdateDraw(func() {
2428
a.header.ShowError("Failed to apply profile: " + err.Error())
2529
})
2630
return
2731
}
2832

33+
uiLogger.Debug("Profile %s applied successfully to config", profileName)
34+
2935
// Note: We don't save the config file when switching profiles in the UI
3036
// The default_profile should only be changed via the config wizard
3137
// This allows temporary profile switching without affecting the saved config
3238

3339
// Recreate the API client with the new profile
40+
uiLogger.Debug("Creating new API client with updated config")
3441
client, err := api.NewClient(&a.config, api.WithLogger(models.GetUILogger()))
3542
if err != nil {
43+
uiLogger.Error("Failed to create API client for profile %s: %v", profileName, err)
3644
a.QueueUpdateDraw(func() {
3745
a.header.ShowError("Failed to create API client: " + err.Error())
3846
})
3947
return
4048
}
4149

50+
uiLogger.Debug("New API client created successfully for profile %s", profileName)
51+
4252
a.QueueUpdateDraw(func() {
53+
uiLogger.Debug("Updating app client and VNC service")
4354
a.client = client
4455

4556
// Update VNC service with new connection details
4657
if a.vncService != nil {
58+
uiLogger.Debug("Updating VNC service client")
4759
a.vncService.UpdateClient(client)
4860
}
4961

5062
// Update the header to show the new active profile
63+
uiLogger.Debug("Updating header with new active profile: %s", profileName)
5164
a.header.ShowActiveProfile(profileName)
5265
})
5366

@@ -57,6 +70,7 @@ func (a *App) applyConnectionProfile(profileName string) {
5770
})
5871

5972
// Then refresh data with new connection (this will update the UI)
73+
uiLogger.Debug("Starting manual refresh with new client")
6074
a.manualRefresh()
6175
}()
6276
}

internal/ui/components/dialogs_creators.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func CreateErrorDialog(title, message string, onClose func()) *tview.Modal {
103103
return createBaseModal(title, message, theme.Colors.Error, onClose)
104104
}
105105

106-
// CreateErrorDialogWithScrollableText creates an error dialog with scrollable text for long content.
106+
// CreateErrorDialogWithScrollableText creates an error dialog with scrollable text for long URLs.
107107
func CreateErrorDialogWithScrollableText(title, message string, onClose func()) *tview.Modal {
108108
// Create a modal with the message
109109
modal := tview.NewModal()
@@ -135,6 +135,38 @@ func CreateErrorDialogWithScrollableText(title, message string, onClose func())
135135
return modal
136136
}
137137

138+
// CreateSuccessDialogWithURL creates a success dialog with URL information for VNC connections.
139+
func CreateSuccessDialogWithURL(title, message string, onClose func()) *tview.Modal {
140+
// Create a modal with the message
141+
modal := tview.NewModal()
142+
modal.SetText(message)
143+
modal.SetTextColor(theme.Colors.Success)
144+
modal.SetBorderColor(theme.Colors.Border)
145+
modal.SetTitle(title)
146+
modal.SetTitleColor(theme.Colors.Title)
147+
148+
// Add close button
149+
modal.AddButtons([]string{"OK"})
150+
modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
151+
if onClose != nil {
152+
onClose()
153+
}
154+
})
155+
156+
// Add keyboard shortcuts for dismissal
157+
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
158+
if event.Key() == tcell.KeyEscape {
159+
if onClose != nil {
160+
onClose()
161+
}
162+
return nil
163+
}
164+
return event
165+
})
166+
167+
return modal
168+
}
169+
138170
// CreateFormDialog creates a form dialog with custom fields.
139171
func CreateFormDialog(title string, fields []FormField, onSubmit, onCancel func(map[string]string)) *tview.Form {
140172
form := tview.NewForm()

internal/ui/components/shell.go

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -46,44 +46,80 @@ func (a *App) openNodeShell() {
4646
a.Sync()
4747
}
4848

49+
// handleVNCOutcome centralizes UI handling for VNC connection results to avoid duplicated code.
50+
func (a *App) handleVNCOutcome(kind string, name string, vncURL string, err error) {
51+
if err != nil {
52+
// Specific handling for missing xdg-open
53+
if strings.Contains(err.Error(), "xdg-open not found") {
54+
message := fmt.Sprintf("Cannot open browser automatically on this system.\n\nxdg-open is not installed or not available.\n\nThe VNC server is still running and ready for connection.\n\nTo connect:\n1. Copy the shortened URL below\n2. Paste it into your browser\n3. It will automatically redirect to the full VNC session\n4. Connect quickly before the session expires\n\nShortened URL:\n%s", vncURL)
55+
56+
modal := CreateErrorDialogWithScrollableText("Browser Not Available", message, func() {
57+
a.pages.RemovePage("vnc_error")
58+
})
59+
a.pages.AddPage("vnc_error", modal, false, true)
60+
a.SetFocus(modal)
61+
62+
return
63+
}
64+
65+
// Generic error dialog
66+
context := "VNC connection"
67+
if kind == "node" {
68+
context = fmt.Sprintf("VNC shell for %s", name)
69+
} else if kind == "vm" {
70+
context = fmt.Sprintf("VNC console for %s", name)
71+
}
72+
73+
errorModal := CreateErrorDialog("VNC Connection Error",
74+
fmt.Sprintf("Failed to start %s:\n\n%s", context, err.Error()),
75+
func() {
76+
a.pages.RemovePage("vnc_error")
77+
})
78+
a.pages.AddPage("vnc_error", errorModal, false, true)
79+
80+
return
81+
}
82+
83+
// Success path: show fallback URL and header success
84+
var title, startedHeader, headerMsg string
85+
if kind == "node" {
86+
title = "VNC Shell Started"
87+
startedHeader = fmt.Sprintf("VNC shell started successfully for %s!", name)
88+
headerMsg = fmt.Sprintf("Embedded VNC shell started for %s", name)
89+
} else {
90+
title = "VNC Console Started"
91+
startedHeader = fmt.Sprintf("VNC console started successfully for %s!", name)
92+
headerMsg = fmt.Sprintf("Embedded VNC console started for %s", name)
93+
}
94+
95+
message := fmt.Sprintf("%s\n\nOpening in your default browser...\n\nIf the browser doesn't open automatically, you can use this URL as a fallback:\n\n%s", startedHeader, vncURL)
96+
modal := CreateSuccessDialogWithURL(title, message, func() {
97+
a.pages.RemovePage("vnc_success")
98+
})
99+
a.pages.AddPage("vnc_success", modal, false, true)
100+
a.SetFocus(modal)
101+
a.header.ShowSuccess(headerMsg)
102+
}
103+
49104
// connectToNodeVNC performs the actual node VNC connection using embedded noVNC client.
50105
func (a *App) connectToNodeVNC(node *api.Node, vncService *vnc.Service) {
51106
// Show loading message
52107
a.header.ShowLoading(fmt.Sprintf("Starting embedded VNC shell for %s...", node.Name))
53108

54109
// Open embedded VNC connection in a goroutine to avoid blocking UI
55110
go func() {
111+
uiLogger := models.GetUILogger()
112+
uiLogger.Debug("Starting VNC connection for node %s with client addr: %s", node.Name, a.config.GetAddr())
113+
56114
vncURL, err := vncService.ConnectToNodeEmbedded(node.Name)
57115

58116
a.QueueUpdateDraw(func() {
59-
if err != nil {
60-
// Clear the loading message from header
61-
a.header.StopLoading()
62-
a.updateHeaderWithActiveProfile() // Restore header with active profile
63-
64-
// Check if this is an xdg-open not found error
65-
if strings.Contains(err.Error(), "xdg-open not found") {
66-
// Show helpful dialog with shortened VNC URL
67-
message := fmt.Sprintf("Cannot open browser automatically on this system.\n\nxdg-open is not installed or not available.\n\nThe VNC server is still running and ready for connection.\n\nTo connect to the VNC shell:\n1. Copy the shortened URL below\n2. Paste it into your browser\n3. It will automatically redirect to the full VNC session\n4. Connect quickly before the session expires\n\nShortened URL:\n%s", vncURL)
68-
69-
// Create a custom dialog with scrollable text area for long URLs
70-
modal := CreateErrorDialogWithScrollableText("Browser Not Available", message, func() {
71-
a.pages.RemovePage("vnc_error")
72-
})
73-
a.pages.AddPage("vnc_error", modal, false, true)
74-
a.SetFocus(modal)
75-
} else {
76-
// Show generic error dialog
77-
errorModal := CreateErrorDialog("VNC Connection Error",
78-
fmt.Sprintf("Failed to start VNC shell for %s:\n\n%s", node.Name, err.Error()),
79-
func() {
80-
a.pages.RemovePage("vnc_error")
81-
})
82-
a.pages.AddPage("vnc_error", errorModal, false, true)
83-
}
84-
} else {
85-
a.header.ShowSuccess(fmt.Sprintf("Embedded VNC shell started for %s", node.Name))
86-
}
117+
// Clear the loading message from header
118+
a.header.StopLoading()
119+
a.updateHeaderWithActiveProfile() // Restore header with active profile
120+
121+
// Unified outcome handling
122+
a.handleVNCOutcome("node", node.Name, vncURL, err)
87123
})
88124
}()
89125
}
@@ -95,37 +131,18 @@ func (a *App) connectToVMVNC(vm *api.VM, vncService *vnc.Service) {
95131

96132
// Open embedded VNC connection in a goroutine to avoid blocking UI
97133
go func() {
134+
uiLogger := models.GetUILogger()
135+
uiLogger.Debug("Starting VNC connection for VM %s with client addr: %s", vm.Name, a.config.GetAddr())
136+
98137
vncURL, err := vncService.ConnectToVMEmbedded(vm)
99138

100139
a.QueueUpdateDraw(func() {
101-
if err != nil {
102-
// Clear the loading message from header
103-
a.header.StopLoading()
104-
a.updateHeaderWithActiveProfile() // Restore header with active profile
105-
106-
// Check if this is an xdg-open not found error
107-
if strings.Contains(err.Error(), "xdg-open not found") {
108-
// Show helpful dialog with shortened VNC URL
109-
message := fmt.Sprintf("Cannot open browser automatically on this system.\n\nxdg-open is not installed or not available.\n\nThe VNC server is still running and ready for connection.\n\nTo connect to the VNC console:\n1. Copy the shortened URL below\n2. Paste it into your browser\n3. It will automatically redirect to the full VNC session\n4. Connect quickly before the session expires\n\nShortened URL:\n%s", vncURL)
110-
111-
// Create a custom dialog with scrollable text area for long URLs
112-
modal := CreateErrorDialogWithScrollableText("Browser Not Available", message, func() {
113-
a.pages.RemovePage("vnc_error")
114-
})
115-
a.pages.AddPage("vnc_error", modal, false, true)
116-
a.SetFocus(modal)
117-
} else {
118-
// Show generic error dialog
119-
errorModal := CreateErrorDialog("VNC Connection Error",
120-
fmt.Sprintf("Failed to start VNC console for %s:\n\n%s", vm.Name, err.Error()),
121-
func() {
122-
a.pages.RemovePage("vnc_error")
123-
})
124-
a.pages.AddPage("vnc_error", errorModal, false, true)
125-
}
126-
} else {
127-
a.header.ShowSuccess(fmt.Sprintf("Embedded VNC console started for %s", vm.Name))
128-
}
140+
// Clear the loading message from header
141+
a.header.StopLoading()
142+
a.updateHeaderWithActiveProfile() // Restore header with active profile
143+
144+
// Unified outcome handling
145+
a.handleVNCOutcome("vm", vm.Name, vncURL, err)
129146
})
130147
}()
131148
}

0 commit comments

Comments
 (0)