272 lines
6.2 KiB
Go
272 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()))
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|