[#1223] lens/tui: Add search by attribute key or key and value
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
parent
c54ae73841
commit
1504a1ae19
8 changed files with 173 additions and 132 deletions
|
@ -2,15 +2,10 @@ 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"
|
||||
|
@ -32,6 +27,10 @@ func tuiFunc(cmd *cobra.Command, _ []string) {
|
|||
}
|
||||
|
||||
func runTUI(cmd *cobra.Command) error {
|
||||
// tview.Styles.PrimaryTextColor = tcell.ColorBlack
|
||||
// tview.Styles.PrimitiveBackgroundColor = tcell.ColorWhite
|
||||
// tview.Styles.GraphicsColor = tcell.ColorBlack
|
||||
|
||||
db, err := openDB(false)
|
||||
common.ExitOnErr(cmd, err)
|
||||
defer db.Close()
|
||||
|
@ -42,58 +41,10 @@ func runTUI(cmd *cobra.Command) error {
|
|||
app := tview.NewApplication()
|
||||
ui := tuiutil.NewUI(ctx, app, db)
|
||||
|
||||
_ = 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
|
||||
})
|
||||
_ = ui.AddFilter("cid", cidParser)
|
||||
_ = ui.AddFilter("oid", oidParser)
|
||||
_ = ui.AddCompositeFilter("addr", addrParser)
|
||||
_ = ui.AddCompositeFilter("attr", attributeParser)
|
||||
|
||||
app.SetRoot(ui, true).SetFocus(ui)
|
||||
|
||||
|
|
97
cmd/frostfs-lens/internal/meta/util.go
Normal file
97
cmd/frostfs-lens/internal/meta/util.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func cidParser(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
|
||||
}
|
||||
|
||||
func oidParser(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
|
||||
}
|
||||
|
||||
func addrParser(s string) (map[string]any, error) {
|
||||
m := make(map[string]any)
|
||||
|
||||
parts := strings.Split(s, "/")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("expected <cid>/<oid>")
|
||||
}
|
||||
cnr, err := cidParser(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj, err := oidParser(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m["cid"] = cnr
|
||||
m["oid"] = obj
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func keyParser(s string) (any, error) {
|
||||
if s == "" {
|
||||
return nil, errors.New("empty attribute key")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func valueParser(s string) (any, error) {
|
||||
if s == "" {
|
||||
return nil, errors.New("empty attribute value")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func attributeParser(s string) (map[string]any, error) {
|
||||
m := make(map[string]any)
|
||||
|
||||
parts := strings.Split(s, "/")
|
||||
if len(parts) != 1 && len(parts) != 2 {
|
||||
return nil, errors.New("expected <key> or <key>/<value>")
|
||||
}
|
||||
|
||||
key, err := keyParser(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["key"] = key
|
||||
|
||||
if len(parts) == 1 {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
value, err := valueParser(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["value"] = value
|
||||
|
||||
return m, nil
|
||||
}
|
|
@ -21,13 +21,6 @@ type (
|
|||
resolvers Resolvers
|
||||
}
|
||||
|
||||
PrefixContainerKeyBucket struct {
|
||||
prefix Prefix
|
||||
id cid.ID
|
||||
key string
|
||||
resolvers Resolvers
|
||||
}
|
||||
|
||||
ContainerBucket struct {
|
||||
id cid.ID
|
||||
resolvers Resolvers
|
||||
|
@ -38,9 +31,14 @@ type (
|
|||
resolvers Resolvers
|
||||
}
|
||||
|
||||
ValueBucket struct {
|
||||
value string
|
||||
resolvers Resolvers
|
||||
UserAttributeKeyBucket struct {
|
||||
prefix Prefix
|
||||
id cid.ID
|
||||
key string
|
||||
}
|
||||
|
||||
UserAttributeValueBucket struct {
|
||||
value string
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -102,27 +100,6 @@ func NewPrefixContainerBucketParser(prefix Prefix, next common.Parser, resolvers
|
|||
}
|
||||
}
|
||||
|
||||
func NewPrefixContainerKeyBucketParser(prefix Prefix, next common.Parser, resolvers Resolvers) common.Parser {
|
||||
return func(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||
if value != nil {
|
||||
return nil, nil, ErrNotBucket
|
||||
}
|
||||
if len(key) < 34 {
|
||||
return nil, nil, ErrInvalidKeyLength
|
||||
}
|
||||
var b PrefixContainerKeyBucket
|
||||
if b.prefix = Prefix(key[0]); b.prefix != prefix {
|
||||
return nil, nil, ErrInvalidPrefix
|
||||
}
|
||||
if err := b.id.Decode(key[1:33]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
b.key = string(key[33:])
|
||||
b.resolvers = resolvers
|
||||
return &b, next, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewUserBucketParser(next common.Parser, resolvers Resolvers) common.Parser {
|
||||
return func(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||
if value != nil {
|
||||
|
@ -154,7 +131,27 @@ func NewContainerBucketParser(next common.Parser, resolvers Resolvers) common.Pa
|
|||
}
|
||||
}
|
||||
|
||||
func NewValueBucketParser(next common.Parser, resolvers Resolvers) common.Parser {
|
||||
func NewUserAttributeKeyBucketParser(next common.Parser) common.Parser {
|
||||
return func(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||
if value != nil {
|
||||
return nil, nil, ErrNotBucket
|
||||
}
|
||||
if len(key) < 34 {
|
||||
return nil, nil, ErrInvalidKeyLength
|
||||
}
|
||||
var b UserAttributeKeyBucket
|
||||
if b.prefix = Prefix(key[0]); b.prefix != UserAttribute {
|
||||
return nil, nil, ErrInvalidPrefix
|
||||
}
|
||||
if err := b.id.Decode(key[1:33]); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
b.key = string(key[33:])
|
||||
return &b, next, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewUserAttributeValueBucketParser(next common.Parser) common.Parser {
|
||||
return func(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||
if value != nil {
|
||||
return nil, nil, ErrNotBucket
|
||||
|
@ -162,9 +159,8 @@ func NewValueBucketParser(next common.Parser, resolvers Resolvers) common.Parser
|
|||
if len(key) == 0 {
|
||||
return nil, nil, ErrInvalidKeyLength
|
||||
}
|
||||
var b ValueBucket
|
||||
var b UserAttributeValueBucket
|
||||
b.value = string(key)
|
||||
b.resolvers = resolvers
|
||||
return &b, next, nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,10 +12,6 @@ func (b *PrefixContainerBucket) DetailedString() string {
|
|||
return spew.Sdump(*b)
|
||||
}
|
||||
|
||||
func (b *PrefixContainerKeyBucket) DetailedString() string {
|
||||
return spew.Sdump(*b)
|
||||
}
|
||||
|
||||
func (b *UserBucket) DetailedString() string {
|
||||
return spew.Sdump(*b)
|
||||
}
|
||||
|
@ -24,6 +20,10 @@ func (b *ContainerBucket) DetailedString() string {
|
|||
return spew.Sdump(*b)
|
||||
}
|
||||
|
||||
func (b *ValueBucket) DetailedString() string {
|
||||
func (b *UserAttributeKeyBucket) DetailedString() string {
|
||||
return spew.Sdump(*b)
|
||||
}
|
||||
|
||||
func (b *UserAttributeValueBucket) DetailedString() string {
|
||||
return spew.Sdump(*b)
|
||||
}
|
||||
|
|
|
@ -28,18 +28,6 @@ func (b *PrefixContainerBucket) Filter(typ string, val any) common.FilterResult
|
|||
}
|
||||
}
|
||||
|
||||
func (b *PrefixContainerKeyBucket) Filter(typ string, val any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
id := val.(cid.ID)
|
||||
return b.resolvers.cidResolver(b.id.Equals(id))
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
||||
func (b *UserBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
|
@ -63,12 +51,30 @@ func (b *ContainerBucket) Filter(typ string, val any) common.FilterResult {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *ValueBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
func (b *UserAttributeKeyBucket) Filter(typ string, val any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
return b.resolvers.cidResolver(false)
|
||||
id := val.(cid.ID)
|
||||
return common.IfThenElse(b.id.Equals(id), common.Yes, common.No)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
return common.Maybe
|
||||
case "key":
|
||||
key := val.(string)
|
||||
return common.IfThenElse(b.key == key, common.Yes, common.No)
|
||||
case "value":
|
||||
return common.Maybe
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
||||
func (b *UserAttributeValueBucket) Filter(typ string, val any) common.FilterResult {
|
||||
switch typ {
|
||||
case "oid":
|
||||
return common.Maybe
|
||||
case "value":
|
||||
value := val.(string)
|
||||
return common.IfThenElse(b.value == value, common.Yes, common.No)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
|
|
|
@ -104,19 +104,8 @@ var (
|
|||
},
|
||||
)
|
||||
|
||||
UserAttributeParser = NewPrefixContainerKeyBucketParser(
|
||||
UserAttribute,
|
||||
NewValueBucketParser(
|
||||
records.UserAttributeRecordParser,
|
||||
Resolvers{
|
||||
cidResolver: StrictResolver,
|
||||
oidResolver: LenientResolver,
|
||||
},
|
||||
),
|
||||
Resolvers{
|
||||
cidResolver: StrictResolver,
|
||||
oidResolver: LenientResolver,
|
||||
},
|
||||
UserAttributeParser = NewUserAttributeKeyBucketParser(
|
||||
NewUserAttributeValueBucketParser(records.UserAttributeRecordParser),
|
||||
)
|
||||
|
||||
PayloadHashParser = NewPrefixContainerBucketParser(PayloadHash, records.PayloadHashRecordParser, Resolvers{
|
||||
|
|
|
@ -10,10 +10,6 @@ func (b *PrefixContainerBucket) String() string {
|
|||
return fmt.Sprintf("%s CID [blue]%s[white]", b.prefix, b.id)
|
||||
}
|
||||
|
||||
func (b *PrefixContainerKeyBucket) String() string {
|
||||
return fmt.Sprintf("%s CID [blue]%-44s[white] %s", b.prefix, b.id, b.key)
|
||||
}
|
||||
|
||||
func (b *UserBucket) String() string {
|
||||
return fmt.Sprintf("UID [blue]%s[white]", b.id)
|
||||
}
|
||||
|
@ -22,6 +18,10 @@ func (b *ContainerBucket) String() string {
|
|||
return fmt.Sprintf("CID [blue]%s[white]", b.id)
|
||||
}
|
||||
|
||||
func (b *ValueBucket) String() string {
|
||||
func (b *UserAttributeKeyBucket) String() string {
|
||||
return fmt.Sprintf("%s CID [blue]%-44s[white] %s", b.prefix, b.id, b.key)
|
||||
}
|
||||
|
||||
func (b *UserAttributeValueBucket) String() string {
|
||||
return b.value
|
||||
}
|
||||
|
|
|
@ -269,7 +269,7 @@ func (ui *UI) mountAndUpdate(ctx context.Context) {
|
|||
// exceptional, we always want to have previously mounted page saved.
|
||||
ui.saveMounted = true
|
||||
|
||||
// After application started the first mount either fails or succeeds.
|
||||
// If the first mount is canceled, quit the application.
|
||||
ui.isFirstMount = false
|
||||
}()
|
||||
|
||||
|
@ -427,15 +427,17 @@ func (ui *UI) handleInputOnSearching(event *tcell.EventKey) {
|
|||
}
|
||||
|
||||
ui.isSearching = false
|
||||
ui.searchBar.SetText("")
|
||||
// ui.searchBar.SetText("")
|
||||
case k == tcell.KeyEsc:
|
||||
ui.isSearching = false
|
||||
ui.searchBar.SetText("")
|
||||
// ui.searchBar.SetText("")
|
||||
case (k == tcell.KeyBackspace2 || m&tcell.ModCtrl != 0 && k == tcell.KeyETB) && len(ui.searchBar.GetText()) == 0:
|
||||
ui.isSearching = false
|
||||
default:
|
||||
ui.searchBar.InputHandler()(event, func(tview.Primitive) {})
|
||||
}
|
||||
|
||||
ui.Box.MouseHandler()
|
||||
}
|
||||
|
||||
func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
|
||||
|
|
Loading…
Reference in a new issue