frostfs-node/cmd/frostfs-lens/internal/tui/records.go
Aleksey Savchuk ed396448ac [#1223] lens/tui: Add TUI app to explore metabase
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
2024-09-05 08:03:52 +00:00

271 lines
6.2 KiB
Go

package tui
import (
"context"
"errors"
"fmt"
"math"
"sync"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type updateType int
const (
other updateType = iota
moveToPrevPage
moveToNextPage
moveUp
moveDown
moveHome
moveEnd
)
type RecordsView struct {
*tview.Box
mu sync.RWMutex
onUnmount func()
bucket *Bucket
records []*Record
buffer chan *Record
firstRecordIndex int
lastRecordIndex int
selectedRecordIndex int
updateType updateType
ui *UI
filter *Filter
}
func NewRecordsView(ui *UI, bucket *Bucket, filter *Filter) *RecordsView {
return &RecordsView{
Box: tview.NewBox(),
bucket: bucket,
ui: ui,
filter: filter,
}
}
func (v *RecordsView) Mount(ctx context.Context) error {
if v.onUnmount != nil {
return errors.New("try to mount already mounted component")
}
ctx, v.onUnmount = context.WithCancel(ctx)
tempBuffer, err := LoadRecords(ctx, v.ui.db, v.bucket.Path, v.ui.loadBufferSize)
if err != nil {
return err
}
v.buffer = make(chan *Record, v.ui.loadBufferSize)
go func() {
defer close(v.buffer)
for item := range tempBuffer {
if item.err != nil {
v.ui.stopOnError(err)
break
}
record := item.val
record.Entry, _, err = v.bucket.NextParser(record.Key, record.Value)
if err != nil {
v.ui.stopOnError(err)
break
}
if v.filter.Apply(record.Entry).Result() != common.Yes {
continue
}
v.buffer <- record
}
}()
return nil
}
func (v *RecordsView) Unmount() {
if v.onUnmount == nil {
panic("try to unmount not mounted component")
}
v.onUnmount()
v.onUnmount = nil
}
func (v *RecordsView) Update(ctx context.Context) error {
_, _, _, recordsPerPage := v.GetInnerRect()
firstRecordIndex, lastRecordIndex, selectedRecordIndex := v.getNewIndexes()
loop:
for len(v.records) < lastRecordIndex {
select {
case <-ctx.Done():
return nil
case record, ok := <-v.buffer:
if !ok {
break loop
}
v.records = append(v.records, record)
}
}
// Set the update type to its default value after some specific key event
// has been handled.
v.updateType = other
firstRecordIndex = max(0, min(firstRecordIndex, len(v.records)-recordsPerPage))
lastRecordIndex = min(firstRecordIndex+recordsPerPage, len(v.records))
selectedRecordIndex = min(selectedRecordIndex, lastRecordIndex-1)
v.mu.Lock()
v.firstRecordIndex = firstRecordIndex
v.lastRecordIndex = lastRecordIndex
v.selectedRecordIndex = selectedRecordIndex
v.mu.Unlock()
return nil
}
func (v *RecordsView) getNewIndexes() (int, int, int) {
v.mu.RLock()
firstRecordIndex := v.firstRecordIndex
lastRecordIndex := v.lastRecordIndex
selectedRecordIndex := v.selectedRecordIndex
v.mu.RUnlock()
_, _, _, recordsPerPage := v.GetInnerRect()
switch v.updateType {
case moveUp:
if selectedRecordIndex != firstRecordIndex {
selectedRecordIndex--
break
}
firstRecordIndex = max(0, firstRecordIndex-1)
lastRecordIndex = min(firstRecordIndex+recordsPerPage, len(v.records))
selectedRecordIndex = firstRecordIndex
case moveToPrevPage:
if selectedRecordIndex != firstRecordIndex {
selectedRecordIndex = firstRecordIndex
break
}
firstRecordIndex = max(0, firstRecordIndex-recordsPerPage)
lastRecordIndex = firstRecordIndex + recordsPerPage
selectedRecordIndex = firstRecordIndex
case moveDown:
if selectedRecordIndex != lastRecordIndex-1 {
selectedRecordIndex++
break
}
firstRecordIndex++
lastRecordIndex++
selectedRecordIndex++
case moveToNextPage:
if selectedRecordIndex != lastRecordIndex-1 {
selectedRecordIndex = lastRecordIndex - 1
break
}
firstRecordIndex += recordsPerPage
lastRecordIndex = firstRecordIndex + recordsPerPage
selectedRecordIndex = lastRecordIndex - 1
case moveHome:
firstRecordIndex = 0
lastRecordIndex = firstRecordIndex + recordsPerPage
selectedRecordIndex = 0
case moveEnd:
lastRecordIndex = math.MaxInt32
firstRecordIndex = lastRecordIndex - recordsPerPage
selectedRecordIndex = lastRecordIndex - 1
default:
lastRecordIndex = firstRecordIndex + recordsPerPage
}
return firstRecordIndex, lastRecordIndex, selectedRecordIndex
}
func (v *RecordsView) GetInnerRect() (int, int, int, int) {
x, y, width, height := v.Box.GetInnerRect()
// Left padding.
x = min(x+3, x+width-1)
width = max(width-3, 0)
return x, y, width, height
}
func (v *RecordsView) Draw(screen tcell.Screen) {
v.mu.RLock()
firstRecordIndex := v.firstRecordIndex
lastRecordIndex := v.lastRecordIndex
selectedRecordIndex := v.selectedRecordIndex
records := v.records
v.mu.RUnlock()
v.DrawForSubclass(screen, v)
x, y, width, height := v.GetInnerRect()
if height == 0 {
return
}
// No records in that bucket.
if firstRecordIndex == lastRecordIndex {
tview.Print(
screen, "Empty Bucket", x, y, width, tview.AlignCenter, tview.Styles.PrimaryTextColor,
)
return
}
for index := firstRecordIndex; index < lastRecordIndex; index++ {
result := records[index].Entry
text := result.String()
if index == selectedRecordIndex {
text = fmt.Sprintf("[:white]%s[:-]", text)
tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimitiveBackgroundColor)
} else {
tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimaryTextColor)
}
y++
}
}
func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.Primitive)) {
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) {
switch m, k := event.Modifiers(), event.Key(); {
case m == 0 && k == tcell.KeyPgUp:
v.updateType = moveToPrevPage
case m == 0 && k == tcell.KeyPgDn:
v.updateType = moveToNextPage
case m == 0 && k == tcell.KeyUp:
v.updateType = moveUp
case m == 0 && k == tcell.KeyDown:
v.updateType = moveDown
case m == 0 && k == tcell.KeyHome:
v.updateType = moveHome
case m == 0 && k == tcell.KeyEnd:
v.updateType = moveEnd
case k == tcell.KeyEnter:
v.mu.RLock()
selectedRecordIndex := v.selectedRecordIndex
records := v.records
v.mu.RUnlock()
if len(records) != 0 {
current := records[selectedRecordIndex]
v.ui.moveNextPage(NewDetailedView(current.Entry.DetailedString()))
}
}
})
}