From b39f00fd826075c476c3a00d3b6b105f8704c4e9 Mon Sep 17 00:00:00 2001 From: Said Boudjelda Date: Sun, 18 Oct 2026 15:40:10 +0200 Subject: [PATCH] fix(tui): the Brokers tab keeps its cursor on a broker when a refresh returns fewer The Brokers tab refreshes every 10 s and stored the new broker list without looking at the cursor. With the cursor on the last broker and that broker gone (it died, or was scaled away), View indexed past the end and the dashboard died with "index out of range [2] with length 2", exactly when it was wanted. A refresh now clamps the cursor to the last broker, or 0 with none. View already handles a nil or empty list. TestBrokersRefreshWithFewerBrokers shrinks 3 brokers to 2 with the cursor on the third, then to none. --- cli/tui/brokers.go | 5 +++++ cli/tui/brokers_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 cli/tui/brokers_test.go diff --git a/cli/tui/brokers.go b/cli/tui/brokers.go index 6c28ffa0..1743c3a4 100644 --- a/cli/tui/brokers.go +++ b/cli/tui/brokers.go @@ -46,6 +46,11 @@ func (m brokersModel) Update(msg tea.Msg) (brokersModel, tea.Cmd) { m.loading = false m.info = msg.info m.err = msg.err + // A broker gone since the last refresh left the cursor past the end, + // and View panicked indexing it. + if m.info != nil && m.cursor >= len(m.info.Brokers) { + m.cursor = max(0, len(m.info.Brokers)-1) + } case tea.KeyMsg: if m.info == nil { break diff --git a/cli/tui/brokers_test.go b/cli/tui/brokers_test.go new file mode 100644 index 00000000..a47ed0a3 --- /dev/null +++ b/cli/tui/brokers_test.go @@ -0,0 +1,36 @@ +package tui + +import ( + "strings" + "testing" + + "github.com/bmscomp/kates/cli/client" +) + +// The cursor on the last broker, and a refresh with one broker fewer: View +// indexed past the end and the dashboard died with the broker. +func TestBrokersRefreshWithFewerBrokers(t *testing.T) { + brokers := func(n int) *client.ClusterInfo { + info := &client.ClusterInfo{ClusterID: "c1", BrokerCount: n} + for i := range n { + info.Brokers = append(info.Brokers, client.BrokerNode{ID: i, Host: "kafka", Port: 9092}) + } + return info + } + m := brokersModel{width: 120, height: 40} + m, _ = m.Update(brokersLoadedMsg{info: brokers(3)}) + m.cursor = 2 + + m, _ = m.Update(brokersLoadedMsg{info: brokers(2)}) + if m.cursor != 1 { + t.Errorf("cursor = %d after the refresh, want 1, the last broker", m.cursor) + } + if view := m.View(); !strings.Contains(view, "Broker 1") { + t.Errorf("the view does not show broker 1:\n%s", view) + } + + m, _ = m.Update(brokersLoadedMsg{info: brokers(0)}) + if m.cursor != 0 || !strings.Contains(m.View(), "No brokers found") { + t.Errorf("cursor = %d with no brokers, want 0 and the empty view", m.cursor) + } +}