Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/cmd/branch/connections/top_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,8 +654,8 @@ func TestTopCmdRunsNekiTop(t *testing.T) {
c.Assert(view, qt.Contains, "router-a")
c.Assert(view, qt.Contains, "incomplete results: router router-b unreachable")
c.Assert(view, qt.Not(qt.Contains), "BLOCK")
c.Assert(view, qt.Contains, "cancel query")
c.Assert(view, qt.Contains, "force terminate")
c.Assert(view, qt.Not(qt.Contains), "cancel query")
c.Assert(view, qt.Not(qt.Contains), "force terminate")
c.Assert(blockersView, qt.Not(qt.Contains), "[blockers]")
c.Assert(cancelConfirming, qt.IsFalse)
}
Expand Down
17 changes: 17 additions & 0 deletions internal/connections/tui/connection_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ func (s ConnectionCapabilities) supports(kind actionKind) bool {
return requirement.field != actionTargetUnset
}

// forConnection drops the actions whose required ID conn lacks, so controls
// are only offered for actions the backend can run against that connection.
func (s ConnectionCapabilities) forConnection(conn live.Connection) ConnectionCapabilities {
s = s.effective()
target := actionTargetFor(conn)
if s.missingActionID(actionCancelQuery, target) != "" {
s.CancelQuery = ActionRequirement{}
}
if s.missingActionID(actionTerminateTxn, target) != "" {
s.TerminateTransaction = ActionRequirement{}
}
if s.missingActionID(actionTerminateConn, target) != "" {
s.TerminateConnection = ActionRequirement{}
}
return s
}

func (s ConnectionCapabilities) requirement(kind actionKind) ActionRequirement {
s = s.effective()
switch kind {
Expand Down
26 changes: 22 additions & 4 deletions internal/connections/tui/detail.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,23 @@ func renderConnectionStatus(label string, conn live.Connection, width int) strin
return clipLine(status, width)
}

// detailActionTarget mirrors Model.actionTargetConnection: on the Blockers tab
// the keys act on the highlighted blocker, otherwise on the subject.
func detailActionTarget(state detailState) (live.Connection, bool) {
if !state.SubjectFound {
return live.Connection{}, false
}
if state.Tab != tabBlockers {
return state.Subject, true
}
rows := detailBlockerRows(state.List, state.Subject)
if state.BlockerSelection < 0 || state.BlockerSelection >= len(rows) {
return state.Subject, true
}
row := rows[state.BlockerSelection]
return row.Connection, row.Present
}

func renderDetailHelp(state detailState) string {
labels := actionHelpLabels(state.DisplayPreset)
cancelHelp := labels.cancel
Expand Down Expand Up @@ -371,14 +388,15 @@ func renderDetailHelp(state detailState) string {
bindings = append(bindings, key.NewBinding(key.WithKeys("q", "esc"), key.WithHelp("q/esc", "back")))
// An ended connection has nothing to act on, so suppress the destructive
// action hints rather than advertise keys that no-op.
if state.SubjectFound && !state.ReadOnlyActions && !state.Replay {
if support.supports(actionCancelQuery) {
if target, ok := detailActionTarget(state); ok && !state.ReadOnlyActions && !state.Replay {
actions := support.forConnection(target)
if actions.supports(actionCancelQuery) {
bindings = append(bindings, key.NewBinding(key.WithKeys("c"), key.WithHelp("c", cancelHelp)))
}
if support.supports(actionTerminateTxn) {
if actions.supports(actionTerminateTxn) {
bindings = append(bindings, key.NewBinding(key.WithKeys("k"), key.WithHelp("k", killTxnHelp)))
}
if support.supports(actionTerminateConn) {
if actions.supports(actionTerminateConn) {
bindings = append(bindings, key.NewBinding(key.WithKeys("K"), key.WithHelp("shift+K", terminateHelp)))
}
}
Expand Down
8 changes: 7 additions & 1 deletion internal/connections/tui/detail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func TestDetailRendersConnectionEndedWhenSubjectMissing(t *testing.T) {
c.Assert(strings.Contains(endedFooter, "terminate"), qt.IsFalse)
c.Assert(strings.Contains(endedFooter, "q/esc back"), qt.IsTrue)

liveFooter := stripANSI(renderDetailHelp(detailState{SubjectFound: true, Width: 200}))
liveFooter := stripANSI(renderDetailHelp(detailState{SubjectFound: true, Subject: connectionWithActionIDs(), Width: 200}))
c.Assert(strings.Contains(liveFooter, "cancel query"), qt.IsTrue)
c.Assert(strings.Contains(liveFooter, "kill transaction"), qt.IsTrue)
}
Expand Down Expand Up @@ -498,19 +498,25 @@ func TestDetailFooterShowsShiftKForForceTerminate(t *testing.T) {

footer := stripANSI(renderDetailHelp(detailState{
SubjectFound: true,
Subject: connectionWithActionIDs(),
Width: 160,
}))

c.Assert(footer, qt.Contains, "shift+K force terminate")
}

func connectionWithActionIDs() live.Connection {
return live.Connection{QueryID: new("query-1"), TransactionID: new("transaction-1"), ConnectionID: new("connection-1")}
}

// The Blockers-tab contextual footer includes both navigation and destructive
// action hints, so it must stay within the default 200-column recording width.
func TestDetailBlockersFooterFitsOneLineAtDefaultRecordingWidth(t *testing.T) {
c := qt.New(t)

footer := stripANSI(renderDetailHelp(detailState{
SubjectFound: true,
Subject: connectionWithActionIDs(),
Tab: tabBlockers,
Width: 200,
}))
Expand Down
8 changes: 4 additions & 4 deletions internal/connections/tui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1946,9 +1946,9 @@ func TestModelNekiConnectionView(t *testing.T) {
c.Assert(tableView, qt.Contains, "ROUTER")
c.Assert(tableView, qt.Contains, "router-a")
c.Assert(tableView, qt.Not(qt.Contains), "BLOCK")
c.Assert(tableView, qt.Contains, "cancel query")
c.Assert(tableView, qt.Contains, "kill transaction")
c.Assert(tableView, qt.Contains, "force terminate")
c.Assert(tableView, qt.Not(qt.Contains), "cancel query")
c.Assert(tableView, qt.Not(qt.Contains), "kill transaction")
c.Assert(tableView, qt.Not(qt.Contains), "force terminate")

for _, key := range []string{"c", "k", "K"} {
updated, _ := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(key)})
Expand All @@ -1971,7 +1971,7 @@ func TestModelNekiConnectionView(t *testing.T) {
c.Assert(detailView, qt.Contains, "shard_backends: 1")
c.Assert(detailView, qt.Contains, "backend_pid: 4242")
c.Assert(detailView, qt.Not(qt.Contains), "left/right tabs")
c.Assert(detailView, qt.Contains, "cancel query")
c.Assert(detailView, qt.Not(qt.Contains), "cancel query")
}

func TestModelNekiConnectionViewSortsWithoutBlockers(t *testing.T) {
Expand Down
9 changes: 8 additions & 1 deletion internal/connections/tui/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,11 +711,18 @@ func renderFooter(state tableState) string {
} else if state.Notice != "" {
lines = append(lines, clipLine("status: "+state.Notice, tableWidth(state.Width)))
}
lines = append(lines, renderHelpFor(state.Width, state.ReadOnlyActions, state.Replay, state.DisplayPreset, state.Capabilities, state.CanSort, state.HasList, state.HasList && len(state.List.Connections) > 0, state.CanStepHistory, state.Paused))
lines = append(lines, renderHelpFor(state.Width, state.ReadOnlyActions, state.Replay, state.DisplayPreset, selectedCapabilities(state), state.CanSort, state.HasList, state.HasList && len(state.List.Connections) > 0, state.CanStepHistory, state.Paused))
}
return strings.Join(lines, "\n")
}

func selectedCapabilities(state tableState) ConnectionCapabilities {
if !state.HasList || state.Selected < 0 || state.Selected >= len(state.List.Connections) {
return state.Capabilities
}
return state.Capabilities.forConnection(state.List.Connections[state.Selected])
}

func renderSelectedStatus(state tableState) string {
if !state.HasList || state.Selected < 0 || state.Selected >= len(state.List.Connections) {
return ""
Expand Down
Loading