[#1223] lens/tui: Refactor UI
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
parent
73b466db3b
commit
79563454c9
9 changed files with 250 additions and 287 deletions
|
@ -5,24 +5,23 @@ import (
|
|||
"errors"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type BucketsView struct {
|
||||
*tview.Box
|
||||
|
||||
view *tview.TreeView
|
||||
ui *UI
|
||||
needExpand bool
|
||||
needUpdate bool
|
||||
onUnmount func()
|
||||
|
||||
ui *UI
|
||||
filter *Filter
|
||||
}
|
||||
|
||||
type BucketNode struct {
|
||||
type bucketNode struct {
|
||||
bucket *Bucket
|
||||
parent *tview.TreeNode
|
||||
filter *Filter
|
||||
}
|
||||
|
||||
|
@ -42,19 +41,16 @@ func (v *BucketsView) Mount(ctx context.Context) error {
|
|||
|
||||
ctx, v.onUnmount = context.WithCancel(ctx)
|
||||
|
||||
handler := metabase.MetabaseParser
|
||||
|
||||
root := tview.NewTreeNode(".")
|
||||
root.
|
||||
SetSelectable(false).
|
||||
SetExpanded(true).
|
||||
SetReference(&BucketNode{
|
||||
bucket: &Bucket{NextHandler: handler},
|
||||
parent: nil,
|
||||
SetReference(&bucketNode{
|
||||
bucket: &Bucket{NextParser: v.ui.rootParser},
|
||||
filter: v.filter,
|
||||
})
|
||||
|
||||
if err := v.getChildrenFilter(ctx, root, v.filter); err != nil {
|
||||
if err := v.loadNodeChildren(ctx, root, v.filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -65,88 +61,45 @@ func (v *BucketsView) Mount(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (v *BucketsView) satisfies(
|
||||
ctx context.Context,
|
||||
bucket *Bucket,
|
||||
filter *Filter,
|
||||
) (bool, error) {
|
||||
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()
|
||||
|
||||
subbuckets, err := LoadBuckets(ctx, v.ui.db, bucket.Path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for b := range subbuckets {
|
||||
b.Entry, b.NextHandler, err = bucket.NextHandler(b.Name, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
flag, err := v.satisfies(ctx, b, filter.Copy())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if flag {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
records, err := LoadRecords(ctx, v.ui.db, bucket.Path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for r := range records {
|
||||
r.Entry, _, err = bucket.NextHandler(r.Key, r.Value)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
f := filter.Copy()
|
||||
f.Apply(r.Entry)
|
||||
if f.Result() == common.Yes {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (v *BucketsView) Update(ctx context.Context) error {
|
||||
if !v.needExpand {
|
||||
if !v.needUpdate {
|
||||
return nil
|
||||
}
|
||||
defer func() { v.needUpdate = false }()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ready := make(chan struct{})
|
||||
errCh := make(chan error)
|
||||
|
||||
current := v.view.GetCurrentNode()
|
||||
tmp := tview.NewTreeNode(current.GetText())
|
||||
tmp.SetReference(current.GetReference())
|
||||
tmp := tview.NewTreeNode(current.GetText()).
|
||||
SetReference(current.GetReference()).
|
||||
SetExpanded(current.IsExpanded())
|
||||
|
||||
node := current.GetReference().(*BucketNode)
|
||||
node := current.GetReference().(*bucketNode)
|
||||
|
||||
go func() {
|
||||
defer close(ready)
|
||||
v.needExpand = false
|
||||
|
||||
if !current.IsExpanded() {
|
||||
_ = v.getChildrenFilter(ctx, tmp, node.filter)
|
||||
hasBuckets, err := HasBuckets(ctx, v.ui.db, node.bucket.Path)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
|
||||
// Show the selected bucket's records instead.
|
||||
if !hasBuckets {
|
||||
v.ui.moveNextPage(NewRecordsView(v.ui, node.bucket, node.filter))
|
||||
}
|
||||
|
||||
if current.IsExpanded() {
|
||||
return
|
||||
}
|
||||
|
||||
err = v.loadNodeChildren(ctx, tmp, node.filter)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -155,6 +108,8 @@ func (v *BucketsView) Update(ctx context.Context) error {
|
|||
case <-ready:
|
||||
current.SetChildren(tmp.GetChildren())
|
||||
current.SetExpanded(!current.IsExpanded())
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -177,86 +132,135 @@ func (v *BucketsView) Draw(screen tcell.Screen) {
|
|||
v.view.Draw(screen)
|
||||
}
|
||||
|
||||
func (v *BucketsView) getChildrenFilter(
|
||||
ctx context.Context,
|
||||
parent *tview.TreeNode,
|
||||
predicate *Filter,
|
||||
func (v *BucketsView) loadNodeChildren(
|
||||
ctx context.Context, node *tview.TreeNode, filter *Filter,
|
||||
) error {
|
||||
parentBucket := parent.GetReference().(*BucketNode).bucket
|
||||
parentBucket := node.GetReference().(*bucketNode).bucket
|
||||
|
||||
path := parentBucket.Path
|
||||
handler := parentBucket.NextHandler
|
||||
parser := parentBucket.NextParser
|
||||
|
||||
buffer, err := LoadBuckets(ctx, v.ui.db, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for bucket := range buffer {
|
||||
bucket.Entry, bucket.NextHandler, err = handler(bucket.Name, nil)
|
||||
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
|
||||
}
|
||||
|
||||
child := tview.NewTreeNode(bucket.Entry.String())
|
||||
|
||||
f := predicate.Copy()
|
||||
f.Apply(bucket.Entry)
|
||||
|
||||
child.SetSelectable(true)
|
||||
child.SetExpanded(false)
|
||||
child.SetReference(&BucketNode{
|
||||
bucket: bucket,
|
||||
parent: parent,
|
||||
filter: f,
|
||||
})
|
||||
|
||||
flag, err := v.satisfies(ctx, bucket, predicate.Copy())
|
||||
satisfies, err := v.bucketSatisfiesFilter(ctx, bucket, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if flag {
|
||||
parent.AddChild(child)
|
||||
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)
|
||||
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 recordsBuffer if exist
|
||||
recordsBuffer, err := LoadRecords(ctx, v.ui.db, bucket.Path)
|
||||
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)) {
|
||||
current := v.view.GetCurrentNode()
|
||||
|
||||
switch m, k, r := event.Modifiers(), event.Key(), event.Rune(); {
|
||||
// TODO go to records on Enter pressed
|
||||
// case m == 0 && k == tcell.KeyRune && r == ' ':
|
||||
// v.needExpand = true
|
||||
case k == tcell.KeyEnter:
|
||||
if current == nil {
|
||||
return
|
||||
}
|
||||
node := current.GetReference().(*BucketNode)
|
||||
|
||||
hasBuckets, _ := HasBuckets(context.Background(), v.ui.db, node.bucket.Path)
|
||||
|
||||
if hasBuckets {
|
||||
v.needExpand = true
|
||||
return
|
||||
}
|
||||
v.ui.moveNextPage(NewRecordsView(v.ui, node.bucket, node.filter.Copy()))
|
||||
// TODO: go to parent while iterating over its children
|
||||
// case m == 0 && k == tcell.KeyLeft:
|
||||
// parent := current.GetReference().(*BucketNode).parent
|
||||
// fmt.Fprintln(os.Stderr, current.GetText(), parent.GetText())
|
||||
// if parent != nil {
|
||||
// parent.SetExpanded(false)
|
||||
// v.view.SetCurrentNode(parent)
|
||||
// }
|
||||
case m == 0 && k == tcell.KeyRune && r == 'd':
|
||||
current := v.view.GetCurrentNode().GetReference().(*BucketNode).bucket
|
||||
if current != nil {
|
||||
v.ui.moveNextPage(NewDetailedView(current.Entry.DetailedString()))
|
||||
}
|
||||
switch event.Key() {
|
||||
case tcell.KeyEnter:
|
||||
// Expand or collapse the selected bucket's nested buckets,
|
||||
// otherwise, navigate to that bucket's records.
|
||||
v.needUpdate = true
|
||||
case tcell.KeyCtrlR:
|
||||
// Navigate to the selected bucket's records.
|
||||
node := v.view.GetCurrentNode().GetReference().(*bucketNode)
|
||||
v.ui.moveNextPage(NewRecordsView(v.ui, node.bucket, node.filter))
|
||||
case tcell.KeyCtrlD:
|
||||
// Navigate to the selected bucket's detailed view.
|
||||
node := v.view.GetCurrentNode().GetReference().(*bucketNode)
|
||||
v.ui.moveNextPage(NewDetailedView(node.bucket.Entry.DetailedString()))
|
||||
default:
|
||||
v.view.InputHandler()(event, func(tview.Primitive) {})
|
||||
}
|
||||
|
|
|
@ -8,11 +8,12 @@ import (
|
|||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
bufferSize = 100
|
||||
)
|
||||
type Item[T any] struct {
|
||||
val T
|
||||
err error
|
||||
}
|
||||
|
||||
func follow(tx *bbolt.Tx, path [][]byte) (*bbolt.Bucket, error) {
|
||||
func resolvePath(tx *bbolt.Tx, path [][]byte) (*bbolt.Bucket, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, errors.New("can't find bucket without path")
|
||||
}
|
||||
|
@ -31,32 +32,27 @@ func follow(tx *bbolt.Tx, path [][]byte) (*bbolt.Bucket, error) {
|
|||
return bucket, nil
|
||||
}
|
||||
|
||||
func load(
|
||||
ctx context.Context,
|
||||
db *bbolt.DB,
|
||||
path [][]byte,
|
||||
filter func(key, value []byte) bool,
|
||||
transform func(key, value []byte) any,
|
||||
) (<-chan any, error) {
|
||||
buffer := make(chan any, bufferSize)
|
||||
errCh := make(chan error)
|
||||
func load[T any](
|
||||
ctx context.Context, db *bbolt.DB, path [][]byte, bufferSize int,
|
||||
filter func(key, value []byte) bool, transform func(key, value []byte) T,
|
||||
) (<-chan Item[T], error) {
|
||||
buffer := make(chan Item[T], bufferSize)
|
||||
|
||||
go func() {
|
||||
// TODO enhance error handling.
|
||||
_ = db.View(func(tx *bbolt.Tx) error {
|
||||
defer close(buffer)
|
||||
defer close(buffer)
|
||||
|
||||
err := db.View(func(tx *bbolt.Tx) error {
|
||||
var cursor *bbolt.Cursor
|
||||
if len(path) == 0 {
|
||||
cursor = tx.Cursor()
|
||||
} else {
|
||||
bucket, err := follow(tx, path)
|
||||
bucket, err := resolvePath(tx, path)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("can't find bucket: %w", err)
|
||||
buffer <- Item[T]{err: fmt.Errorf("can't find bucket: %w", err)}
|
||||
return nil
|
||||
}
|
||||
cursor = bucket.Cursor()
|
||||
}
|
||||
close(errCh)
|
||||
|
||||
key, value := cursor.First()
|
||||
for {
|
||||
|
@ -71,26 +67,26 @@ func load(
|
|||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case buffer <- transform(key, value):
|
||||
case buffer <- Item[T]{val: transform(key, value)}:
|
||||
key, value = cursor.Next()
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
buffer <- Item[T]{err: err}
|
||||
}
|
||||
}()
|
||||
if err := <-errCh; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func LoadBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Bucket, error) {
|
||||
in, err := load(
|
||||
ctx, db, path,
|
||||
func LoadBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan Item[*Bucket], error) {
|
||||
buffer, err := load(
|
||||
ctx, db, path, 100,
|
||||
func(_, value []byte) bool {
|
||||
return value == nil
|
||||
},
|
||||
func(key, _ []byte) any {
|
||||
func(key, _ []byte) *Bucket {
|
||||
base := make([][]byte, 0, len(path))
|
||||
base = append(base, path...)
|
||||
|
||||
|
@ -104,23 +100,16 @@ func LoadBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Buck
|
|||
return nil, fmt.Errorf("can't start iterating bucket: %w", err)
|
||||
}
|
||||
|
||||
out := make(chan *Bucket, bufferSize)
|
||||
go func() {
|
||||
defer close(out)
|
||||
for x := range in {
|
||||
out <- x.(*Bucket)
|
||||
}
|
||||
}()
|
||||
return out, nil
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
func LoadRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Record, error) {
|
||||
in, err := load(
|
||||
ctx, db, path,
|
||||
func LoadRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan Item[*Record], error) {
|
||||
buffer, err := load(
|
||||
ctx, db, path, 100,
|
||||
func(_, value []byte) bool {
|
||||
return value != nil
|
||||
},
|
||||
func(key, value []byte) any {
|
||||
func(key, value []byte) *Record {
|
||||
base := make([][]byte, 0, len(path))
|
||||
base = append(base, path...)
|
||||
|
||||
|
@ -135,35 +124,33 @@ func LoadRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Reco
|
|||
return nil, fmt.Errorf("can't start iterating bucket: %w", err)
|
||||
}
|
||||
|
||||
out := make(chan *Record, bufferSize)
|
||||
go func() {
|
||||
defer close(out)
|
||||
for x := range in {
|
||||
out <- x.(*Record)
|
||||
}
|
||||
}()
|
||||
return out, nil
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
// HasBuckets checks if a bucket has nested buckets. It relies on assumption
|
||||
// that a bucket can have either nested buckets or records but not both.
|
||||
func HasBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (bool, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
buffer, err := load(
|
||||
ctx, db, path,
|
||||
ctx, db, path, 1,
|
||||
nil,
|
||||
func(_, value []byte) any {
|
||||
return value
|
||||
},
|
||||
func(_, value []byte) []byte { return value },
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
x, ok := <-buffer
|
||||
if !ok || x.([]byte) != nil {
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if x.err != nil {
|
||||
return false, err
|
||||
}
|
||||
if x.val != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -3,43 +3,22 @@ package tuiutil
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type DetailedView struct {
|
||||
*tview.Box
|
||||
view *tview.TextView
|
||||
*tview.TextView
|
||||
}
|
||||
|
||||
func NewDetailedView(detailed string) *DetailedView {
|
||||
v := &DetailedView{
|
||||
Box: tview.NewBox(),
|
||||
view: tview.NewTextView(),
|
||||
TextView: tview.NewTextView(),
|
||||
}
|
||||
v.view.SetText(detailed)
|
||||
|
||||
v.SetDynamicColors(true)
|
||||
v.SetText(detailed)
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *DetailedView) Mount(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DetailedView) Update(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DetailedView) Unmount() {}
|
||||
|
||||
func (v *DetailedView) Draw(screen tcell.Screen) {
|
||||
x, y, width, height := v.GetInnerRect()
|
||||
v.view.SetRect(x, y, width, height)
|
||||
v.view.Draw(screen)
|
||||
}
|
||||
|
||||
func (v *DetailedView) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
|
||||
v.view.InputHandler()(event, func(tview.Primitive) {})
|
||||
})
|
||||
}
|
||||
func (v *DetailedView) Mount(_ context.Context) error { return nil }
|
||||
func (v *DetailedView) Update(_ context.Context) error { return nil }
|
||||
func (v *DetailedView) Unmount() {}
|
||||
|
|
|
@ -13,21 +13,26 @@ type Filter struct {
|
|||
|
||||
func NewFilter(values map[string]any) *Filter {
|
||||
f := &Filter{
|
||||
values: values,
|
||||
values: maps.Clone(values),
|
||||
results: make(map[string]common.FilterResult),
|
||||
}
|
||||
for id := range values {
|
||||
var zero common.FilterResult
|
||||
f.results[id] = zero
|
||||
for tag := range values {
|
||||
f.results[tag] = common.No
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *Filter) Apply(e common.SchemaEntry) {
|
||||
// TODO: make apply return new Filter.
|
||||
for id, value := range f.values {
|
||||
f.results[id] = max(f.results[id], e.Filter(id, value))
|
||||
func (f *Filter) Apply(e common.SchemaEntry) *Filter {
|
||||
filter := &Filter{
|
||||
values: f.values,
|
||||
results: maps.Clone(f.results),
|
||||
}
|
||||
|
||||
for tag, value := range filter.values {
|
||||
filter.results[tag] = max(filter.results[tag], e.Filter(tag, value))
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
func (f *Filter) Result() common.FilterResult {
|
||||
|
@ -37,11 +42,3 @@ func (f *Filter) Result() common.FilterResult {
|
|||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func (f *Filter) Copy() *Filter {
|
||||
c := &Filter{
|
||||
values: maps.Clone(f.values),
|
||||
results: maps.Clone(f.results),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -23,21 +23,20 @@ func NewInputFieldWithHistory(historyLimit int) *InputFieldWithHistory {
|
|||
}
|
||||
}
|
||||
|
||||
func (f *InputFieldWithHistory) AddToHistory(s string) *InputFieldWithHistory {
|
||||
func (f *InputFieldWithHistory) AddToHistory(s string) {
|
||||
// Stop scrolling history on history change, need to start scrolling again.
|
||||
defer func() { f.historyPointer = len(f.history) }()
|
||||
|
||||
// Used history for search prompt
|
||||
// Used history data for search prompt, so just make that data recent.
|
||||
if f.historyPointer != len(f.history) && s == f.history[f.historyPointer] {
|
||||
f.history = append(f.history[:f.historyPointer], f.history[f.historyPointer+1:]...)
|
||||
f.history = append(f.history, s)
|
||||
return f
|
||||
}
|
||||
|
||||
if len(f.history) == f.historyLimit {
|
||||
f.history = f.history[1:]
|
||||
}
|
||||
f.history = append(f.history, s)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *InputFieldWithHistory) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
|
@ -49,7 +48,7 @@ func (f *InputFieldWithHistory) InputHandler() func(event *tcell.EventKey, setFo
|
|||
if len(f.history) == 0 {
|
||||
return
|
||||
}
|
||||
// Must start iterating before
|
||||
// Need to start iterating before.
|
||||
if f.historyPointer == len(f.history) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -57,17 +57,14 @@ func (b *LoadingBar) Stop() {
|
|||
func (b *LoadingBar) Draw(screen tcell.Screen) {
|
||||
seconds := b.secondsElapsed.Load()
|
||||
|
||||
var text string
|
||||
if seconds < 60 {
|
||||
text = fmt.Sprintf(
|
||||
" Loading... %ds (press Escape to cancel) ", seconds,
|
||||
)
|
||||
} else {
|
||||
text = fmt.Sprintf(
|
||||
" Loading... %dm%ds (press Escape to cancel) ", seconds/60, seconds%60,
|
||||
)
|
||||
var time string
|
||||
switch {
|
||||
case seconds < 60:
|
||||
time = fmt.Sprintf("%ds", seconds)
|
||||
default:
|
||||
time = fmt.Sprintf("%dm%ds", seconds/60, seconds%60)
|
||||
}
|
||||
b.view.SetText(text)
|
||||
b.view.SetText(fmt.Sprintf(" Loading... %s (press Escape to cancel) ", time))
|
||||
|
||||
x, y, width, _ := b.GetInnerRect()
|
||||
b.view.SetRect(x, y, width, 1)
|
||||
|
|
|
@ -52,15 +52,20 @@ func (v *RecordsView) Mount(ctx context.Context) error {
|
|||
go func() {
|
||||
defer close(v.buffer)
|
||||
|
||||
for record := range tempBuffer {
|
||||
record.Entry, _, err = v.bucket.NextHandler(record.Key, record.Value)
|
||||
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
|
||||
}
|
||||
|
||||
f := v.filter.Copy()
|
||||
f.Apply(record.Entry)
|
||||
if f.Result() != common.Yes {
|
||||
if v.filter.Apply(record.Entry).Result() != common.Yes {
|
||||
continue
|
||||
}
|
||||
v.buffer <- record
|
||||
|
@ -113,23 +118,12 @@ func (v *RecordsView) Draw(screen tcell.Screen) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO: show page number.
|
||||
// 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)
|
||||
|
||||
// No records in that bucket.
|
||||
if v.firstRecordIndex == v.lastRecordIndex {
|
||||
tview.Print(
|
||||
screen, "Empty Bucket", x, y+1, width,
|
||||
tview.AlignCenter, tview.Styles.PrimaryTextColor,
|
||||
screen, "Empty Bucket", x, y, width, tview.AlignCenter, tview.Styles.PrimaryTextColor,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -137,6 +131,7 @@ func (v *RecordsView) Draw(screen tcell.Screen) {
|
|||
for index := v.firstRecordIndex; index < v.lastRecordIndex; index++ {
|
||||
result := v.records[index].Entry
|
||||
text := result.String()
|
||||
|
||||
if index == v.selectedRecordIndex {
|
||||
text = fmt.Sprintf("[:white]%s[:black]", text)
|
||||
tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimitiveBackgroundColor)
|
||||
|
@ -146,7 +141,6 @@ func (v *RecordsView) Draw(screen tcell.Screen) {
|
|||
|
||||
y++
|
||||
}
|
||||
// v.SetTitle(title)
|
||||
}
|
||||
|
||||
func (v *RecordsView) moveToPrevPage() {
|
||||
|
@ -184,12 +178,19 @@ func (v *RecordsView) selectLastRecord() {
|
|||
v.selectedRecordIndex = v.lastRecordIndex - 1
|
||||
}
|
||||
|
||||
func (v *RecordsView) getSelectedItem() *Record {
|
||||
if v.selectedRecordIndex < len(v.records) {
|
||||
return v.records[v.selectedRecordIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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.KeyLeft || k == tcell.KeyPgUp):
|
||||
case m == 0 && k == tcell.KeyPgUp:
|
||||
v.moveToPrevPage()
|
||||
case m == 0 && (k == tcell.KeyRight || k == tcell.KeyPgDn):
|
||||
case m == 0 && k == tcell.KeyPgDn:
|
||||
v.moveToNextPage()
|
||||
case m == 0 && k == tcell.KeyDown:
|
||||
// Need to move onto the next page.
|
||||
|
@ -208,7 +209,7 @@ func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.
|
|||
v.moveToPrevPage()
|
||||
v.selectLastRecord()
|
||||
}
|
||||
case k == tcell.KeyEnter || m&tcell.ModCtrl != 0 && k == tcell.KeyRight:
|
||||
case k == tcell.KeyEnter:
|
||||
current := v.getSelectedItem()
|
||||
if current != nil {
|
||||
v.ui.moveNextPage(NewDetailedView(current.Entry.DetailedString()))
|
||||
|
@ -216,10 +217,3 @@ func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (v *RecordsView) getSelectedItem() *Record {
|
||||
if v.selectedRecordIndex < len(v.records) {
|
||||
return v.records[v.selectedRecordIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,19 +5,14 @@ import (
|
|||
)
|
||||
|
||||
type Bucket struct {
|
||||
Name []byte
|
||||
Path [][]byte
|
||||
|
||||
HasBuckets,
|
||||
HasRecords bool
|
||||
|
||||
NextHandler common.Parser
|
||||
Entry common.SchemaEntry
|
||||
Name []byte
|
||||
Path [][]byte
|
||||
Entry common.SchemaEntry
|
||||
NextParser common.Parser
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Key []byte
|
||||
Value []byte
|
||||
Path [][]byte
|
||||
Entry common.SchemaEntry
|
||||
Key, Value []byte
|
||||
Path [][]byte
|
||||
Entry common.SchemaEntry
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
|
@ -61,6 +63,8 @@ type UI struct {
|
|||
|
||||
loadBufferSize int
|
||||
|
||||
rootParser common.Parser
|
||||
|
||||
loadingBarDelay time.Duration
|
||||
|
||||
cancelLoading func()
|
||||
|
@ -78,6 +82,7 @@ func NewUI(ctx context.Context, app *tview.Application, db *bbolt.DB) *UI {
|
|||
Box: tview.NewBox(),
|
||||
app: app,
|
||||
db: db,
|
||||
rootParser: metabase.MetabaseParser,
|
||||
isFirstMount: true,
|
||||
loadBufferSize: 100,
|
||||
infoBar: tview.NewTextView(),
|
||||
|
@ -89,7 +94,7 @@ func NewUI(ctx context.Context, app *tview.Application, db *bbolt.DB) *UI {
|
|||
filters: make(map[string]func(string) (any, error)),
|
||||
compositeFilters: make(map[string]func(string) (map[string]any, error)),
|
||||
saveMounted: true,
|
||||
loadingBarDelay: 100 * time.Millisecond,
|
||||
loadingBarDelay: 1 * time.Second,
|
||||
}
|
||||
|
||||
barBackgroundColor := tview.Styles.PrimaryTextColor
|
||||
|
@ -305,6 +310,9 @@ func (ui *UI) mountAndUpdate(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
x, y, width, height := ui.GetInnerRect()
|
||||
ui.pageToMount.SetRect(x, y, width, height)
|
||||
|
||||
s = loadOp(ctx, ui.pageToMount.Update)
|
||||
if s.err != nil {
|
||||
ui.stopOnError(s.err)
|
||||
|
@ -323,6 +331,9 @@ func (ui *UI) mountAndUpdate(ctx context.Context) {
|
|||
}
|
||||
|
||||
func (ui *UI) update(ctx context.Context) {
|
||||
x, y, width, height := ui.GetInnerRect()
|
||||
ui.mountedPage.SetRect(x, y, width, height)
|
||||
|
||||
s := loadOp(ctx, ui.mountedPage.Update)
|
||||
if s.err != nil {
|
||||
ui.stopOnError(s.err)
|
||||
|
|
Loading…
Reference in a new issue