Skip to content

Commit b19034b

Browse files
committed
feat: Add context switcher to application dashboard
1 parent 9a4cc60 commit b19034b

8 files changed

Lines changed: 320 additions & 29 deletions

File tree

applicationDashboard/applicationDashboard.go

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package appllicationdashboard
22

33
import (
4+
"fmt"
5+
46
"github.com/FredrikMWold/radix-tui/applicationDashboard/commands"
7+
contextswitcher "github.com/FredrikMWold/radix-tui/applicationDashboard/contextSwitcher"
58
"github.com/FredrikMWold/radix-tui/styles"
69
tea "github.com/charmbracelet/bubbletea"
710
"github.com/charmbracelet/lipgloss"
@@ -34,9 +37,45 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3437
m.hasAuthRedirect = false
3538
return m, commands.GetApplications
3639

40+
case contextswitcher.ContextSelected:
41+
// Context was selected, save it and reload
42+
m.focused = m.previousFocused
43+
return m, commands.SetContext(string(msg))
44+
45+
case contextswitcher.ContextCancelled:
46+
// Context switcher was cancelled, restore previous focus
47+
m.focused = m.previousFocused
48+
return m, nil
49+
50+
case commands.ContextChanged:
51+
// Context changed successfully, reload everything
52+
m.context = string(msg)
53+
m.applications = []string{}
54+
m.application = commands.Application{}
55+
m.isLoadingApplication = false
56+
m.loadError = ""
57+
m.appsLoaded = false
58+
m.focused = application
59+
m.keys = ApplicationTableKeys
60+
// Reinitialize context switcher with new context
61+
m.contextSwitcher = contextswitcher.New(m.context)
62+
return m, tea.Batch(commands.GetApplications, m.applicationsTable.Init())
63+
3764
case tea.KeyMsg:
65+
// Handle context switch key globally (except when in context switcher)
66+
if msg.String() == "ctrl+s" && m.focused != contextSwitch {
67+
m.previousFocused = m.focused
68+
m.focused = contextSwitch
69+
m.contextSwitcher = contextswitcher.New(m.context)
70+
return m, nil
71+
}
72+
3873
switch msg.String() {
3974
case "ctrl+c", "q":
75+
if m.focused == contextSwitch {
76+
m.focused = m.previousFocused
77+
return m, nil
78+
}
4079
return m, tea.Quit
4180
case "ctrl+r":
4281
if m.application.Name != "" {
@@ -54,6 +93,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5493
m.keys = BuildDeployFormKeys
5594
}
5695
case "esc":
96+
if m.focused == contextSwitch {
97+
m.focused = m.previousFocused
98+
return m, nil
99+
}
57100
if m.focused == pipeline {
58101
m.focused = application
59102
m.keys = ApplicationTableKeys
@@ -65,9 +108,30 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
65108
}
66109

67110
case commands.Applications:
68-
// Loaded apps → clear any implicit auth-waiting state
69-
m.applications = msg
70-
m.hasAuthRedirect = false
111+
// Accept applications if:
112+
// 1. Context matches current context, OR
113+
// 2. We don't have a context yet (initial load - accept cache immediately)
114+
if msg.Context == m.context || m.context == "" {
115+
m.applications = msg.Apps
116+
m.hasAuthRedirect = false
117+
m.loadError = ""
118+
m.appsLoaded = true
119+
// If we didn't have context yet, set it from the message
120+
if m.context == "" {
121+
m.context = msg.Context
122+
}
123+
}
124+
125+
case commands.ApplicationsError:
126+
// Only show error if it's for the current context (or no context set yet)
127+
if msg.Context == m.context || m.context == "" {
128+
m.hasAuthRedirect = false
129+
m.loadError = fmt.Sprintf("Failed to load applications for context '%s':\n%s\n\nPress ctrl+s to switch context or q to quit", msg.Context, msg.Err.Error())
130+
m.appsLoaded = true
131+
if m.context == "" {
132+
m.context = msg.Context
133+
}
134+
}
71135

72136
case Context:
73137
m.context = string(msg)
@@ -96,6 +160,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
96160
}
97161

98162
var pageCmd tea.Cmd
163+
if m.focused == contextSwitch {
164+
m.contextSwitcher, pageCmd = m.contextSwitcher.Update(msg)
165+
}
99166
if m.focused == buildAndDeploy {
100167
m.buildAndDeploy, pageCmd = m.buildAndDeploy.Update(msg)
101168
}
@@ -139,7 +206,7 @@ func (m Model) View() string {
139206
)
140207

141208
}
142-
if len(m.applications) == 0 {
209+
if !m.appsLoaded {
143210
if m.hasAuthRedirect {
144211
// During redirect, show text without spinner
145212
return lipgloss.NewStyle().
@@ -149,6 +216,23 @@ func (m Model) View() string {
149216
AlignVertical(lipgloss.Center).
150217
Render("Waiting for authentication")
151218
}
219+
if m.loadError != "" {
220+
// Show error message with option to switch context
221+
errorStyle := lipgloss.NewStyle().
222+
Foreground(lipgloss.Color("196")).
223+
Bold(true)
224+
content := errorStyle.Render(m.loadError)
225+
// If context switcher is open, show it as overlay
226+
if m.focused == contextSwitch {
227+
return m.renderContextSwitcherOverlay(content)
228+
}
229+
return lipgloss.NewStyle().
230+
Height(m.height).
231+
Width(m.width).
232+
AlignHorizontal(lipgloss.Center).
233+
AlignVertical(lipgloss.Center).
234+
Render(content)
235+
}
152236
// Otherwise, show normal loading with spinner
153237
return lipgloss.NewStyle().
154238
Height(m.height).
@@ -157,7 +241,24 @@ func (m Model) View() string {
157241
AlignVertical(lipgloss.Center).
158242
Render("Loading applications " + m.spinner.View())
159243
}
160-
return lipgloss.JoinHorizontal(
244+
245+
// Handle empty apps list (loaded successfully but no apps)
246+
if len(m.applications) == 0 {
247+
content := fmt.Sprintf("No applications found in context '%s'\n\nPress ctrl+s to switch context or q to quit", m.context)
248+
// If context switcher is open, show it as overlay
249+
if m.focused == contextSwitch {
250+
return m.renderContextSwitcherOverlay(content)
251+
}
252+
return lipgloss.NewStyle().
253+
Height(m.height).
254+
Width(m.width).
255+
AlignHorizontal(lipgloss.Center).
256+
AlignVertical(lipgloss.Center).
257+
Render(content)
258+
}
259+
260+
// Build the main view
261+
mainView := lipgloss.JoinHorizontal(
161262
lipgloss.Top,
162263
lipgloss.JoinVertical(
163264
lipgloss.Top,
@@ -167,4 +268,27 @@ func (m Model) View() string {
167268
),
168269
m.getActivePageView(),
169270
) + "\n" + m.help.View(m.keys)
271+
272+
// If context switcher is active, show it as a modal overlay
273+
if m.focused == contextSwitch {
274+
return m.renderContextSwitcherOverlay(mainView)
275+
}
276+
277+
return mainView
278+
}
279+
280+
func (m Model) renderContextSwitcherOverlay(_ string) string {
281+
// Create the modal
282+
modal := m.contextSwitcher.View()
283+
284+
// Create a dimmed background effect by placing the modal on top
285+
return lipgloss.Place(
286+
m.width,
287+
m.height,
288+
lipgloss.Center,
289+
lipgloss.Center,
290+
modal,
291+
lipgloss.WithWhitespaceChars(" "),
292+
lipgloss.WithWhitespaceForeground(lipgloss.Color("0")),
293+
)
170294
}

applicationDashboard/applicationTable/applicationTable.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
2626

2727
case commands.Applications:
2828
m.isLoadingApplications = false
29-
rows := make([]table.Row, len(msg))
30-
for i, app := range msg {
29+
rows := make([]table.Row, len(msg.Apps))
30+
for i, app := range msg.Apps {
3131
rows[i] = table.Row([]string{app})
3232
}
3333
m.table.SetRows(rows)

applicationDashboard/commands/commands.go

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,17 @@ func SelectApplication(application string) tea.Cmd {
2121
}
2222
}
2323

24-
type Applications []string
24+
// Applications contains loaded apps and the context they were loaded for
25+
type Applications struct {
26+
Apps []string
27+
Context string
28+
}
29+
30+
// ApplicationsError is sent when loading applications fails
31+
type ApplicationsError struct {
32+
Err error
33+
Context string
34+
}
2535

2636
// AuthWaiting indicates we expect to perform interactive login.
2737
type AuthWaiting struct{}
@@ -78,34 +88,33 @@ func LoadCachedApplications() tea.Cmd {
7888
var apps []string
7989
if err := json.Unmarshal([]byte(raw), &apps); err == nil && len(apps) > 0 {
8090
sort.Strings(apps)
81-
return Applications(apps)
91+
return Applications{Apps: apps, Context: ctxName}
8292
}
8393
}
8494
return nil
8595
}
8696
}
8797

8898
func GetApplications() tea.Msg {
99+
// Get current context for error reporting
100+
cfg, _ := radixconfig.GetRadixConfig()
101+
var ctxName string
102+
if cfg != nil && cfg.CustomConfig != nil {
103+
ctxName = cfg.CustomConfig.Context
104+
}
105+
89106
ctx := context.Background()
90107
client, err := radix.New(false)
91108
if err != nil {
92-
fmt.Println(err)
93-
return Applications{}
109+
return ApplicationsError{Err: err, Context: ctxName}
94110
}
95111
apps, err := client.ListApplications(ctx)
96112
if err != nil {
97-
fmt.Println(err)
98-
return Applications{}
113+
return ApplicationsError{Err: err, Context: ctxName}
99114
}
100115
sort.Strings(apps)
101116
// Persist to cache for fast next startup
102117
func() {
103-
// Determine current context
104-
cfg, err := radixconfig.GetRadixConfig()
105-
var ctxName string
106-
if err == nil && cfg != nil && cfg.CustomConfig != nil {
107-
ctxName = cfg.CustomConfig.Context
108-
}
109118
cacheFile := fmt.Sprintf("%s/radix-tui-cache.json", radixconfig.RadixConfigDir)
110119
c := cache.New(cacheFile, "applications")
111120
key := fmt.Sprintf("apps_%s", ctxName)
@@ -114,7 +123,7 @@ func GetApplications() tea.Msg {
114123
c.SetItem(key, string(b), 7*24*time.Hour)
115124
}
116125
}()
117-
return Applications(apps)
126+
return Applications{Apps: apps, Context: ctxName}
118127
}
119128

120129
type Application struct {
@@ -200,3 +209,24 @@ func stringDeref(p *string) string {
200209
}
201210
return *p
202211
}
212+
213+
// ContextChanged is sent when the context is successfully changed
214+
type ContextChanged string
215+
216+
// SetContext changes the radix context and saves it to config
217+
func SetContext(ctx string) tea.Cmd {
218+
return func() tea.Msg {
219+
if !radixconfig.IsValidContext(ctx) {
220+
return nil
221+
}
222+
radixConfig, err := radixconfig.GetRadixConfig()
223+
if err != nil {
224+
return nil
225+
}
226+
radixConfig.CustomConfig.Context = ctx
227+
if err := radixconfig.Save(radixConfig); err != nil {
228+
return nil
229+
}
230+
return ContextChanged(ctx)
231+
}
232+
}

applicationDashboard/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ var ApplicationTableKeys = keyMap{
1515
key.WithKeys("down"),
1616
key.WithHelp("down", "move down"),
1717
),
18+
SwitchContext: key.NewBinding(
19+
key.WithKeys("ctrl+s"),
20+
key.WithHelp("ctrl+s", "switch context"),
21+
),
1822
}
1923

2024
var PipelineTableKeys = keyMap{
@@ -46,6 +50,10 @@ var PipelineTableKeys = keyMap{
4650
key.WithKeys("esc"),
4751
key.WithHelp("esc", "back"),
4852
),
53+
SwitchContext: key.NewBinding(
54+
key.WithKeys("ctrl+s"),
55+
key.WithHelp("ctrl+s", "switch context"),
56+
),
4957
}
5058

5159
var BuildDeployFormKeys = keyMap{

0 commit comments

Comments
 (0)