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

Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
Aleksey Savchuk 2024-08-07 11:30:36 +03:00
parent 78a39f47b3
commit 235cc15037
No known key found for this signature in database
17 changed files with 497 additions and 203 deletions

View file

@ -2,10 +2,15 @@ package meta
import (
"context"
"errors"
"fmt"
"strings"
common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/tuiutil"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"github.com/mr-tron/base58"
"github.com/rivo/tview"
"github.com/spf13/cobra"
"go.etcd.io/bbolt"
@ -37,8 +42,58 @@ func runTUI(cmd *cobra.Command) error {
app := tview.NewApplication()
ui := tuiutil.NewUI(ctx, app, db)
ui.AddFilter("cid", func(s string) (any, error) { return s, nil })
ui.AddFilter("oid", func(s string) (any, error) { return s, nil })
_ = ui.AddFilter("cid", func(s string) (any, error) {
data, err := base58.Decode(s)
if err != nil {
return nil, err
}
var id cid.ID
if err = id.Decode(data); err != nil {
return nil, err
}
return id, nil
})
_ = ui.AddFilter("oid", func(s string) (any, error) {
data, err := base58.Decode(s)
if err != nil {
return nil, err
}
var id oid.ID
if err = id.Decode(data); err != nil {
return nil, err
}
return id, nil
})
_ = ui.AddCompositeFilter("addr", func(s string) (map[string]any, error) {
m := make(map[string]any)
parts := strings.Split(s, "/")
if len(parts) != 2 {
return nil, errors.New("invalid syntax")
}
data, err := base58.Decode(parts[0])
if err != nil {
return nil, err
}
cnr := cid.ID{}
if err = cnr.Decode(data); err != nil {
return nil, err
}
data, err = base58.Decode(parts[1])
if err != nil {
return nil, err
}
obj := oid.ID{}
if err = obj.Decode(data); err != nil {
return nil, err
}
m["cid"] = cnr
m["oid"] = obj
return m, nil
})
app.SetRoot(ui, true).SetFocus(ui)

View file

@ -26,31 +26,32 @@ func (r *Raw) Filter(string, any) FilterResult {
return No
}
func WithErrorHandling(parser Parser) Parser {
if parser == nil {
return rawParser
}
return func(key, value []byte) (SchemaEntry, Parser, error) {
entry, next, err := parser(key, value)
if err != nil {
return &Error{err: err}, rawParser, nil
}
return entry, WithErrorHandling(next), err
}
}
// TODO: enhance unparsed data display
// func WithErrorHandling(parser Parser) Parser {
// if parser == nil {
// return rawParser
// }
// return func(key, value []byte) (SchemaEntry, Parser, error) {
// entry, next, err := parser(key, value)
// if err != nil {
// return &Error{err: err}, rawParser, nil
// }
// return entry, WithErrorHandling(next), err
// }
// }
type Error struct {
err error
}
// type Error struct {
// err error
// }
func (e *Error) String() string {
return e.err.Error()
}
// func (e *Error) String() string {
// return e.err.Error()
// }
func (e *Error) DetailedString() string {
return spew.Sdump(e)
}
// func (e *Error) DetailedString() string {
// return e.err.Error()
// }
func (e *Error) Filter(string, any) FilterResult {
return No
}
// func (e *Error) Filter(string, any) FilterResult {
// return No
// }

View file

@ -5,12 +5,12 @@ import (
"fmt"
)
type FilterResult int
type FilterResult byte
const (
Yes FilterResult = iota
No
No FilterResult = iota
Maybe
Yes
)
func IfThenElse(condition bool, onSuccess, onFailure FilterResult) FilterResult {

View file

@ -1,27 +1,29 @@
package metabase
import "fmt"
import (
"github.com/davecgh/go-spew/spew"
)
func (b *PrefixBucket) DetailedString() string {
return b.prefix.String()
return spew.Sdump(*b)
}
func (b *PrefixContainerBucket) DetailedString() string {
return fmt.Sprintf("%s CID [red]%s[white]", b.prefix, b.id)
return spew.Sdump(*b)
}
func (b *PrefixContainerKeyBucket) DetailedString() string {
return fmt.Sprintf("%s CID [red]%-44s[white] %s", b.prefix, b.id, b.key)
return spew.Sdump(*b)
}
func (b *UserBucket) DetailedString() string {
return fmt.Sprintf("UID [red]%s[white]", b.id)
return spew.Sdump(*b)
}
func (b *ContainerBucket) DetailedString() string {
return fmt.Sprintf("CID [red]%s[white]", b.id)
return spew.Sdump(*b)
}
func (b *ValueBucket) DetailedString() string {
return b.value
return spew.Sdump(*b)
}

View file

@ -6,28 +6,6 @@ import (
)
var (
// MetabaseParser = common.WithErrorHandling(
// common.Any(
// GraveyardParser,
// GarbageParser,
// ContainerVolumeParser,
// LockedParser,
// ShardInfoParser,
// PrimaryParser,
// LockersParser,
// TombstoneParser,
// SmallParser,
// RootParser,
// OwnerParser,
// UserAttributeParser,
// PayloadHashParser,
// ParentParser,
// SplitParser,
// ContainerCountersParser,
// ECInfoParser,
// ),
// )
MetabaseParser = common.WithFallback(
common.Any(
GraveyardParser,

View file

@ -5,61 +5,61 @@ import (
)
func (r *GraveyardRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *GarbageRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *ContainerVolumeRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *LockedRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *ShardInfoRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *ObjectRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *SmallRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *RootRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *OwnerRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *UserAttributeRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *PayloadHashRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *ParentRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *SplitRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *ContainerCountersRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}
func (r *ECInfoRecord) DetailedString() string {
return spew.Sdump(r)
return spew.Sdump(*r)
}

View file

@ -3,11 +3,10 @@ package records
import (
"encoding/binary"
"errors"
"fmt"
"os"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)
@ -46,7 +45,7 @@ func GraveyardRecordParser(key, value []byte) (common.SchemaEntry, common.Parser
return &r, nil, nil
}
func GarbageRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
func GarbageRecordParser(key, _ []byte) (common.SchemaEntry, common.Parser, error) {
if len(key) != 64 {
return nil, nil, ErrInvalidKeyLength
}
@ -100,7 +99,6 @@ func ShardInfoRecordParser(key, value []byte) (common.SchemaEntry, common.Parser
return nil, nil, ErrInvalidKeyLength
}
if len(value) != 8 {
fmt.Fprintln(os.Stderr, key, value, string(key), string(value))
return nil, nil, ErrInvalidValueLength
}
var r ShardInfoRecord
@ -145,13 +143,14 @@ func RootRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, err
if len(value) == 0 {
return &r, nil, nil
}
r.info = &objectSDK.SplitInfo{}
if err := r.info.Unmarshal(value); err != nil {
return nil, nil, err
}
return &r, nil, nil
}
func OwnerRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
func OwnerRecordParser(key, _ []byte) (common.SchemaEntry, common.Parser, error) {
var r OwnerRecord
if err := r.id.Decode(key); err != nil {
return nil, nil, err
@ -159,7 +158,7 @@ func OwnerRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, er
return &r, nil, nil
}
func UserAttributeRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
func UserAttributeRecordParser(key, _ []byte) (common.SchemaEntry, common.Parser, error) {
var r UserAttributeRecord
if err := r.id.Decode(key); err != nil {
return nil, nil, err

View file

@ -4,24 +4,24 @@ import "fmt"
func (r *GraveyardRecord) String() string {
return fmt.Sprintf(
"Object CID [red]%-44s[white] OID [red]%-44s[white] | Tombstone CID [red]%-44s[white] OID [red]%-44s[white]",
"Object CID [blue]%-44s[white] OID [blue]%-44s[white] | Tombstone CID [blue]%-44s[white] OID [blue]%-44s[white]",
r.object.Container(), r.object.Object(), r.tombstone.Container(), r.object.Object(),
)
}
func (r *GarbageRecord) String() string {
return fmt.Sprintf(
"CID [red]%-44s[white] OID [red]%-44s[white]",
"CID [blue]%-44s[white] OID [blue]%-44s[white]",
r.addr.Container(), r.addr.Object(),
)
}
func (r *ContainerVolumeRecord) String() string {
return fmt.Sprintf("CID [red]%-44s[white] | %d", r.id, r.volume)
return fmt.Sprintf("CID [blue]%-44s[white] | %d", r.id, r.volume)
}
func (r *LockedRecord) String() string {
return fmt.Sprintf("Locker OID [red]%-44s[white] | Locked [%d]OID {...}", r.id, len(r.ids))
return fmt.Sprintf("Locker OID [blue]%-44s[white] | Locked [%d]OID {...}", r.id, len(r.ids))
}
func (r *ShardInfoRecord) String() string {
@ -29,50 +29,50 @@ func (r *ShardInfoRecord) String() string {
}
func (r *ObjectRecord) String() string {
return fmt.Sprintf("OID [red]%-44s[white] | Object {...}", r.id)
return fmt.Sprintf("OID [blue]%-44s[white] | Object {...}", r.id)
}
func (r *SmallRecord) String() string {
if r.storageID != nil {
return fmt.Sprintf("OID [red]%-44s[white] | %s", r.id, *r.storageID)
return fmt.Sprintf("OID [blue]%-44s[white] | %s", r.id, *r.storageID)
}
return fmt.Sprintf("OID [red]%-44s[white] |", r.id)
return fmt.Sprintf("OID [blue]%-44s[white] |", r.id)
}
func (r *RootRecord) String() string {
if r.info != nil {
return fmt.Sprintf("Root OID [red]%-44s[white] | Split info {...}", r.id)
return fmt.Sprintf("Root OID [blue]%-44s[white] | Split info {...}", r.id)
}
return fmt.Sprintf("Root OID [red]%-44s[white]", r.id)
return fmt.Sprintf("Root OID [blue]%-44s[white] |", r.id)
}
func (r *OwnerRecord) String() string {
return fmt.Sprintf("OID [red]%s[white]", r.id)
return fmt.Sprintf("OID [blue]%s[white]", r.id)
}
func (r *UserAttributeRecord) String() string {
return fmt.Sprintf("OID [red]%s[white]", r.id)
return fmt.Sprintf("OID [blue]%s[white]", r.id)
}
func (r *PayloadHashRecord) String() string {
return fmt.Sprintf("Checksum [red]%32s[white] | [%d]OID {...}", r.checksum, len(r.ids))
return fmt.Sprintf("Checksum [blue]%32s[white] | [%d]OID {...}", r.checksum, len(r.ids))
}
func (r *ParentRecord) String() string {
return fmt.Sprintf("Parent OID [red]%-44s[white] | [%d]OID {...}", r.parent, len(r.ids))
return fmt.Sprintf("Parent OID [blue]%-44s[white] | [%d]OID {...}", r.parent, len(r.ids))
}
func (r *SplitRecord) String() string {
return fmt.Sprintf("Split ID [red]%32s[white] | [%d]OID {...}", r.id, len(r.ids))
return fmt.Sprintf("Split ID [blue]%32s[white] | [%d]OID {...}", r.id, len(r.ids))
}
func (r *ContainerCountersRecord) String() string {
return fmt.Sprintf(
"CID [red]%-44s[white] | logical %d, physical %d, user %d",
"CID [blue]%-44s[white] | logical %d, physical %d, user %d",
r.id, r.logical, r.physical, r.user,
)
}
func (r *ECInfoRecord) String() string {
return fmt.Sprintf("OID [red]%-44s[white] | [%d]OID {...}", r.id, len(r.ids))
return fmt.Sprintf("OID [blue]%-44s[white] | [%d]OID {...}", r.id, len(r.ids))
}

View file

@ -7,19 +7,19 @@ func (b *PrefixBucket) String() string {
}
func (b *PrefixContainerBucket) String() string {
return fmt.Sprintf("%s CID [red]%s[white]", b.prefix, b.id)
return fmt.Sprintf("%s CID [blue]%s[white]", b.prefix, b.id)
}
func (b *PrefixContainerKeyBucket) String() string {
return fmt.Sprintf("%s CID [red]%-44s[white] %s", b.prefix, b.id, b.key)
return fmt.Sprintf("%s CID [blue]%-44s[white] %s", b.prefix, b.id, b.key)
}
func (b *UserBucket) String() string {
return fmt.Sprintf("UID [red]%s[white]", b.id)
return fmt.Sprintf("UID [blue]%s[white]", b.id)
}
func (b *ContainerBucket) String() string {
return fmt.Sprintf("CID [red]%s[white]", b.id)
return fmt.Sprintf("CID [blue]%s[white]", b.id)
}
func (b *ValueBucket) String() string {

View file

@ -2,9 +2,8 @@ package tuiutil
import (
"context"
"fmt"
"os"
"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"
@ -16,13 +15,22 @@ type BucketsView struct {
ui *UI
needExpand bool
onUnmount func()
filter *Filter
}
func NewBucketsView(ui *UI) *BucketsView {
type BucketNode struct {
bucket *Bucket
parent *tview.TreeNode
filter *Filter
}
func NewBucketsView(ui *UI, filter *Filter) *BucketsView {
return &BucketsView{
Box: tview.NewBox(),
view: tview.NewTreeView(),
ui: ui,
Box: tview.NewBox(),
view: tview.NewTreeView(),
ui: ui,
filter: filter,
}
}
@ -35,9 +43,13 @@ func (v *BucketsView) Mount(ctx context.Context) error {
root.
SetSelectable(false).
SetExpanded(true).
SetReference(&Bucket{NextHandler: handler})
SetReference(&BucketNode{
bucket: &Bucket{NextHandler: handler},
parent: nil,
filter: v.filter,
})
if err := v.getChildren(ctx, root); err != nil {
if err := v.getChildrenFilter(ctx, root, v.filter); err != nil {
return err
}
@ -48,6 +60,66 @@ 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 {
return nil
@ -62,13 +134,14 @@ func (v *BucketsView) Update(ctx context.Context) error {
tmp := tview.NewTreeNode(current.GetText())
tmp.SetReference(current.GetReference())
node := current.GetReference().(*BucketNode)
go func() {
defer close(ready)
v.needExpand = false
if !current.IsExpanded() {
fmt.Fprintf(os.Stderr, `"%+v" "%+v"\n`, v, tmp)
_ = v.getChildren(ctx, tmp)
_ = v.getChildrenFilter(ctx, tmp, node.filter)
}
}()
@ -99,8 +172,12 @@ func (v *BucketsView) Draw(screen tcell.Screen) {
v.view.Draw(screen)
}
func (v *BucketsView) getChildren(ctx context.Context, parent *tview.TreeNode) error {
parentBucket := parent.GetReference().(*Bucket)
func (v *BucketsView) getChildrenFilter(
ctx context.Context,
parent *tview.TreeNode,
predicate *Filter,
) error {
parentBucket := parent.GetReference().(*BucketNode).bucket
path := parentBucket.Path
handler := parentBucket.NextHandler
@ -111,25 +188,31 @@ func (v *BucketsView) getChildren(ctx context.Context, parent *tview.TreeNode) e
}
for bucket := range buffer {
// if parent.GetText() != "." {
// <-time.After(10 * time.Millisecond)
// }
res, next, err := handler(bucket.Name, nil)
bucket.Entry, bucket.NextHandler, err = handler(bucket.Name, nil)
if err != nil {
return err
}
bucket.NextHandler = next
bucket.Result = res
child := tview.NewTreeNode(bucket.Entry.String())
child := tview.NewTreeNode(res.String())
f := predicate.Copy()
f.Apply(bucket.Entry)
child.SetSelectable(true)
child.SetExpanded(false)
child.SetReference(bucket)
child.SetReference(&BucketNode{
bucket: bucket,
parent: parent,
filter: f,
})
parent.AddChild(child)
flag, err := v.satisfies(ctx, bucket, predicate.Copy())
if err != nil {
return err
}
if flag {
parent.AddChild(child)
}
}
return nil
@ -137,22 +220,31 @@ func (v *BucketsView) getChildren(ctx context.Context, parent *tview.TreeNode) e
func (v *BucketsView) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
switch m, k := event.Modifiers(), event.Key(); {
case k == tcell.KeyEnter:
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.KeyRune && event.Rune() == 'z':
current := v.view.GetCurrentNode()
case k == tcell.KeyEnter || m&tcell.ModCtrl != 0 && k == tcell.KeyRight:
if current == nil {
return
}
v.ui.moveNextPage(NewRecordsView(v.ui, current.GetReference().(*Bucket)))
case m&tcell.ModCtrl != 0 && k == tcell.KeyRight:
current := v.view.GetCurrentNode()
if current == nil {
return
node := current.GetReference().(*BucketNode)
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()))
}
bucket := current.GetReference().(*Bucket)
v.ui.moveNextPage(NewRecordsView(v.ui, bucket))
default:
v.view.InputHandler()(event, func(tview.Primitive) {})
}

View file

@ -42,8 +42,7 @@ func load(
errCh := make(chan error)
go func() {
// TODO how to handle an error
// return two channels, one for result, another for error?
// TODO enhance error handling.
_ = db.View(func(tx *bbolt.Tx) error {
defer close(buffer)
@ -145,3 +144,37 @@ func LoadRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Reco
}()
return out, nil
}
func HasBuckets(ctx context.Context, db *bbolt.DB, path [][]byte) (bool, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
buffer, err := LoadBuckets(ctx, db, path)
if err != nil {
return false, err
}
_, ok := <-buffer
if !ok {
return false, nil
}
return true, nil
}
func HasRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (bool, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
buffer, err := LoadRecords(ctx, db, path)
if err != nil {
return false, err
}
_, ok := <-buffer
if !ok {
return false, nil
}
return true, nil
}

View file

@ -0,0 +1,47 @@
package tuiutil
import (
"maps"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
)
type Filter struct {
values map[string]any
results map[string]common.FilterResult
}
func NewFilter(values map[string]any) *Filter {
f := &Filter{
values: values,
results: make(map[string]common.FilterResult),
}
for id := range values {
var zero common.FilterResult
f.results[id] = zero
}
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) Result() common.FilterResult {
current := common.Yes
for _, r := range f.results {
current = min(r, current)
}
return current
}
func (f *Filter) Copy() *Filter {
c := &Filter{
values: maps.Clone(f.values),
results: maps.Clone(f.results),
}
return c
}

View file

@ -0,0 +1 @@
package tuiutil

View file

@ -55,9 +55,19 @@ func (b *LoadingBar) Stop() {
}
func (b *LoadingBar) Draw(screen tcell.Screen) {
b.view.SetText(fmt.Sprintf(
" Loading... %ds (press Escape to cancel) ", b.secondsElapsed.Load(),
))
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,
)
}
b.view.SetText(text)
x, y, width, _ := b.GetInnerRect()
b.view.SetRect(x, y, width, 1)

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
@ -21,17 +22,22 @@ type RecordsView struct {
lastRecordIndex int
selectedRecordIndex int
mainTextStyle tcell.Style
selectedTextStyle tcell.Style
filter *Filter
// TODO: use app color theme.
// mainTextStyle tcell.Style
// selectedTextStyle tcell.Style
}
func NewRecordsView(ui *UI, bucket *Bucket) *RecordsView {
func NewRecordsView(ui *UI, bucket *Bucket, filter *Filter) *RecordsView {
view := &RecordsView{
Box: tview.NewBox(),
ui: ui,
bucket: bucket,
filter: filter,
}
// TODO: use app color theme.
// view.mainTextStyle = tcell.StyleDefault.
// Background(ui.GetBackgroundColor()).
// Foreground(tview.Styles.PrimaryTextColor)
@ -56,11 +62,16 @@ func (v *RecordsView) Mount(ctx context.Context) error {
defer close(v.buffer)
for record := range tempBuffer {
result, _, err := v.bucket.NextHandler(record.Key, record.Value)
record.Entry, _, err = v.bucket.NextHandler(record.Key, record.Value)
if err != nil {
panic(err)
}
record.Result = result
f := v.filter.Copy()
f.Apply(record.Entry)
if f.Result() != common.Yes {
continue
}
v.buffer <- record
}
}()
@ -76,7 +87,8 @@ func (v *RecordsView) Unmount() {
v.onUnmount = nil
}
func (v *RecordsView) Update(ctx context.Context) error {
func (v *RecordsView) Update(_ context.Context) error {
// TODO: make update `commit or rollback`
_, _, _, recordsPerPage := v.GetInnerRect()
newLastRecordIndex := v.firstRecordIndex + recordsPerPage
@ -110,6 +122,7 @@ func (v *RecordsView) Draw(screen tcell.Screen) {
return
}
// TODO: show page number.
// pageNum := v.firstRecordIndex/height + 1
// title := v.GetTitle()
@ -121,7 +134,7 @@ func (v *RecordsView) Draw(screen tcell.Screen) {
v.DrawForSubclass(screen, v)
for index := v.firstRecordIndex; index < v.lastRecordIndex; index++ {
result := v.records[index].Result
result := v.records[index].Entry
text := result.String()
if index == v.selectedRecordIndex {
text = fmt.Sprintf("[:white]%s[:black]", text)
@ -192,10 +205,10 @@ func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.
v.moveToPrevPage()
v.selectLastRecord()
}
case m&tcell.ModCtrl != 0 && k == tcell.KeyRight:
case k == tcell.KeyEnter || m&tcell.ModCtrl != 0 && k == tcell.KeyRight:
current := v.getSelectedItem()
if current != nil {
v.ui.moveNextPage(NewDetailedView(current.Result.DetailedString()))
v.ui.moveNextPage(NewDetailedView(current.Entry.DetailedString()))
}
}
})

View file

@ -8,13 +8,16 @@ type Bucket struct {
Name []byte
Path [][]byte
HasBuckets,
HasRecords bool
NextHandler common.Parser
Result common.SchemaEntry
Entry common.SchemaEntry
}
type Record struct {
Key []byte
Value []byte
Path [][]byte
Result common.SchemaEntry
Key []byte
Value []byte
Path [][]byte
Entry common.SchemaEntry
}

View file

@ -2,8 +2,8 @@ package tuiutil
import (
"context"
"errors"
"fmt"
"os"
"strings"
"sync/atomic"
"time"
@ -27,6 +27,10 @@ type Primitive interface {
type UI struct {
*tview.Box
// Need to use context while drawing some pages those read data from
// a database. Context should be shared among multiple draw events.
// Library tview doesn't use contexts at all, so do that feature by myself.
//nolint:containedctx
ctx context.Context
app *tview.Application
@ -47,27 +51,29 @@ type UI struct {
cancelLoading func()
filters map[string]func(string) (any, error)
filters map[string]func(string) (any, error)
compositeFilters map[string]func(string) (map[string]any, error)
}
func NewUI(ctx context.Context, app *tview.Application, db *bbolt.DB) *UI {
spew.Config.DisableMethods = true
ui := &UI{
ctx: ctx,
Box: tview.NewBox(),
app: app,
db: db,
isFirstMount: true,
infoBar: tview.NewTextView(),
pageStub: tview.NewBox(),
searchBar: tview.NewInputField(),
filters: make(map[string]func(string) (any, error)),
ctx: ctx,
Box: tview.NewBox(),
app: app,
db: db,
isFirstMount: true,
infoBar: tview.NewTextView(),
pageStub: tview.NewBox(),
searchBar: tview.NewInputField(),
filters: make(map[string]func(string) (any, error)),
compositeFilters: make(map[string]func(string) (map[string]any, error)),
}
ui.loadingBar = NewLoadingBar(ui.triggerDraw)
ui.pageToMount = NewBucketsView(ui)
ui.pageToMount = NewBucketsView(ui, NewFilter(nil))
ui.searchBar.SetFieldBackgroundColor(ui.GetBackgroundColor())
ui.searchBar.SetFieldTextColor(tview.Styles.PrimaryTextColor)
@ -80,20 +86,45 @@ func NewUI(ctx context.Context, app *tview.Application, db *bbolt.DB) *UI {
return ui
}
func (ui *UI) AddFilter(typ string, parser func(string) (any, error)) error {
func (ui *UI) checkFilterExists(typ string) bool {
if _, ok := ui.filters[typ]; ok {
return true
}
if _, ok := ui.compositeFilters[typ]; ok {
return true
}
return false
}
func (ui *UI) AddFilter(
typ string,
parser func(string) (any, error),
) error {
if ui.checkFilterExists(typ) {
return fmt.Errorf("filter %s already exists", typ)
}
ui.filters[typ] = parser
return nil
}
func (ui *UI) stopOnError(err error) {
if err != nil {
ui.app.QueueEvent(tcell.NewEventError(err))
func (ui *UI) AddCompositeFilter(
typ string,
parser func(string) (map[string]any, error),
) error {
if ui.checkFilterExists(typ) {
return fmt.Errorf("filter %s already exists", typ)
}
ui.compositeFilters[typ] = parser
return nil
}
// TODO: enhance error handling
// func (ui *UI) stopOnError(err error) {
// if err != nil {
// ui.app.QueueEvent(tcell.NewEventError(err))
// }
// }
func (ui *UI) movePrevPage() {
ui.mountedPage.Unmount()
ui.mountedPage = ui.pageHistory[len(ui.pageHistory)-1]
@ -162,7 +193,8 @@ func (ui *UI) load(ctx context.Context) {
ready := make(chan struct{})
go func() {
defer close(ready)
ui.pageToMount.Mount(ctx)
// TODO: add error handling
_ = ui.pageToMount.Mount(ctx)
}()
select {
@ -181,7 +213,8 @@ func (ui *UI) load(ctx context.Context) {
pageToUpdate.SetRect(x, y, w, h-1)
pageToUpdate.Update(ctx)
// TODO: add error handling
_ = pageToUpdate.Update(ctx)
}()
select {
@ -228,27 +261,44 @@ func (ui *UI) draw(screen tcell.Screen) {
func (ui *UI) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return ui.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
m, k, r := event.Modifiers(), event.Key(), event.Rune()
if ui.isLoading.Load() {
k, r := event.Key(), event.Rune()
if k != tcell.KeyEsc && r != 'q' {
return
}
ui.cancelLoading()
if r == 'q' || ui.isFirstMount {
ui.app.Stop()
return
}
}
if ui.isSearching {
switch event.Key() {
case tcell.KeyEnter:
res, err := processPrompt(ui.searchBar.GetText()[1:], ui.filters)
fmt.Fprintln(os.Stderr, res, err)
prompt := ui.searchBar.GetText()[1:]
res, err := ui.processPrompt(prompt)
if err != nil {
return
}
// TODO: make two mount strategies.
switch ui.mountedPage.(type) {
case *BucketsView:
// ui.mountedPage.Unmount()
// ui.mountedPage = nil
ui.moveNextPage(NewBucketsView(ui, res))
case *RecordsView:
bucket := ui.mountedPage.(*RecordsView).bucket
// ui.mountedPage.Unmount()
// ui.mountedPage = nil
ui.moveNextPage(NewRecordsView(ui, bucket, res))
}
ui.isSearching = false
ui.searchBar.SetText("")
case tcell.KeyEsc:
@ -263,50 +313,60 @@ func (ui *UI) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.P
return
}
switch m, k, r := event.Modifiers(), event.Key(), event.Rune(); {
case k == tcell.KeyRune && r == '/':
switch {
case m == 0 && k == tcell.KeyRune && r == '/':
ui.isSearching = true
ui.searchBar.SetText("/")
case k == tcell.KeyRune && r == 'q':
case m == 0 && k == tcell.KeyRune && r == 'q':
ui.app.Stop()
case m&tcell.ModCtrl != 0 && k == tcell.KeyLeft:
case k == tcell.KeyEsc || m&tcell.ModCtrl != 0 && k == tcell.KeyLeft:
if len(ui.pageHistory) != 0 {
ui.movePrevPage()
}
default:
if ui.mountedPage != nil {
ui.mountedPage.InputHandler()(event, func(tview.Primitive) {})
} else {
ui.pageStub.InputHandler()(event, func(tview.Primitive) {})
}
}
})
}
func processPrompt(prompt string, filters map[string]func(string) (any, error)) (m map[string]any, err error) {
m = make(map[string]any)
prompt = strings.TrimSpace(prompt)
for _, word := range strings.Split(prompt, "+") {
word = strings.TrimSpace(word)
if word == "" {
continue
}
filter := strings.Split(word, ":")
if len(filter) != 2 {
return nil, fmt.Errorf("invalid syntax '%s'", word)
}
filterKey := strings.TrimSpace(filter[0])
filterValue := strings.TrimSpace(filter[1])
f, ok := filters[filterKey]
if !ok {
return nil, fmt.Errorf("unknown filter '%s'", word)
}
if m[filterKey], err = f(filterValue); err != nil {
return nil, err
}
func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
if prompt == "" {
return NewFilter(nil), nil
}
return m, nil
parts := strings.Split(prompt, ":")
if len(parts) != 2 {
return nil, errors.New("invalid filter syntax")
}
filterID := strings.TrimSpace(parts[0])
filterString := strings.TrimSpace(parts[1])
parser, ok := ui.filters[filterID]
if ok {
filterValue, err := parser(filterString)
if err != nil {
return nil, fmt.Errorf(
"can't parse '%s' filter value '%s': %w",
filterID, filterString, err,
)
}
return NewFilter(map[string]any{filterID: filterValue}), nil
}
compositeParser, ok := ui.compositeFilters[filterID]
if ok {
filterValue, err := compositeParser(filterString)
if err != nil {
return nil, fmt.Errorf(
"can't parse '%s' filter value '%s': %w",
filterID, filterString, err,
)
}
return NewFilter(filterValue), nil
}
return nil, fmt.Errorf("unknown filter '%s'", filterID)
}