[#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:
parent
78a39f47b3
commit
235cc15037
17 changed files with 497 additions and 203 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
47
cmd/frostfs-lens/internal/tuiutil/filter.go
Normal file
47
cmd/frostfs-lens/internal/tuiutil/filter.go
Normal 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
|
||||
}
|
1
cmd/frostfs-lens/internal/tuiutil/help.go
Normal file
1
cmd/frostfs-lens/internal/tuiutil/help.go
Normal file
|
@ -0,0 +1 @@
|
|||
package tuiutil
|
|
@ -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)
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue