From 8b3cdc71531bde07c8eec5b948bca3ceca80ff9f Mon Sep 17 00:00:00 2001 From: "Y.Matsuda" Date: Mon, 4 Nov 2024 11:10:36 +0900 Subject: [PATCH] Full text modal for attribute tree --- tuiexporter/internal/tui/component/log.go | 4 +- .../internal/tui/component/log_test.go | 2 +- tuiexporter/internal/tui/component/metric.go | 4 +- tuiexporter/internal/tui/component/page.go | 49 +++++++++++++++++-- .../internal/tui/component/timeline.go | 14 +++--- tuiexporter/internal/tui/component/trace.go | 8 ++- .../internal/tui/component/trace_test.go | 11 ++++- tuiexporter/internal/tui/component/tree.go | 47 ++++++++++++++++++ 8 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 tuiexporter/internal/tui/component/tree.go diff --git a/tuiexporter/internal/tui/component/log.go b/tuiexporter/internal/tui/component/log.go index 5359cd3..664a734 100644 --- a/tuiexporter/internal/tui/component/log.go +++ b/tuiexporter/internal/tui/component/log.go @@ -81,7 +81,7 @@ func getCellFromLog(log *telemetry.LogData, column int) *tview.TableCell { return tview.NewTableCell(text) } -func getLogInfoTree(commands *tview.TextView, l *telemetry.LogData, tcache *telemetry.TraceCache, drawTimelineFn func(traceID string)) *tview.TreeView { +func getLogInfoTree(commands *tview.TextView, showModalFn showModalFunc, hideModalFn hideModalFunc, l *telemetry.LogData, tcache *telemetry.TraceCache, drawTimelineFn func(traceID string)) *tview.TreeView { if l == nil { return tview.NewTreeView() } @@ -169,6 +169,8 @@ func getLogInfoTree(commands *tview.TextView, l *telemetry.LogData, tcache *tele node.SetExpanded(!node.IsExpanded()) }) + attachModalForTreeAttributes(tree, showModalFn, hideModalFn) + registerCommandList(commands, tree, nil, KeyMaps{ { key: tcell.NewEventKey(tcell.KeyRune, 'L', tcell.ModCtrl), diff --git a/tuiexporter/internal/tui/component/log_test.go b/tuiexporter/internal/tui/component/log_test.go index 369dbba..a6e4b94 100644 --- a/tuiexporter/internal/tui/component/log_test.go +++ b/tuiexporter/internal/tui/component/log_test.go @@ -182,7 +182,7 @@ func TestGetLogInfoTree(t *testing.T) { screen.Init() screen.SetSize(sw, sh) - gottree := getLogInfoTree(nil, logs[0], nil, nil) + gottree := getLogInfoTree(nil, noopShowModalFn, noopHideModalFn, logs[0], nil, nil) gottree.SetRect(0, 0, sw, sh) gottree.Draw(screen) screen.Sync() diff --git a/tuiexporter/internal/tui/component/metric.go b/tuiexporter/internal/tui/component/metric.go index 45dc91a..b01d6c4 100644 --- a/tuiexporter/internal/tui/component/metric.go +++ b/tuiexporter/internal/tui/component/metric.go @@ -90,7 +90,7 @@ func getCellFromMetrics(metric *telemetry.MetricData, column int) *tview.TableCe return tview.NewTableCell(text) } -func getMetricInfoTree(commands *tview.TextView, m *telemetry.MetricData) *tview.TreeView { +func getMetricInfoTree(commands *tview.TextView, showModalFn showModalFunc, hideModalFn hideModalFunc, m *telemetry.MetricData) *tview.TreeView { if m == nil { return nil } @@ -371,6 +371,8 @@ func getMetricInfoTree(commands *tview.TextView, m *telemetry.MetricData) *tview node.SetExpanded(!node.IsExpanded()) }) + attachModalForTreeAttributes(tree, showModalFn, hideModalFn) + registerCommandList(commands, tree, nil, KeyMaps{ { key: tcell.NewEventKey(tcell.KeyRune, 'L', tcell.ModCtrl), diff --git a/tuiexporter/internal/tui/component/page.go b/tuiexporter/internal/tui/component/page.go index 06732de..d175307 100644 --- a/tuiexporter/internal/tui/component/page.go +++ b/tuiexporter/internal/tui/component/page.go @@ -1,6 +1,7 @@ package component import ( + "fmt" "log" "strings" @@ -16,6 +17,7 @@ const ( PAGE_LOGS = "Logs" PAGE_DEBUG_LOG = "DebugLog" PAGE_METRICS = "Metrics" + PAGE_MODAL = "Modal" DEFAULT_PROPORTION_TRACE_DETAILS = 20 DEFAULT_PROPORTION_TRACE_TABLE = 30 @@ -32,6 +34,7 @@ type TUIPages struct { metrics *tview.Flex logs *tview.Flex debuglog *tview.Flex + modal *tview.Flex current string setFocusFn func(p tview.Primitive) // This is used when other components trigger to draw the timeline @@ -71,6 +74,20 @@ func (p *TUIPages) ToggleLog() { } } +func (p *TUIPages) showModal(current tview.Primitive, text string) *tview.TextView { + textView := p.updateModelPage(text) + p.pages.ShowPage(PAGE_MODAL) + p.pages.SendToFront(PAGE_MODAL) + p.setFocusFn(current) + return textView +} + +func (p *TUIPages) hideModal(current tview.Primitive) { + p.pages.SendToBack(PAGE_MODAL) + p.pages.HidePage(PAGE_MODAL) + p.setFocusFn(current) +} + // TogglePage toggles Traces & Logs page. func (p *TUIPages) TogglePage() { if p.current == PAGE_TRACES { @@ -88,6 +105,10 @@ func (p *TUIPages) switchToPage(name string) { } func (p *TUIPages) registerPages(store *telemetry.Store) { + modal, _ := p.createModalPage("") + p.modal = modal + p.pages.AddPage(PAGE_MODAL, modal, true, true) + logpage := p.createDebugLogPage() p.debuglog = logpage p.pages.AddPage(PAGE_DEBUG_LOG, logpage, true, true) @@ -230,7 +251,7 @@ func (p *TUIPages) createTracePage(store *telemetry.Store) *tview.Flex { return } details.Clear() - details.AddItem(getTraceInfoTree(commands, store.GetFilteredServiceSpansByIdx(row-1)), 0, 1, true) + details.AddItem(getTraceInfoTree(commands, p.showModal, p.hideModal, store.GetFilteredServiceSpansByIdx(row-1)), 0, 1, true) log.Printf("selected row(original): %d", row) }) tableContainer. @@ -284,6 +305,26 @@ func (p *TUIPages) createTimelinePage() *tview.Flex { return page } +func (p *TUIPages) createModalPage(text string) (*tview.Flex, *tview.TextView) { + textView := tview.NewTextView() + textView.SetBorder(true) + fmt.Fprint(textView, text) + return tview.NewFlex().SetDirection(tview.FlexColumn). + AddItem(nil, 0, 2, false). + AddItem(nil, 0, 2, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 2, false). + AddItem(nil, 0, 1, false). + AddItem(textView, 0, 1, false), 0, 3, false), textView +} + +func (p *TUIPages) updateModelPage(text string) *tview.TextView { + modal, textView := p.createModalPage(text) + p.modal = modal + p.pages.AddPage(PAGE_MODAL, modal, true, false) + return textView +} + func (p *TUIPages) showTimelineByRow(store *telemetry.Store, row int) { if store == nil { return @@ -302,6 +343,8 @@ func (p *TUIPages) showTimeline(traceID string, tcache *telemetry.TraceCache, lc timeline := tview.NewFlex().SetDirection(tview.FlexRow) tl := DrawTimeline( p.commandsTimeline, + p.showModal, + p.hideModal, traceID, tcache, lcache, @@ -424,7 +467,7 @@ func (p *TUIPages) createMetricsPage(store *telemetry.Store) *tview.Flex { } selected := store.GetFilteredMetricByIdx(row - 1) details.Clear() - details.AddItem(getMetricInfoTree(commands, selected), 0, 1, true) + details.AddItem(getMetricInfoTree(commands, p.showModal, p.hideModal, selected), 0, 1, true) // TODO: async rendering with spinner chart.Clear() chart.AddItem(drawMetricChartByRow(commands, store, row-1), 0, 1, true) @@ -580,7 +623,7 @@ func (p *TUIPages) createLogPage(store *telemetry.Store) *tview.Flex { } selected := store.GetFilteredLogByIdx(row - 1) details.Clear() - details.AddItem(getLogInfoTree(commands, selected, store.GetTraceCache(), func(traceID string) { + details.AddItem(getLogInfoTree(commands, p.showModal, p.hideModal, selected, store.GetTraceCache(), func(traceID string) { p.showTimeline(traceID, store.GetTraceCache(), store.GetLogCache(), func(pr tview.Primitive) { p.setFocusFn(pr) }) diff --git a/tuiexporter/internal/tui/component/timeline.go b/tuiexporter/internal/tui/component/timeline.go index 14b35e7..a2d532c 100644 --- a/tuiexporter/internal/tui/component/timeline.go +++ b/tuiexporter/internal/tui/component/timeline.go @@ -42,7 +42,7 @@ type spanTreeNode struct { children []*spanTreeNode } -func DrawTimeline(commands *tview.TextView, traceID string, tcache *telemetry.TraceCache, lcache *telemetry.LogCache, setFocusFn func(p tview.Primitive)) tview.Primitive { +func DrawTimeline(commands *tview.TextView, showModalFn showModalFunc, hideModalFn hideModalFunc, traceID string, tcache *telemetry.TraceCache, lcache *telemetry.LogCache, setFocusFn func(p tview.Primitive)) tview.Primitive { if traceID == "" || tcache == nil { return newTextView(commands, "No spans found") } @@ -122,7 +122,7 @@ func DrawTimeline(commands *tview.TextView, traceID string, tcache *telemetry.Tr } // details - details := getSpanInfoTree(commands, nodes[0].span, TIMELINE_TREE_TITLE) + details := getSpanInfoTree(commands, showModalFn, hideModalFn, nodes[0].span, TIMELINE_TREE_TITLE) detailspro := DEFAULT_PROPORTION_TIMELINE_DETAILS gridpro := DEFAULT_PROPORTION_TIMELINE_GRID @@ -154,7 +154,7 @@ func DrawTimeline(commands *tview.TextView, traceID string, tcache *telemetry.Tr // update details oldDetails := traceContainer.GetItem(TIMELINE_DETAILS_IDX) traceContainer.RemoveItem(oldDetails) - details := getSpanInfoTree(commands, nodes[currentRow].span, TIMELINE_TREE_TITLE) + details := getSpanInfoTree(commands, showModalFn, hideModalFn, nodes[currentRow].span, TIMELINE_TREE_TITLE) details.SetInputCapture(detailsInputFunc(traceContainer, grid, details, &gridpro, &detailspro)) traceContainer.AddItem(details, 0, detailspro, false) } @@ -163,12 +163,12 @@ func DrawTimeline(commands *tview.TextView, traceID string, tcache *telemetry.Tr if currentRow > 0 { currentRow-- setFocusFn(tvs[currentRow]) - details = getSpanInfoTree(commands, nodes[currentRow].span, TIMELINE_TREE_TITLE) + details = getSpanInfoTree(commands, showModalFn, hideModalFn, nodes[currentRow].span, TIMELINE_TREE_TITLE) // update details oldDetails := traceContainer.GetItem(TIMELINE_DETAILS_IDX) traceContainer.RemoveItem(oldDetails) - details := getSpanInfoTree(commands, nodes[currentRow].span, TIMELINE_TREE_TITLE) + details := getSpanInfoTree(commands, showModalFn, hideModalFn, nodes[currentRow].span, TIMELINE_TREE_TITLE) details.SetInputCapture(detailsInputFunc(traceContainer, grid, details, &gridpro, &detailspro)) traceContainer.AddItem(details, 0, detailspro, false) } @@ -465,7 +465,7 @@ func createSpan(color tcell.Color, total, start, end time.Duration) (span *tview }) } -func getSpanInfoTree(commands *tview.TextView, span *telemetry.SpanData, title string) *tview.TreeView { +func getSpanInfoTree(commands *tview.TextView, showModalFn showModalFunc, hideModalFn hideModalFunc, span *telemetry.SpanData, title string) *tview.TreeView { traceID := span.Span.TraceID().String() sname, _ := span.ResourceSpan.Resource().Attributes().Get("service.name") root := tview.NewTreeNode(fmt.Sprintf("%s (%s)", sname.AsString(), traceID)) @@ -592,6 +592,8 @@ func getSpanInfoTree(commands *tview.TextView, span *telemetry.SpanData, title s node.SetExpanded(!node.IsExpanded()) }) + attachModalForTreeAttributes(tree, showModalFn, hideModalFn) + registerCommandList(commands, tree, nil, KeyMaps{ { key: tcell.NewEventKey(tcell.KeyRune, 'L', tcell.ModCtrl), diff --git a/tuiexporter/internal/tui/component/trace.go b/tuiexporter/internal/tui/component/trace.go index fbabb9c..353b5c4 100644 --- a/tuiexporter/internal/tui/component/trace.go +++ b/tuiexporter/internal/tui/component/trace.go @@ -107,7 +107,7 @@ func getHeaderCell(header []string, column int, sortType *telemetry.SortType) *t return cell } -func getTraceInfoTree(commands *tview.TextView, spans []*telemetry.SpanData) *tview.TreeView { +func getTraceInfoTree(commands *tview.TextView, showModalFn showModalFunc, hideModalFn hideModalFunc, spans []*telemetry.SpanData) *tview.TreeView { if len(spans) == 0 { return tview.NewTreeView() } @@ -157,9 +157,7 @@ func getTraceInfoTree(commands *tview.TextView, spans []*telemetry.SpanData) *tv root.AddChild(resource) - tree.SetSelectedFunc(func(node *tview.TreeNode) { - node.SetExpanded(!node.IsExpanded()) - }) + attachModalForTreeAttributes(tree, showModalFn, hideModalFn) registerCommandList(commands, tree, nil, KeyMaps{ { @@ -172,7 +170,7 @@ func getTraceInfoTree(commands *tview.TextView, spans []*telemetry.SpanData) *tv }, { key: tcell.NewEventKey(tcell.KeyEnter, ' ', tcell.ModNone), - description: "Toggle folding the child nodes", + description: "Toggle folding (parent), Show full text (child)", }, }) diff --git a/tuiexporter/internal/tui/component/trace_test.go b/tuiexporter/internal/tui/component/trace_test.go index e6e77b7..c1d6fe6 100644 --- a/tuiexporter/internal/tui/component/trace_test.go +++ b/tuiexporter/internal/tui/component/trace_test.go @@ -7,12 +7,19 @@ import ( "time" "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" "github.com/stretchr/testify/assert" "github.com/ymtdzzz/otel-tui/tuiexporter/internal/telemetry" "github.com/ymtdzzz/otel-tui/tuiexporter/internal/test" "go.opentelemetry.io/collector/pdata/ptrace" ) +var noopShowModalFn showModalFunc = func(p tview.Primitive, s string) *tview.TextView { + return tview.NewTextView() +} + +var noopHideModalFn hideModalFunc = func(p tview.Primitive) {} + func TestSpanDataForTable(t *testing.T) { // traceid: 1 // └- resource: test-service-1 @@ -232,7 +239,7 @@ func TestGetTraceInfoTree(t *testing.T) { screen.Init() screen.SetSize(sw, sh) - gottree := getTraceInfoTree(nil, spans) + gottree := getTraceInfoTree(nil, noopShowModalFn, noopHideModalFn, spans) gottree.SetRect(0, 0, sw, sh) gottree.Draw(screen) screen.Sync() @@ -276,5 +283,5 @@ func TestGetTraceInfoTree(t *testing.T) { } func TestGetTraceInfoTreeNoSpans(t *testing.T) { - assert.Nil(t, getTraceInfoTree(nil, nil).GetRoot()) + assert.Nil(t, getTraceInfoTree(nil, noopShowModalFn, noopHideModalFn, nil).GetRoot()) } diff --git a/tuiexporter/internal/tui/component/tree.go b/tuiexporter/internal/tui/component/tree.go new file mode 100644 index 0000000..e116683 --- /dev/null +++ b/tuiexporter/internal/tui/component/tree.go @@ -0,0 +1,47 @@ +package component + +import ( + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type showModalFunc func(tview.Primitive, string) *tview.TextView + +type hideModalFunc func(tview.Primitive) + +func attachModalForTreeAttributes(tree *tview.TreeView, showFn showModalFunc, hideFn hideModalFunc) { + var currentModalNode *tview.TreeNode = nil + tree.SetSelectedFunc(func(node *tview.TreeNode) { + if len(node.GetChildren()) > 0 { + node.SetExpanded(!node.IsExpanded()) + return + } + if currentModalNode == node { + hideFn(tree) + currentModalNode = nil + return + } + textView := showFn(tree, node.GetText()) + textView.SetTitle("Scroll (Ctrl+J, Ctrl+K)") + currentModalNode = node + tree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyCtrlJ: + row, col := textView.GetScrollOffset() + textView.ScrollTo(row+1, col) + return nil + case tcell.KeyCtrlK: + row, col := textView.GetScrollOffset() + textView.ScrollTo(row-1, col) + return nil + } + return event + }) + }) + tree.SetChangedFunc(func(node *tview.TreeNode) { + if currentModalNode != nil { + hideFn(tree) + currentModalNode = nil + } + }) +}