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) {}) } }) }