[#1223] lens/tui: Add search by CID and OID

Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
Aleksey Savchuk 2024-07-30 15:47:50 +03:00
parent c65b3a9ba9
commit ec66b4cee2
No known key found for this signature in database
12 changed files with 648 additions and 505 deletions

View file

@ -3,6 +3,9 @@ package meta
import (
"context"
"fmt"
"os"
"runtime/pprof"
"time"
common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/tuiutil"
@ -22,7 +25,32 @@ func init() {
}
func tuiFunc(cmd *cobra.Command, _ []string) {
err := runTUI(cmd)
cpuProfile, err := os.Create(fmt.Sprintf("tui-cpu-%s.prof", time.Now()))
if err != nil {
panic(err)
}
defer cpuProfile.Close()
err = pprof.StartCPUProfile(cpuProfile)
if err != nil {
panic(err)
}
defer pprof.StopCPUProfile()
memProfile, err := os.Create(fmt.Sprintf("tui-mem-%s.prof", time.Now()))
if err != nil {
panic(err)
}
defer memProfile.Close()
defer func() {
err = pprof.WriteHeapProfile(memProfile)
if err != nil {
panic(err)
}
}()
err = runTUI(cmd)
common.ExitOnErr(cmd, err)
}

View file

@ -2,19 +2,24 @@ package tuiutil
import (
"context"
"errors"
"fmt"
"os"
"sync/atomic"
"time"
handlers "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/schema"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/davecgh/go-spew/spew"
"github.com/gdamore/tcell/v2"
"github.com/mr-tron/base58"
"github.com/rivo/tview"
"go.etcd.io/bbolt"
)
// 29NkY3QtH6r8Hd9RBejwtUuju1ViAQ6jZPR7pjuTnkoH
// 1QVJo3LCACFnbeqJ7uHz97PyoPrkZr4BnZ9cVYAhRPx
// 6E6b24qy32p3L3wjRSUEGNFHMxkUsaqj7udwWVKLzkU
// 1GcnNjFnPof2YbPi2RBi3Sjy3qeR2cymc9BR6i9U2Kt
type Bucket struct {
name []byte
path [][]byte
@ -48,7 +53,7 @@ func NewApplication(ctx context.Context, db *bbolt.DB) (*Application, error) {
a.nav = NewNavigation(a.app)
a.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Rune() == 'q' {
if event.Key() == tcell.KeyEsc {
a.app.Stop()
return nil
}
@ -74,227 +79,74 @@ func (a *Application) stopOnErr(err error) {
}
func (a *Application) newIntoView(ctx context.Context) (tview.Primitive, error) {
view := tview.NewBox()
view.SetBorder(true).SetTitle("Intro")
form := tview.NewForm()
view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEnter {
err := a.nav.Push(ctx, a.newBucketsView)
a.stopOnErr(err)
return nil
unsetErrMsg := func() {
if form.GetFormItemCount() >= 4 {
form.RemoveFormItem(3)
}
return event
})
return view, nil
}
func (a *Application) newBucketsView(ctx context.Context) (tview.Primitive, error) {
tree := tview.NewTreeView()
tree.
SetBorder(true).
SetTitle(a.db.Path())
// id := cid.ID{}
// data, _ := base58.Decode("29NkY3QtH6r8Hd9RBejwtUuju1ViAQ6jZPR7pjuTnkoH")
// id.Decode(data)
handler := handlers.GetHandler(
// handlers.WithCID(id),
)
root := tview.NewTreeNode(".")
root.
SetSelectable(false).
SetExpanded(true).
SetReference(&Bucket{nextHandler: handler})
err := a.getChildren(ctx, root)
if err != nil {
return nil, err
}
setErrMsg := func(msg string) {
form.AddTextView("", fmt.Sprintf("[red]%s[white]", msg), 45, 1, true, false)
}
tree.
SetRoot(root).
SetCurrentNode(root)
var cidText, oidText string
tree.SetSelectedFunc(func(node *tview.TreeNode) {
if node.IsExpanded() {
node.ClearChildren()
} else {
err := a.getChildren(ctx, node)
a.stopOnErr(err)
}
node.SetExpanded(!node.IsExpanded())
})
form.
AddInputField("CID", "", 45, nil, func(text string) { cidText = text }).
AddInputField("OID", "", 45, nil, func(text string) { oidText = text }).
AddButton("Search", func() {
unsetErrMsg()
a.setBucketsInputCapture(ctx, tree)
return tree, nil
}
func (a *Application) getChildren(ctx context.Context, parent *tview.TreeNode) error {
parentBucket := parent.GetReference().(*Bucket)
path := parentBucket.path
handler := parentBucket.nextHandler
buffer, err := LoadBuckets(ctx, a.db, path)
if err != nil {
return err
}
for bucket := range buffer {
repr, next, err := handler(bucket.name, nil)
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
if errors.Is(err, handlers.ErrFilter) {
continue
}
if err != nil {
return err
}
bucket.nextHandler = next
bucket.result = repr
child := tview.NewTreeNode(repr.Key.String())
child.SetSelectable(true)
child.SetExpanded(false)
child.SetReference(bucket)
parent.AddChild(child)
}
return nil
}
func (a *Application) setBucketsInputCapture(ctx context.Context, view *tview.TreeView) {
view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyTab:
err := a.nav.Push(ctx, func(ctx context.Context) (tview.Primitive, error) {
ref := view.GetCurrentNode().GetReference().(*Bucket)
return a.NewRecordsView(ctx, ref), nil
})
cidVal := &cid.ID{}
data, err := base58.Decode(cidText)
if err != nil {
a.stopOnErr(err)
return nil
}
default:
return event
}
return nil
})
}
func (a *Application) NewRecordsView(ctx context.Context, bkt *Bucket) tview.Primitive {
view := NewMultipageView(a.app)
view.SetBorder(true)
buffer, err := LoadRecords(ctx, a.db, bkt.path)
if err != nil {
a.stopOnErr(err)
return nil
}
view.SetNextItemFunc(func() *MultipageItem {
record, ok := <-buffer
if !ok {
return nil
}
repr, next, err := bkt.nextHandler(record.key, record.value)
if errors.Is(err, handlers.ErrFilter) {
err = nil
}
if err != nil {
a.stopOnErr(err)
return nil
}
record.nextHandler = next
record.result = repr
return &MultipageItem{
Text: repr.Key.String() + " " + repr.Val.String(),
Reference: record,
}
})
view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyBacktab {
a.nav.Pop()
return nil
}
if event.Key() == tcell.KeyTab {
item := view.GetSelectedItem()
if item == nil {
return nil
}
err := a.nav.Push(ctx, func(_ context.Context) (tview.Primitive, error) {
rec := item.Reference.(*Record)
text := tview.NewTextView()
text.SetBorder(true).SetTitle(rec.result.Key.String())
text.SetText(rec.result.Val.DetailedString())
text.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyBacktab {
a.nav.Pop()
return nil
}
return event
})
return text, nil
})
if err != nil {
a.stopOnErr(err)
return nil
}
}
return event
})
view.OnLoading(
func() {
err := a.nav.Push(ctx, a.NewLoadingScreen)
if err != nil {
a.stopOnErr(err)
}
},
func() { a.nav.Pop() },
)
return view
}
func (a *Application) NewLoadingScreen(ctx context.Context) (tview.Primitive, error) {
view := tview.NewBox()
view.SetBorder(true)
var seconds atomic.Int32
view.SetTitle(fmt.Sprintf("Loading... %ds", seconds.Load()))
go func() {
tick := time.NewTicker(1 * time.Second)
for {
select {
case <-ctx.Done():
setErrMsg("not a valid CID")
return
case <-tick.C:
a.app.QueueUpdateDraw(func() {
seconds.Add(1)
view.SetTitle(fmt.Sprintf("Loading... %ds", seconds.Load()))
})
}
}
}()
err = cidVal.Decode(data)
if err != nil {
setErrMsg("not a valid CID")
return
}
oidVal := &oid.ID{}
data, err = base58.Decode(oidText)
if err != nil {
setErrMsg("not a valid OID")
return
}
err = oidVal.Decode(data)
if err != nil {
setErrMsg("not a valid OID")
return
}
err = a.nav.Push(ctx, func(ctx context.Context) (tview.Primitive, error) {
return a.newBucketsView(ctx, cidVal, oidVal)
})
a.stopOnErr(err)
}).
AddButton("Continue w/o search", func() {
err := a.nav.Push(ctx, func(ctx context.Context) (tview.Primitive, error) {
return a.newBucketsView(ctx, nil, nil)
})
a.stopOnErr(err)
})
form.SetBorder(true)
view := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(tview.NewBox(), 0, 1, false).
AddItem(
tview.NewFlex().
AddItem(tview.NewBox(), 0, 1, false).
AddItem(form, 54, 0, true).
AddItem(tview.NewBox(), 0, 1, false),
13, 0, true,
).
AddItem(tview.NewBox(), 0, 1, false)
return view, nil
}
// func (a *Application) NewDetailedView(_ context.Context, rec *Record) (tview.Primitive, error) {
// }

View file

@ -0,0 +1,111 @@
package tuiutil
import (
"context"
"errors"
handlers "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/schema"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func (a *Application) newBucketsView(ctx context.Context, cnr *cid.ID, obj *oid.ID) (tview.Primitive, error) {
tree := tview.NewTreeView()
tree.
SetBorder(true).
SetTitle(a.db.Path())
var filters []handlers.Filter
if cnr != nil {
filters = append(filters, handlers.WithCID(*cnr))
}
if obj != nil {
filters = append(filters, handlers.WithOID(*obj))
}
handler := handlers.GetHandler(filters...)
root := tview.NewTreeNode(".")
root.
SetSelectable(false).
SetExpanded(true).
SetReference(&Bucket{nextHandler: handler})
err := a.getChildren(ctx, root)
if err != nil {
return nil, err
}
tree.
SetRoot(root).
SetCurrentNode(root).
SetSelectedFunc(nil)
tree.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
node := tree.GetCurrentNode()
switch event.Key() {
case tcell.KeyEnter:
if node.IsExpanded() {
node.ClearChildren()
} else {
err := a.getChildren(ctx, node)
a.stopOnErr(err)
}
node.SetExpanded(!node.IsExpanded())
case tcell.KeyTab:
err := a.nav.Push(ctx, func(ctx context.Context) (tview.Primitive, error) {
return a.newRecordsView(ctx, node.GetReference().(*Bucket))
})
a.stopOnErr(err)
case tcell.KeyBacktab:
a.nav.Pop()
default:
return event
}
return nil
})
return tree, nil
}
func (a *Application) getChildren(ctx context.Context, parent *tview.TreeNode) error {
parentBucket := parent.GetReference().(*Bucket)
path := parentBucket.path
handler := parentBucket.nextHandler
buffer, err := LoadBuckets(ctx, a.db, path)
if err != nil {
return err
}
for bucket := range buffer {
repr, next, err := handler(bucket.name, nil)
if errors.Is(err, handlers.ErrFilter) {
continue
}
if err != nil {
return err
}
bucket.nextHandler = next
bucket.result = repr
child := tview.NewTreeNode(repr.Key.String())
child.SetSelectable(true)
child.SetExpanded(false)
child.SetReference(bucket)
parent.AddChild(child)
err = a.getChildren(ctx, child)
if err != nil {
return err
}
}
return nil
}

View file

@ -43,6 +43,7 @@ func load(
go func() {
// TODO how to handle an error
// return two channels, one for result, another for error?
_ = db.View(func(tx *bbolt.Tx) error {
defer close(buffer)
@ -60,23 +61,18 @@ func load(
key, value := cursor.First()
for {
if key == nil {
return nil
}
if filter != nil && !filter(key, value) {
key, value = cursor.Next()
continue
}
select {
case <-ctx.Done():
return nil
default:
// End of iteration
if key == nil {
return nil
}
if filter != nil && !filter(key, value) {
key, value = cursor.Next()
continue
}
if len(buffer) == bufferSize {
continue
}
buffer <- transform(key, value)
case buffer <- transform(key, value):
key, value = cursor.Next()
}
}
@ -97,13 +93,11 @@ func LoadBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Buck
},
func(key, _ []byte) any {
base := make([][]byte, 0, len(path))
for _, name := range path {
base = append(base, name[:])
}
base = append(base, path...)
return &Bucket{
name: key[:],
path: append(base, key[:]),
name: key,
path: append(base, key),
}
},
)
@ -129,14 +123,12 @@ func LoadRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Reco
},
func(key, value []byte) any {
base := make([][]byte, 0, len(path))
for _, name := range path {
base = append(base, name[:])
}
base = append(base, path...)
return &Record{
key: key[:],
value: value[:],
path: append(base, key[:]),
key: key,
value: value,
path: append(base, key),
}
},
)

View file

@ -0,0 +1,28 @@
package tuiutil
import (
"context"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func (a *Application) newDetailedView(_ context.Context, rec *Record) (tview.Primitive, error) {
view := tview.NewTextView()
view.
SetText(rec.result.Val.DetailedString()).
SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyBacktab {
a.nav.Pop()
return nil
}
return event
})
view.
SetBorder(true).
SetTitle(rec.result.Key.String())
return view, nil
}

View file

@ -0,0 +1,49 @@
package tuiutil
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/rivo/tview"
)
type LoadingScreen struct {
*tview.Box
app *tview.Application
count atomic.Int64
}
func NewLoadingScreen(app *tview.Application) (*LoadingScreen, error) {
view := &LoadingScreen{
Box: tview.NewBox(),
app: app,
}
view.
SetBorder(true).
SetTitle(fmt.Sprintf("Loading... %ds", view.count.Load()))
return view, nil
}
func (s *LoadingScreen) Start(ctx context.Context) *LoadingScreen {
go func() {
tick := time.NewTicker(1 * time.Second)
for {
select {
case <-ctx.Done():
return
case <-tick.C:
s.app.QueueUpdateDraw(func() {
s.count.Add(1)
s.SetTitle(fmt.Sprintf("Loading... %ds", s.count.Load()))
})
}
}
}()
return s
}

View file

@ -1,217 +0,0 @@
package tuiutil
import (
"fmt"
"sync/atomic"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type MultipageItem struct {
Text string
Reference any
}
type MultipageView struct {
*tview.Box
app *tview.Application
items []*MultipageItem
nextItemFunc func() *MultipageItem
firstItemIndex int
lastItemIndex int
selectedItemIndex int
mainTextStyle tcell.Style
selectedTextStyle tcell.Style
loading atomic.Bool
setOnLoading func()
unsetOnLoading func()
}
func NewMultipageView(app *tview.Application) *MultipageView {
view := &MultipageView{Box: tview.NewBox(), app: app}
view.mainTextStyle = tcell.StyleDefault.
Foreground(tview.Styles.PrimaryTextColor).
Background(tview.Styles.PrimitiveBackgroundColor)
view.selectedTextStyle = tcell.StyleDefault.
Foreground(tview.Styles.PrimitiveBackgroundColor).
Background(tview.Styles.PrimaryTextColor)
return view
}
func (v *MultipageView) SetNextItemFunc(nextItemFunc func() *MultipageItem) *MultipageView {
v.nextItemFunc = nextItemFunc
return v
}
func (v *MultipageView) OnLoading(setOnLoading, unsetOnLoading func()) *MultipageView {
v.setOnLoading = setOnLoading
v.unsetOnLoading = unsetOnLoading
return v
}
func (v *MultipageView) process() {
_, _, _, itemsPerPage := v.GetInnerRect()
// The terminal's been resized and become shorter
if v.lastItemIndex > v.firstItemIndex+itemsPerPage {
v.lastItemIndex = v.firstItemIndex + itemsPerPage
// If the selected item is invisible, select the last one visible
v.selectedItemIndex = min(v.selectedItemIndex, v.lastItemIndex-1)
return
}
newLastIndex := v.firstItemIndex + itemsPerPage
for v.nextItemFunc != nil && len(v.items) < newLastIndex {
item := v.nextItemFunc()
// if item != nil {
// <-time.After(1000 * time.Millisecond)
// }
if item == nil {
break
}
v.items = append(v.items, item)
}
v.lastItemIndex = min(len(v.items), newLastIndex)
}
func (v *MultipageView) moveToPrevPage() {
// We'are on the first page
if v.firstItemIndex == 0 {
return
}
_, _, _, itemsPerPage := v.GetInnerRect()
v.firstItemIndex -= itemsPerPage
v.lastItemIndex = v.firstItemIndex + itemsPerPage
v.selectFirstItem()
}
func (v *MultipageView) moveToNextPage() {
_, _, _, itemsPerPage := v.GetInnerRect()
// We're on the last page
if v.firstItemIndex+itemsPerPage > v.lastItemIndex {
return
}
v.firstItemIndex += itemsPerPage
v.lastItemIndex += itemsPerPage
v.selectFirstItem()
}
func (v *MultipageView) selectFirstItem() {
v.selectedItemIndex = v.firstItemIndex
}
func (v *MultipageView) selectLastItem() {
v.selectedItemIndex = v.lastItemIndex - 1
}
func (v *MultipageView) draw(screen tcell.Screen) {
x, y, width, height := v.GetInnerRect()
if height == 0 {
v.DrawForSubclass(screen, v)
return
}
pageNum := v.firstItemIndex/height + 1
title := v.GetTitle()
if title != "" {
v.SetTitle(fmt.Sprintf("%s: page %d", title, pageNum))
} else {
v.SetTitle(fmt.Sprintf("page %d", pageNum))
}
v.DrawForSubclass(screen, v)
for index := v.firstItemIndex; index < v.lastItemIndex; index++ {
text := v.items[index].Text
if index == v.selectedItemIndex {
text = fmt.Sprintf("[:white]%s[:black]", text)
}
tview.Print(screen, text, x, y, width, tview.AlignLeft, tcell.ColorWhite)
y++
}
v.SetTitle(title)
}
func (v *MultipageView) Draw(screen tcell.Screen) {
ready := make(chan struct{})
v.loading.Store(true)
go func() {
v.process()
ready <- struct{}{}
}()
select {
case <-ready:
v.draw(screen)
v.loading.Store(false)
case <-time.After(1 * time.Second):
v.DrawForSubclass(screen, v)
go func() {
v.setOnLoading()
<-ready
v.loading.Store(false)
v.unsetOnLoading()
}()
}
}
func (v *MultipageView) InputHandler() func(event *tcell.EventKey, _ func(p tview.Primitive)) {
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) {
// The view is loading, prevent key events from being handled
if v.loading.Load() {
return
}
switch event.Key() {
case tcell.KeyLeft:
v.moveToPrevPage()
case tcell.KeyRight:
v.moveToNextPage()
case tcell.KeyDown:
if v.selectedItemIndex+1 == v.lastItemIndex {
v.moveToNextPage()
// The page's been switched
if v.selectedItemIndex+1 != v.lastItemIndex {
v.selectedItemIndex++
}
} else {
v.selectedItemIndex++
}
case tcell.KeyUp:
v.selectedItemIndex = max(v.selectedItemIndex-1, 0)
if v.selectedItemIndex < v.firstItemIndex {
v.moveToPrevPage()
v.selectLastItem()
}
default:
return
}
})
}
func (v *MultipageView) GetSelectedItem() *MultipageItem {
if v.selectedItemIndex < len(v.items) {
return v.items[v.selectedItemIndex]
}
return nil
}

View file

@ -2,8 +2,8 @@ package tuiutil
import (
"context"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
@ -25,24 +25,59 @@ func (n *Navigation) Push(
ctx context.Context,
init func(context.Context) (tview.Primitive, error),
) error {
ready := make(chan struct{})
ctx, cancel := context.WithCancel(ctx)
page, err := init(ctx)
if err != nil {
cancel()
return err
var page tview.Primitive
var err error
go func() {
page, err = init(ctx)
if err != nil {
cancel()
panic(err)
}
ready <- struct{}{}
}()
select {
case <-ready:
n.cancels = append(n.cancels, cancel)
n.pages = append(n.pages, n.current)
n.current = page
n.app.
SetRoot(n.current, true).
SetFocus(n.current)
go n.app.QueueUpdateDraw(func() {})
case <-time.After(100 * time.Millisecond):
go func() {
loadingScreen, _ := NewLoadingScreen(n.app)
loadingScreen.Start(ctx)
n.app.
SetRoot(loadingScreen, true).
SetFocus(loadingScreen)
go n.app.QueueUpdateDraw(func() {})
<-ready
n.cancels = append(n.cancels, cancel)
n.pages = append(n.pages, n.current)
n.current = page
n.app.
SetRoot(n.current, true).
SetFocus(n.current)
go n.app.QueueUpdateDraw(func() {})
}()
}
n.cancels = append(n.cancels, cancel)
n.pages = append(n.pages, n.current)
n.current = page
n.app.
SetRoot(n.current, true).
SetFocus(n.current)
n.app.QueueEvent(tcell.NewEventInterrupt(nil))
return nil
}
@ -63,5 +98,5 @@ func (n *Navigation) Pop() {
SetRoot(n.current, true).
SetFocus(n.current)
n.app.QueueEvent(tcell.NewEventInterrupt(nil))
go n.app.QueueUpdateDraw(func() {})
}

View file

@ -0,0 +1,249 @@
package tuiutil
import (
"context"
"errors"
"fmt"
"os"
"sync/atomic"
"time"
handlers "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/schema"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func (a *Application) newRecordsView(ctx context.Context, bkt *Bucket) (tview.Primitive, error) {
temp, err := LoadRecords(ctx, a.db, bkt.path)
a.stopOnErr(err)
if err != nil {
return nil, err
}
records := make(chan *Record, 100)
go func() {
defer close(records)
for record := range temp {
result, next, err := bkt.nextHandler(record.key, record.value)
if errors.Is(err, handlers.ErrFilter) {
continue
}
if err != nil {
a.stopOnErr(err)
return
}
record.nextHandler = next
record.result = result
records <- record
}
}()
box := tview.NewBox().
SetBorder(true).
SetTitle(bkt.result.Key.String())
return &RecordsView{
Box: box,
app: a,
buffer: records,
mainTextStyle: tcell.StyleDefault.Foreground(tview.Styles.PrimaryTextColor).Background(tview.Styles.PrimitiveBackgroundColor),
selectedTextStyle: tcell.StyleDefault.Foreground(tview.Styles.PrimitiveBackgroundColor).Background(tview.Styles.PrimaryTextColor),
}, nil
}
type RecordsView struct {
*tview.Box
app *Application
records []*Record
buffer <-chan *Record
firstRecordIndex int
lastRecordIndex int
selectedRecordIndex int
mainTextStyle tcell.Style
selectedTextStyle tcell.Style
isLoading atomic.Bool
}
func (v *RecordsView) process() {
v.isLoading.Store(true)
defer v.isLoading.Store(false)
_, _, _, recordsPerPage := v.GetInnerRect()
// The terminal's been resized and become shorter
if v.lastRecordIndex > v.firstRecordIndex+recordsPerPage {
v.lastRecordIndex = v.firstRecordIndex + recordsPerPage
// If the selected record is invisible, select the last one visible
v.selectedRecordIndex = min(v.selectedRecordIndex, v.lastRecordIndex-1)
return
}
newLastIndex := v.firstRecordIndex + recordsPerPage
for len(v.records) < newLastIndex {
record, ok := <-v.buffer
if !ok {
break
}
fmt.Fprintln(os.Stderr, record.result.Key)
v.records = append(v.records, record)
}
v.lastRecordIndex = min(len(v.records), newLastIndex)
}
func (v *RecordsView) draw(screen tcell.Screen) {
x, y, width, height := v.GetInnerRect()
if height == 0 {
v.DrawForSubclass(screen, v)
return
}
pageNum := v.firstRecordIndex/height + 1
title := v.GetTitle()
if title != "" {
v.SetTitle(fmt.Sprintf("%s: page %d", title, pageNum))
} else {
v.SetTitle(fmt.Sprintf("page %d", pageNum))
}
v.DrawForSubclass(screen, v)
for index := v.firstRecordIndex; index < v.lastRecordIndex; index++ {
result := v.records[index].result
text := fmt.Sprintf("%s | %s", result.Key, result.Val)
if index == v.selectedRecordIndex {
text = fmt.Sprintf("[:white]%s[:black]", text)
}
tview.Print(screen, text, x, y, width, tview.AlignLeft, tcell.ColorWhite)
y++
}
v.SetTitle(title)
}
func (v *RecordsView) Draw(screen tcell.Screen) {
ready := make(chan struct{}, 1)
go func() {
v.process()
ready <- struct{}{}
fmt.Fprintln(os.Stderr, "done")
}()
select {
case <-ready:
v.draw(screen)
return
case <-time.After(100 * time.Millisecond):
loadingScreen, _ := NewLoadingScreen(v.app.app)
loadingScreen.Focus(func(_ tview.Primitive) {})
x, y, w, h := v.GetRect()
loadingScreen.SetRect(x, y, w, h)
loadingScreen.Draw(screen)
go func() {
_ = v.app.nav.Push(context.Background(), func(ctx context.Context) (tview.Primitive, error) {
return loadingScreen.Start(ctx), nil
})
fmt.Fprintln(os.Stderr, "pushed")
<-ready
v.app.nav.Pop()
fmt.Fprintln(os.Stderr, "pop")
}()
}
}
func (v *RecordsView) moveToPrevPage() {
// We'are on the first page
if v.firstRecordIndex == 0 {
return
}
_, _, _, recordsPerPage := v.GetInnerRect()
v.firstRecordIndex -= recordsPerPage
v.lastRecordIndex = v.firstRecordIndex + recordsPerPage
v.selectFirstRecord()
}
func (v *RecordsView) moveToNextPage() {
_, _, _, recordsPerPage := v.GetInnerRect()
// We're on the last page
if v.firstRecordIndex+recordsPerPage > v.lastRecordIndex {
return
}
v.firstRecordIndex += recordsPerPage
v.lastRecordIndex += recordsPerPage
v.selectFirstRecord()
}
func (v *RecordsView) selectFirstRecord() {
v.selectedRecordIndex = v.firstRecordIndex
}
func (v *RecordsView) selectLastRecord() {
v.selectedRecordIndex = v.lastRecordIndex - 1
}
func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.Primitive)) {
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) {
// The view is loading, prevent key events from being handled
if v.isLoading.Load() {
return
}
switch event.Key() {
case tcell.KeyLeft:
v.moveToPrevPage()
case tcell.KeyRight:
v.moveToNextPage()
case tcell.KeyDown:
if v.selectedRecordIndex+1 == v.lastRecordIndex {
v.moveToNextPage()
// The page's been switched
if v.selectedRecordIndex+1 != v.lastRecordIndex {
v.selectedRecordIndex++
}
} else {
v.selectedRecordIndex++
}
case tcell.KeyUp:
v.selectedRecordIndex = max(v.selectedRecordIndex-1, 0)
if v.selectedRecordIndex < v.firstRecordIndex {
v.moveToPrevPage()
v.selectLastRecord()
}
case tcell.KeyTab:
record := v.getSelectedItem()
if record == nil {
return
}
err := v.app.nav.Push(context.Background(), func(ctx context.Context) (tview.Primitive, error) {
return v.app.newDetailedView(ctx, record)
})
v.app.stopOnErr(err)
case tcell.KeyBacktab:
v.app.nav.Pop()
default:
return
}
})
}
func (v *RecordsView) getSelectedItem() *Record {
if v.selectedRecordIndex < len(v.records) {
return v.records[v.selectedRecordIndex]
}
return nil
}

View file

@ -24,15 +24,21 @@ func NewHandler(
next Handler,
) Handler {
return func(key, value []byte) (repr *KV, _ Handler, err error) {
// ? check rest bytes
keyRepr, _, err := keyParser(key)
if err != nil {
return nil, nil, fmt.Errorf("can't parse key: %w", err)
var keyRepr parse.Result = parse.NewRawResult("")
var valueRepr parse.Result = parse.NewRawResult("")
if keyParser != nil {
keyRepr, _, err = keyParser(key)
if err != nil {
return nil, nil, fmt.Errorf("can't parse key: %w", err)
}
}
valueRepr, _, err := valParser(value)
if err != nil {
return nil, nil, fmt.Errorf("can't parse value: %w", err)
if valParser != nil {
valueRepr, _, err = valParser(value)
if err != nil {
return nil, nil, fmt.Errorf("can't parse value: %w", err)
}
}
return &KV{keyRepr, valueRepr}, next, nil
@ -123,13 +129,11 @@ func GetHandler(filters ...Filter) Handler {
GetECInfoBucketHandler(cfg),
)
handler = FilterFailHandler(handler)
// if len(filters) > 0 {
// handler = FilterFailHandler(handler)
// } else {
// handler = WithFallback(handler, RawHandler)
// }
if len(filters) > 0 {
handler = FilterFailHandler(handler)
} else {
handler = WithFallback(handler, RawHandler)
}
return handler
}
@ -254,13 +258,15 @@ func GetShardInfoHandler(_ *HandlerConfig) Handler {
parse.Collect(parse.Chain(
parse.Map(
parse.Any(
parse.ExactString("version"),
parse.ExactString("id"),
parse.ExactString("version"),
parse.ExactString("logic_counter"),
parse.ExactString("phy_counter"),
parse.ExactString("user_counter"),
),
func(x string) parse.Result { return parse.NewRawResult(x) },
func(x string) parse.Result {
return parse.NewRawResult(x, func(s string) string { return fmt.Sprintf("%13s", s) })
},
),
parse.Finalize(parse.EOF),
)),

View file

@ -34,16 +34,16 @@ func Any[T any](parsers ...Parser[T]) Parser[T] {
func Chain[T any](parsers ...Parser[T]) Parser[[]T] {
return func(data []byte) (_ []T, rest []byte, err error) {
var results []T
var result T
results := make([]T, len(parsers))
rest = data
for _, parser := range parsers {
for index, parser := range parsers {
result, rest, err = parser(rest)
if err != nil {
return nil, data, err
}
results = append(results, result)
results[index] = result
}
return results, rest, nil
}
@ -98,7 +98,7 @@ func Collect(parser Parser[[]Result]) Parser[Result] {
if err != nil {
return nil, data, err
}
return NewResultArray(results...), rest, nil
return &ResultArray{results: results}, rest, nil
}
}

View file

@ -45,11 +45,12 @@ func NewResultArray(results ...Result) *ResultArray {
}
func (r *ResultArray) String() string {
var acc []string
b := strings.Builder{}
for _, result := range r.results {
acc = append(acc, result.String())
_, _ = b.WriteString(result.String())
_, _ = b.WriteString(" ")
}
return strings.TrimRight(strings.Join(acc, " "), " ")
return b.String()
}
func (r *ResultArray) DetailedString() string {
@ -61,17 +62,26 @@ func (r *ResultArray) DetailedString() string {
}
type RawResult struct {
s string
s string
formatters []func(string) string
}
func NewRawResult(s string) *RawResult {
return &RawResult{s}
func NewRawResult(s string, formatters ...func(string) string) *RawResult {
return &RawResult{s: s, formatters: formatters}
}
func (r *RawResult) String() string {
return r.s
s := r.s
for _, f := range r.formatters {
s = f(s)
}
return s
}
func (r *RawResult) DetailedString() string {
return r.s + "\n"
s := r.s
for _, f := range r.formatters {
s = f(s)
}
return s + "\n"
}