Skip to content

Commit 1ce9271

Browse files
committed
feat(tui): improve UX with spinner and query replacement
- Add spinner component to show loading state visually - Store selected field to enable query value replacement - Implement query replacement logic instead of always appending - Adjust layout spacing (remove empty lines, adjust pane height) - Fix bug where results list was set before items were added - Show spinner during initial load and search operations - Display debug info alongside status in UI The query replacement logic allows users to change values for the same field without creating duplicate conditions, improving the query building experience.
1 parent b9acee4 commit 1ce9271

File tree

1 file changed

+62
-29
lines changed

1 file changed

+62
-29
lines changed

tui.go

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/Ajnasz/go-loggly-cli/search"
1313
"github.com/charmbracelet/bubbles/list"
14+
"github.com/charmbracelet/bubbles/spinner"
1415
"github.com/charmbracelet/bubbles/textinput"
1516
"github.com/charmbracelet/bubbles/viewport"
1617
tea "github.com/charmbracelet/bubbletea"
@@ -146,8 +147,11 @@ type model struct {
146147
valuesList list.Model
147148
resultsList list.Model
148149
detailView viewport.Model
150+
spinner spinner.Model
149151
debugView string
150152

153+
selectedField fieldItem
154+
151155
currentPane pane
152156
width int
153157
height int
@@ -197,7 +201,6 @@ func initialModel(ctx context.Context, account, token string, size int, maxPages
197201
ti.Placeholder = "Enter your Loggly query..."
198202
ti.Focus()
199203
ti.CharLimit = 500
200-
ti.Width = 50
201204
ti.SetValue(query)
202205

203206
fieldsList := list.New([]list.Item{}, list.NewDefaultDelegate(), 20, 20)
@@ -236,6 +239,7 @@ func initialModel(ctx context.Context, account, token string, size int, maxPages
236239
valuesList: valuesList,
237240
resultsList: resultsList,
238241
detailView: detailView,
242+
spinner: spinner.New(),
239243
debugView: "",
240244
currentPane: queryPane,
241245
allFields: make(map[string]int),
@@ -246,7 +250,7 @@ func initialModel(ctx context.Context, account, token string, size int, maxPages
246250
}
247251

248252
func (m model) Init() tea.Cmd {
249-
return textinput.Blink
253+
return tea.Batch(textinput.Blink, m.spinner.Tick)
250254
}
251255

252256
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -290,14 +294,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
290294
m.loading = true
291295
return m, m.executeQuery()
292296
}
297+
293298
if m.currentPane == fieldsPane {
294299
cmd := m.selectField()
295300
return m, cmd
296301
}
302+
297303
if m.currentPane == valuesPane {
298304
m.addValueToQuery()
305+
if m.loading {
306+
return m, nil
307+
}
308+
309+
m.loading = true
299310
return m, m.executeQuery()
300311
}
312+
301313
if m.currentPane == resultsPane {
302314
// Show detail view for selected result
303315
if item, ok := m.resultsList.SelectedItem().(resultItem); ok {
@@ -316,6 +328,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
316328
}
317329
}
318330

331+
case spinner.TickMsg:
332+
var cmd tea.Cmd
333+
m.spinner, cmd = m.spinner.Update(msg)
334+
return m, cmd
335+
319336
case resultsMsg:
320337
m.loading = false
321338
if msg.err != nil {
@@ -333,8 +350,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
333350
}
334351
m.debugView = fmt.Sprintf("Loaded %d results", len(msg.results))
335352
return m, nil
353+
336354
case fieldSelectedMsg:
337-
// Field was selected, lists are already updated
338355
return m, nil
339356
}
340357

@@ -377,7 +394,7 @@ func (m *model) updateSizes() {
377394
borderWidth := 6 // 2 chars per pane * 3 panes
378395
rightPaneWidth := m.width - leftPaneWidth - midPaneWidth - borderWidth
379396

380-
paneHeight := m.height - 10
397+
paneHeight := m.height - 8
381398

382399
// Set sizes to content area (borders will be added by lipgloss)
383400
m.fieldsList.SetSize(leftPaneWidth, paneHeight-2)
@@ -408,7 +425,7 @@ func (m *model) updateFocus() {
408425

409426
func (m model) View() string {
410427
if m.width == 0 {
411-
return "Loading..."
428+
return m.spinner.View()
412429
}
413430

414431
// If showing detail view, render it full screen
@@ -471,19 +488,20 @@ func (m model) View() string {
471488

472489
status := ""
473490
if m.loading {
474-
status = "Loading..."
491+
status = m.spinner.View() + " Loading..."
475492
} else if m.err != nil {
476493
status = fmt.Sprintf("Error: %s", m.err)
477494
} else if len(m.results) > 0 {
478495
status = fmt.Sprintf("%d results", len(m.results))
479496
}
480497

481-
return lipgloss.JoinVertical(lipgloss.Left,
498+
status = status + " " + m.debugView
499+
500+
return lipgloss.JoinVertical(
501+
lipgloss.Left,
482502
querySection,
483-
"",
484503
lipgloss.NewStyle().Foreground(lipgloss.Color("170")).Render(fieldTitle),
485504
panesRow,
486-
"",
487505
status,
488506
// m.debugView,
489507
help,
@@ -558,8 +576,6 @@ func (m *model) analyzeObject(obj map[string]any, path []string) {
558576
}
559577

560578
func (m *model) updateFieldsList() {
561-
var items []list.Item
562-
563579
// Get fields at current path level
564580
prefix := ""
565581
if len(m.fieldPath) > 0 {
@@ -587,6 +603,8 @@ func (m *model) updateFieldsList() {
587603
return fields[i].count > fields[j].count
588604
})
589605

606+
var items []list.Item
607+
590608
for _, f := range fields {
591609
items = append(items, f)
592610
}
@@ -596,6 +614,7 @@ func (m *model) updateFieldsList() {
596614

597615
func (m *model) selectField() tea.Cmd {
598616
if item, ok := m.fieldsList.SelectedItem().(fieldItem); ok {
617+
m.selectedField = item
599618
// Check if this field has nested fields
600619
testPath := append(m.fieldPath, item.name)
601620
pathStr := strings.Join(testPath, ".")
@@ -647,6 +666,7 @@ func (m *model) updateResultsView() {
647666
var items []list.Item
648667

649668
for i, result := range m.results {
669+
m.resultsList.SetItems(items)
650670
items = append(items, resultItem{
651671
index: i,
652672
data: result,
@@ -660,28 +680,41 @@ func (m *model) updateResultsView() {
660680
}
661681
}
662682

683+
func replaceExisitingSearch(query, field, value string) string {
684+
// Simple replacement logic: look for field:value and replace it
685+
parts := strings.Split(query, " AND ")
686+
for i, part := range parts {
687+
if strings.HasPrefix(part, field+":") {
688+
parts[i] = fmt.Sprintf("%s:%s", field, value)
689+
return strings.Join(parts, " AND ")
690+
}
691+
}
692+
// If not found, append
693+
if query != "" {
694+
return query + " AND " + fmt.Sprintf("%s:%s", field, value)
695+
}
696+
return fmt.Sprintf("%s:%s", field, value)
697+
}
698+
663699
func (m *model) addValueToQuery() tea.Cmd {
664-
if selectedField, ok := m.fieldsList.SelectedItem().(fieldItem); ok {
665-
if selectedValue, ok := m.valuesList.SelectedItem().(valueItem); ok {
666-
fieldPath := append(m.fieldPath, selectedField.name)
667-
fieldStr := "json." + strings.Join(fieldPath, ".")
668-
669-
current := m.queryInput.Value()
670-
if current != "" {
671-
current += " AND "
672-
}
700+
if m.selectedField.name == "" {
701+
return nil
702+
}
673703

674-
// Quote value if it contains spaces
675-
value := selectedValue.value
676-
if strings.Contains(value, " ") {
677-
value = fmt.Sprintf(`"%s"`, value)
678-
}
704+
selectedField := m.selectedField
705+
if selectedValue, ok := m.valuesList.SelectedItem().(valueItem); ok {
706+
fieldPath := append(m.fieldPath, selectedField.name)
707+
fieldStr := "json." + strings.Join(fieldPath, ".")
679708

680-
m.queryInput.SetValue(current + fmt.Sprintf("%s:%s", fieldStr, value))
681-
m.debugView = fmt.Sprintf("Added to query: %s:%s", fieldStr, value)
682-
}
709+
current := m.queryInput.Value()
710+
value := selectedValue.value
711+
712+
m.queryInput.SetValue(replaceExisitingSearch(current, fieldStr, value))
713+
m.debugView = fmt.Sprintf("Added to query: %s:%s", fieldStr, value)
714+
return func() tea.Msg { return fieldSelectedMsg{} }
683715
}
684-
return func() tea.Msg { return fieldSelectedMsg{} }
716+
717+
return nil
685718
}
686719

687720
func (m *model) showDetailView(item resultItem) {

0 commit comments

Comments
 (0)