feat(dev): toggle dashboard screens

This commit is contained in:
Thomas Camlong committed 2026-08-01 23:49:23 +02:00
1 parent 3b450eb04a
commit df587ec76a
4 files changed
+163 -8

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
package tui
import (
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2"
)
type appModel struct {
dev prsModel
dashboard dashModel
showDashboard bool
width int
height int
}
func newAppModel(showDashboard bool) appModel {
return appModel{dev: newPRsModel(), dashboard: newDashModel(), showDashboard: showDashboard}
}
func (m appModel) Init() tea.Cmd {
if m.showDashboard {
return m.dashboard.Init()
}
return m.dev.Init()
}
func (m appModel) canSwitch() bool {
if m.showDashboard {
return !m.dashboard.filtering
}
return !m.dev.filtering && !m.dev.pulling && !m.dev.rebuilding
}
func (m appModel) switchScreen() (tea.Model, tea.Cmd) {
m.showDashboard = !m.showDashboard
commands := make([]tea.Cmd, 0, 2)
if m.width > 0 && m.height > 0 {
resize := tea.WindowSizeMsg{Width: m.width, Height: m.height}
if m.showDashboard {
updated, command := m.dashboard.Update(resize)
m.dashboard = updated.(dashModel)
commands = append(commands, command)
} else {
updated, command := m.dev.Update(resize)
m.dev = updated.(prsModel)
commands = append(commands, command)
}
}
if m.showDashboard {
commands = append(commands, m.dashboard.Init())
} else {
commands = append(commands, m.dev.Init())
}
return m, tea.Batch(commands...)
}
func (m appModel) updateDev(msg tea.Msg) (appModel, tea.Cmd) {
updated, command := m.dev.Update(msg)
m.dev = updated.(prsModel)
return m, command
}
func (m appModel) updateDashboard(msg tea.Msg) (appModel, tea.Cmd) {
updated, command := m.dashboard.Update(msg)
m.dashboard = updated.(dashModel)
return m, command
}
func (m appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if window, ok := msg.(tea.WindowSizeMsg); ok {
m.width = window.Width
m.height = window.Height
m, devCommand := m.updateDev(msg)
m, dashboardCommand := m.updateDashboard(msg)
return m, tea.Batch(devCommand, dashboardCommand)
}
if key, ok := msg.(tea.KeyPressMsg); ok {
if key.String() == "d" && m.canSwitch() {
return m.switchScreen()
}
if m.showDashboard {
return m.updateDashboard(msg)
}
return m.updateDev(msg)
}
switch msg.(type) {
case prsLoadedMsg, imageCheckedMsg, prActionMsg, rebuildDoneMsg, runningRefreshedMsg, pullReadyMsg, pullEvent, spinner.TickMsg:
return m.updateDev(msg)
case tickMsg, actionMsg, rowsMsg:
if m.showDashboard {
return m.updateDashboard(msg)
}
return m, nil
case prLogTickMsg:
if !m.showDashboard {
return m.updateDev(msg)
}
return m, nil
case logsMsg:
m, devCommand := m.updateDev(msg)
m, dashboardCommand := m.updateDashboard(msg)
return m, tea.Batch(devCommand, dashboardCommand)
}
if m.showDashboard {
return m.updateDashboard(msg)
}
return m.updateDev(msg)
}
func (m appModel) View() tea.View {
if m.showDashboard {
return m.dashboard.View()
}
return m.dev.View()
}
func runApp(showDashboard bool) error {
program := tea.NewProgram(newAppModel(showDashboard))
_, err := program.Run()
return err
}
+37
View File
@@ -0,0 +1,37 @@
package tui
import (
"testing"
tea "charm.land/bubbletea/v2"
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh"
)
func TestAppTogglePreservesDevelopmentState(t *testing.T) {
m := newAppModel(false)
m.dev.rows = []prRow{{kind: "remote", pr: gh.PR{Number: 6502}}}
updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"}))
dashboard := updated.(appModel)
if !dashboard.showDashboard {
t.Fatal("d did not open the instance dashboard")
}
updated, _ = dashboard.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"}))
development := updated.(appModel)
if development.showDashboard || len(development.dev.rows) != 1 || development.dev.rows[0].pr.Number != 6502 {
t.Fatalf("development state was not preserved: %#v", development.dev.rows)
}
}
func TestAppToggleDoesNotInterceptFilterInput(t *testing.T) {
m := newAppModel(false)
m.dev.filtering = true
m.dev.filter.Focus()
updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"}))
got := updated.(appModel)
if got.showDashboard || got.dev.filter.Value() != "d" {
t.Fatalf("filter input was intercepted: dashboard=%v filter=%q", got.showDashboard, got.dev.filter.Value())
}
}
+2 -4
View File
@@ -402,7 +402,7 @@ func (m dashModel) View() tea.View {
}
b.WriteString("\n" + statusStyle.Render(m.status))
}
b.WriteString("\n\n" + helpStyle.Render("/ filter · ↑/↓ select · l logs · f follow · pgup/pgdn scroll · s stop · R restart · x remove · o open · c copy URL · r refresh · q quit"))
b.WriteString("\n\n" + helpStyle.Render("/ filter · ↑/↓ select · l logs · f follow · pgup/pgdn scroll · s stop · R restart · x remove · o open · c copy URL · r refresh · d development · q quit"))
b.WriteString("\n" + helpStyle.Render(runningLegend+" · "+stoppedLegend))
v := tea.NewView(b.String())
v.AltScreen = true
@@ -411,7 +411,5 @@ func (m dashModel) View() tea.View {
}
func RunDashboard() error {
p := tea.NewProgram(newDashModel())
_, err := p.Run()
return err
return runApp(true)
}
+2 -4
View File
@@ -1049,7 +1049,7 @@ func (m prsModel) View() tea.View {
b.WriteString("\n\n" + helpStyle.Render("q/esc cancel rebuild"))
} else if !m.pulling {
b.WriteString("\n\n" + helpStyle.Render("/ filter · ↑/↓ select · enter start/stop · R build locally · p pull remote · m demo · o PR · a app"))
b.WriteString("\n" + helpStyle.Render("b "+botState+" · r refresh · q quit"))
b.WriteString("\n" + helpStyle.Render("b "+botState+" · r refresh · d instances · q quit"))
b.WriteString("\n" + helpStyle.Render("l toggle logs · f follow · pgup/pgdn scroll"))
}
b.WriteString("\n" + helpStyle.Render("SOURCE: local Docker / remote GHCR · CI: "+legendCIYes+" pass "+legendCINo+" fail "+legendCIPend+" pending "+legendNone+" none · IMAGE: "+legendImageYes+" available "+legendImageNo+" not built"))
@@ -1060,7 +1060,5 @@ func (m prsModel) View() tea.View {
}
func RunDev() error {
p := tea.NewProgram(newPRsModel())
_, err := p.Run()
return err
return runApp(false)
}