frostfs-node/cmd/frostfs-lens/internal/tui/buckets.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

257 lines
5.3 KiB
Go

package tui
import (
"context"
"sync"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type BucketsView struct {
*tview.Box
mu sync.Mutex
view *tview.TreeView
nodeToUpdate *tview.TreeNode
ui *UI
filter *Filter
}
type bucketNode struct {
bucket *Bucket
filter *Filter
}
func NewBucketsView(ui *UI, filter *Filter) *BucketsView {
return &BucketsView{
Box: tview.NewBox(),
view: tview.NewTreeView(),
ui: ui,
filter: filter,
}
}
func (v *BucketsView) Mount(_ context.Context) error {
root := tview.NewTreeNode(".")
root.SetExpanded(false)
root.SetSelectable(false)
root.SetReference(&bucketNode{
bucket: &Bucket{NextParser: v.ui.rootParser},
filter: v.filter,
})
v.nodeToUpdate = root
v.view.SetRoot(root)
v.view.SetCurrentNode(root)
return nil
}
func (v *BucketsView) Update(ctx context.Context) error {
if v.nodeToUpdate == nil {
return nil
}
defer func() { v.nodeToUpdate = nil }()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ready := make(chan struct{})
errCh := make(chan error)
tmp := tview.NewTreeNode(v.nodeToUpdate.GetText())
tmp.SetReference(v.nodeToUpdate.GetReference())
node := v.nodeToUpdate.GetReference().(*bucketNode)
go func() {
defer close(ready)
hasBuckets, err := HasBuckets(ctx, v.ui.db, node.bucket.Path)
if err != nil {
errCh <- err
}
// Show the selected bucket's records instead.
if !hasBuckets && node.bucket.NextParser != nil {
v.ui.moveNextPage(NewRecordsView(v.ui, node.bucket, node.filter))
}
if v.nodeToUpdate.IsExpanded() {
return
}
err = v.loadNodeChildren(ctx, tmp, node.filter)
if err != nil {
errCh <- err
}
}()
select {
case <-ctx.Done():
case <-ready:
v.mu.Lock()
v.nodeToUpdate.SetChildren(tmp.GetChildren())
v.nodeToUpdate.SetExpanded(!v.nodeToUpdate.IsExpanded())
v.mu.Unlock()
case err := <-errCh:
return err
}
return nil
}
func (v *BucketsView) Unmount() {
}
func (v *BucketsView) Draw(screen tcell.Screen) {
x, y, width, height := v.GetInnerRect()
v.view.SetRect(x, y, width, height)
v.view.Draw(screen)
}
func (v *BucketsView) loadNodeChildren(
ctx context.Context, node *tview.TreeNode, filter *Filter,
) error {
parentBucket := node.GetReference().(*bucketNode).bucket
path := parentBucket.Path
parser := parentBucket.NextParser
buffer, err := LoadBuckets(ctx, v.ui.db, path, v.ui.loadBufferSize)
if err != nil {
return err
}
for item := range buffer {
if item.err != nil {
return item.err
}
bucket := item.val
bucket.Entry, bucket.NextParser, err = parser(bucket.Name, nil)
if err != nil {
return err
}
satisfies, err := v.bucketSatisfiesFilter(ctx, bucket, filter)
if err != nil {
return err
}
if !satisfies {
continue
}
child := tview.NewTreeNode(bucket.Entry.String()).
SetSelectable(true).
SetExpanded(false).
SetReference(&bucketNode{
bucket: bucket,
filter: filter.Apply(bucket.Entry),
})
node.AddChild(child)
}
return nil
}
func (v *BucketsView) bucketSatisfiesFilter(
ctx context.Context, bucket *Bucket, filter *Filter,
) (bool, error) {
// Does the current bucket satisfies the filter?
filter = filter.Apply(bucket.Entry)
if filter.Result() == common.Yes {
return true, nil
}
if filter.Result() == common.No {
return false, nil
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Check the current bucket's nested buckets if exist
bucketsBuffer, err := LoadBuckets(ctx, v.ui.db, bucket.Path, v.ui.loadBufferSize)
if err != nil {
return false, err
}
for item := range bucketsBuffer {
if item.err != nil {
return false, item.err
}
b := item.val
b.Entry, b.NextParser, err = bucket.NextParser(b.Name, nil)
if err != nil {
return false, err
}
satisfies, err := v.bucketSatisfiesFilter(ctx, b, filter)
if err != nil {
return false, err
}
if satisfies {
return true, nil
}
}
// Check the current bucket's nested records if exist
recordsBuffer, err := LoadRecords(ctx, v.ui.db, bucket.Path, v.ui.loadBufferSize)
if err != nil {
return false, err
}
for item := range recordsBuffer {
if item.err != nil {
return false, item.err
}
r := item.val
r.Entry, _, err = bucket.NextParser(r.Key, r.Value)
if err != nil {
return false, err
}
if filter.Apply(r.Entry).Result() == common.Yes {
return true, nil
}
}
return false, nil
}
func (v *BucketsView) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
currentNode := v.view.GetCurrentNode()
if currentNode == nil {
return
}
switch event.Key() {
case tcell.KeyEnter:
// Expand or collapse the selected bucket's nested buckets,
// otherwise, navigate to that bucket's records.
v.nodeToUpdate = currentNode
case tcell.KeyCtrlR:
// Navigate to the selected bucket's records.
bucketNode := currentNode.GetReference().(*bucketNode)
v.ui.moveNextPage(NewRecordsView(v.ui, bucketNode.bucket, bucketNode.filter))
case tcell.KeyCtrlD:
// Navigate to the selected bucket's detailed view.
bucketNode := currentNode.GetReference().(*bucketNode)
v.ui.moveNextPage(NewDetailedView(bucketNode.bucket.Entry.DetailedString()))
default:
v.view.InputHandler()(event, func(tview.Primitive) {})
}
})
}