[#1223] lens/tui: add records view and detailed view
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
parent
172492a9be
commit
b05fb6b767
19 changed files with 764 additions and 320 deletions
|
@ -1,6 +1,7 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal"
|
||||
|
@ -54,12 +55,15 @@ func runTUI(cmd *cobra.Command) error {
|
|||
common.ExitOnErr(cmd, err)
|
||||
defer db.Close()
|
||||
|
||||
// ctx, cancel := context.WithCancel(cmd.Context())
|
||||
// defer cancel()
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
app := tview.NewApplication()
|
||||
ui := lib.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 := lib.NewUI(app, db)
|
||||
app.SetRoot(ui, true).SetFocus(ui)
|
||||
|
||||
return app.Run()
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mr-tron/base58/base58"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
type Raw struct {
|
||||
|
@ -17,9 +15,42 @@ func rawParser(key, value []byte) (SchemaEntry, Parser, error) {
|
|||
}
|
||||
|
||||
func (r *Raw) String() string {
|
||||
return fmt.Sprintf("[red]%+v %+v[white]", base58.Encode(r.key), base58.Encode(r.value))
|
||||
return "[red]can't parse[white]"
|
||||
}
|
||||
|
||||
func (r *Raw) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *Error) String() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e *Error) DetailedString() string {
|
||||
return spew.Sdump(e)
|
||||
}
|
||||
|
||||
func (e *Error) Filter(string, any) FilterResult {
|
||||
return No
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ func IfThenElse(condition bool, onSuccess, onFailure FilterResult) FilterResult
|
|||
|
||||
type SchemaEntry interface {
|
||||
String() string
|
||||
DetailedString() string
|
||||
Filter(typ string, val any) FilterResult
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package metabase
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
|
@ -169,96 +168,3 @@ func NewValueBucketParser(next common.Parser, resolvers Resolvers) common.Parser
|
|||
return &b, next, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PrefixBucket) String() string {
|
||||
return b.prefix.String()
|
||||
}
|
||||
|
||||
func (b *PrefixBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
return b.resolvers.cidResolver(false)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PrefixContainerBucket) String() string {
|
||||
return fmt.Sprintf("%s CID %s", b.prefix, b.id)
|
||||
}
|
||||
|
||||
func (b *PrefixContainerBucket) 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 *PrefixContainerKeyBucket) String() string {
|
||||
return fmt.Sprintf("%s CID %s %s", b.prefix, b.id, b.key)
|
||||
}
|
||||
|
||||
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) String() string {
|
||||
return fmt.Sprintf("UID %s", b.id)
|
||||
}
|
||||
|
||||
func (b *UserBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
return b.resolvers.cidResolver(false)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ContainerBucket) String() string {
|
||||
return fmt.Sprintf("CID %s", b.id)
|
||||
}
|
||||
|
||||
func (b *ContainerBucket) 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 *ValueBucket) String() string {
|
||||
return b.value
|
||||
}
|
||||
|
||||
func (b *ValueBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
return b.resolvers.cidResolver(false)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
|
27
cmd/frostfs-lens/internal/schema/metabase/detailed.go
Normal file
27
cmd/frostfs-lens/internal/schema/metabase/detailed.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package metabase
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (b *PrefixBucket) DetailedString() string {
|
||||
return b.prefix.String()
|
||||
}
|
||||
|
||||
func (b *PrefixContainerBucket) DetailedString() string {
|
||||
return fmt.Sprintf("%s CID [red]%s[white]", b.prefix, b.id)
|
||||
}
|
||||
|
||||
func (b *PrefixContainerKeyBucket) DetailedString() string {
|
||||
return fmt.Sprintf("%s CID [red]%-44s[white] %s", b.prefix, b.id, b.key)
|
||||
}
|
||||
|
||||
func (b *UserBucket) DetailedString() string {
|
||||
return fmt.Sprintf("UID [red]%s[white]", b.id)
|
||||
}
|
||||
|
||||
func (b *ContainerBucket) DetailedString() string {
|
||||
return fmt.Sprintf("CID [red]%s[white]", b.id)
|
||||
}
|
||||
|
||||
func (b *ValueBucket) DetailedString() string {
|
||||
return b.value
|
||||
}
|
75
cmd/frostfs-lens/internal/schema/metabase/filter.go
Normal file
75
cmd/frostfs-lens/internal/schema/metabase/filter.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package metabase
|
||||
|
||||
import (
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
)
|
||||
|
||||
func (b *PrefixBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
return b.resolvers.cidResolver(false)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PrefixContainerBucket) 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 *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":
|
||||
return b.resolvers.cidResolver(false)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ContainerBucket) 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 *ValueBucket) Filter(typ string, _ any) common.FilterResult {
|
||||
switch typ {
|
||||
case "cid":
|
||||
return b.resolvers.cidResolver(false)
|
||||
case "oid":
|
||||
return b.resolvers.oidResolver(false)
|
||||
default:
|
||||
return common.No
|
||||
}
|
||||
}
|
|
@ -6,6 +6,28 @@ 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,
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package records
|
||||
|
||||
import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
)
|
||||
|
||||
func (r *GraveyardRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *GarbageRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *ContainerVolumeRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *LockedRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *ShardInfoRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *ObjectRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *SmallRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *RootRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *OwnerRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *UserAttributeRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *PayloadHashRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *ParentRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *SplitRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *ContainerCountersRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
||||
|
||||
func (r *ECInfoRecord) DetailedString() string {
|
||||
return spew.Sdump(r)
|
||||
}
|
|
@ -3,6 +3,8 @@ 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"
|
||||
|
@ -48,9 +50,6 @@ func GarbageRecordParser(key, value []byte) (common.SchemaEntry, common.Parser,
|
|||
if len(key) != 64 {
|
||||
return nil, nil, ErrInvalidKeyLength
|
||||
}
|
||||
if len(value) != 0 {
|
||||
return nil, nil, ErrInvalidValueLength
|
||||
}
|
||||
var (
|
||||
cnr cid.ID
|
||||
obj oid.ID
|
||||
|
@ -101,6 +100,7 @@ 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
|
||||
|
@ -152,9 +152,6 @@ func RootRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, err
|
|||
}
|
||||
|
||||
func OwnerRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||
if len(value) != 0 {
|
||||
return nil, nil, ErrInvalidValueLength
|
||||
}
|
||||
var r OwnerRecord
|
||||
if err := r.id.Decode(key); err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -163,9 +160,6 @@ func OwnerRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, er
|
|||
}
|
||||
|
||||
func UserAttributeRecordParser(key, value []byte) (common.SchemaEntry, common.Parser, error) {
|
||||
if len(value) != 0 {
|
||||
return nil, nil, ErrInvalidValueLength
|
||||
}
|
||||
var r UserAttributeRecord
|
||||
if err := r.id.Decode(key); err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -3,35 +3,41 @@ package records
|
|||
import "fmt"
|
||||
|
||||
func (r *GraveyardRecord) String() string {
|
||||
return fmt.Sprintf("%s %s", r.object, r.tombstone)
|
||||
return fmt.Sprintf(
|
||||
"Object CID [red]%-44s[white] OID [red]%-44s[white] | Tombstone CID [red]%-44s[white] OID [red]%-44s[white]",
|
||||
r.object.Container(), r.object.Object(), r.tombstone.Container(), r.object.Object(),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *GarbageRecord) String() string {
|
||||
return r.addr.String()
|
||||
return fmt.Sprintf(
|
||||
"CID [red]%-44s[white] OID [red]%-44s[white]",
|
||||
r.addr.Container(), r.addr.Object(),
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ContainerVolumeRecord) String() string {
|
||||
return fmt.Sprintf("%s %d", r.id, r.volume)
|
||||
return fmt.Sprintf("CID [red]%-44s[white] | %d", r.id, r.volume)
|
||||
}
|
||||
|
||||
func (r *LockedRecord) String() string {
|
||||
return fmt.Sprintf("%s %v", r.id, r.ids)
|
||||
return fmt.Sprintf("Locker OID [red]%-44s[white] | Locked [%d]OID {...}", r.id, len(r.ids))
|
||||
}
|
||||
|
||||
func (r *ShardInfoRecord) String() string {
|
||||
return fmt.Sprintf("%s %d", r.label, r.value)
|
||||
return fmt.Sprintf("%-13s | %d", r.label, r.value)
|
||||
}
|
||||
|
||||
func (r *ObjectRecord) String() string {
|
||||
return fmt.Sprintf("%s %+v", r.id, r.object)
|
||||
return fmt.Sprintf("OID [red]%-44s[white] | Object {...}", r.id)
|
||||
}
|
||||
|
||||
func (r *SmallRecord) String() string {
|
||||
var s string
|
||||
if r.storageID != nil {
|
||||
s = fmt.Sprintf("OID %s %s", r.id, *r.storageID)
|
||||
s = fmt.Sprintf("OID [red]%-44s[white] | Path %s", r.id, *r.storageID)
|
||||
} else {
|
||||
s = fmt.Sprintf("OID %s", r.id)
|
||||
s = fmt.Sprintf("OID [red]%-44s[white]", r.id)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
@ -39,37 +45,40 @@ func (r *SmallRecord) String() string {
|
|||
func (r *RootRecord) String() string {
|
||||
var s string
|
||||
if r.info != nil {
|
||||
s = fmt.Sprintf("OID %s %v", r.id, *r.info)
|
||||
s = fmt.Sprintf("Root OID [red]%-44s[white] | Split info {...}", r.id)
|
||||
} else {
|
||||
s = fmt.Sprintf("OID %s", r.id)
|
||||
s = fmt.Sprintf("Root OID [red]%-44s[white]", r.id)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *OwnerRecord) String() string {
|
||||
return fmt.Sprintf("OID %s", r.id)
|
||||
return fmt.Sprintf("OID [red]%s[white]", r.id)
|
||||
}
|
||||
|
||||
func (r *UserAttributeRecord) String() string {
|
||||
return fmt.Sprintf("OID %s", r.id)
|
||||
return fmt.Sprintf("OID [red]%s[white]", r.id)
|
||||
}
|
||||
|
||||
func (r *PayloadHashRecord) String() string {
|
||||
return fmt.Sprintf("%s [...]", r.checksum)
|
||||
return fmt.Sprintf("Checksum [red]%32s[white] | [%d]OID {...}", r.checksum, len(r.ids))
|
||||
}
|
||||
|
||||
func (r *ParentRecord) String() string {
|
||||
return fmt.Sprintf("OID %s %v", r.parent, r.ids)
|
||||
return fmt.Sprintf("Parent OID [red]%-44s[white] | [%d]OID {...}", r.parent, len(r.ids))
|
||||
}
|
||||
|
||||
func (r *SplitRecord) String() string {
|
||||
return fmt.Sprintf("Split ID %s %v", r.id, r.ids)
|
||||
return fmt.Sprintf("Split ID [red]%32s[white] | [%d]OID {...}", r.id, len(r.ids))
|
||||
}
|
||||
|
||||
func (r *ContainerCountersRecord) String() string {
|
||||
return fmt.Sprintf("%s %d %d %d", r.id, r.logical, r.physical, r.user)
|
||||
return fmt.Sprintf(
|
||||
"CID [red]%-44s[white] | logical %d, physical %d, user %d",
|
||||
r.id, r.logical, r.physical, r.user,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ECInfoRecord) String() string {
|
||||
return fmt.Sprintf("%s %v", r.id, r.ids)
|
||||
return fmt.Sprintf("OID [red]%-44s[white] | [%d]OID {...}", r.id, len(r.ids))
|
||||
}
|
||||
|
|
27
cmd/frostfs-lens/internal/schema/metabase/string.go
Normal file
27
cmd/frostfs-lens/internal/schema/metabase/string.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package metabase
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (b *PrefixBucket) String() string {
|
||||
return b.prefix.String()
|
||||
}
|
||||
|
||||
func (b *PrefixContainerBucket) String() string {
|
||||
return fmt.Sprintf("%s CID [red]%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)
|
||||
}
|
||||
|
||||
func (b *UserBucket) String() string {
|
||||
return fmt.Sprintf("UID [red]%s[white]", b.id)
|
||||
}
|
||||
|
||||
func (b *ContainerBucket) String() string {
|
||||
return fmt.Sprintf("CID [red]%s[white]", b.id)
|
||||
}
|
||||
|
||||
func (b *ValueBucket) String() string {
|
||||
return b.value
|
||||
}
|
|
@ -14,15 +14,6 @@ import (
|
|||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// 29NkY3QtH6r8Hd9RBejwtUuju1ViAQ6jZPR7pjuTnkoH
|
||||
// 1QVJo3LCACFnbeqJ7uHz97PyoPrkZr4BnZ9cVYAhRPx
|
||||
|
||||
// 6E6b24qy32p3L3wjRSUEGNFHMxkUsaqj7udwWVKLzkU
|
||||
// 1GcnNjFnPof2YbPi2RBi3Sjy3qeR2cymc9BR6i9U2Kt
|
||||
|
||||
// 9wynaSKbhrkpQe7knLZvX8XQDW2hk3ZHJnjaLpXsWHtz
|
||||
// h9MW74svQ8hufDVcaAdyYiaSDZWeTEWo32aqKbTUqAu
|
||||
|
||||
type Bucket struct {
|
||||
Name []byte
|
||||
Path [][]byte
|
||||
|
@ -32,10 +23,10 @@ type Bucket struct {
|
|||
}
|
||||
|
||||
type Record struct {
|
||||
key []byte
|
||||
value []byte
|
||||
path [][]byte
|
||||
result common.SchemaEntry
|
||||
Key []byte
|
||||
Value []byte
|
||||
Path [][]byte
|
||||
Result common.SchemaEntry
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
|
|
|
@ -126,9 +126,9 @@ func LoadRecords(ctx context.Context, db *bbolt.DB, path [][]byte) (<-chan *Reco
|
|||
base = append(base, path...)
|
||||
|
||||
return &Record{
|
||||
key: key,
|
||||
value: value,
|
||||
path: append(base, key),
|
||||
Key: key,
|
||||
Value: value,
|
||||
Path: append(base, key),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -84,7 +84,11 @@ func (v *BucketsView) Update(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (v *BucketsView) Unmount() {
|
||||
if v.onUnmount == nil {
|
||||
panic("try to unmount not mounted component")
|
||||
}
|
||||
v.onUnmount()
|
||||
v.onUnmount = nil
|
||||
}
|
||||
|
||||
func (v *BucketsView) Draw(screen tcell.Screen) {
|
||||
|
@ -109,7 +113,7 @@ func (v *BucketsView) getChildren(ctx context.Context, parent *tview.TreeNode) e
|
|||
|
||||
for bucket := range buffer {
|
||||
// if parent.GetText() != "." {
|
||||
// <-time.After(500 * time.Millisecond)
|
||||
// <-time.After(10 * time.Millisecond)
|
||||
// }
|
||||
|
||||
res, next, err := handler(bucket.Name, nil)
|
||||
|
@ -137,8 +141,19 @@ func (v *BucketsView) InputHandler() func(event *tcell.EventKey, setFocus func(p
|
|||
switch m, k := event.Modifiers(), event.Key(); {
|
||||
case k == tcell.KeyEnter:
|
||||
v.needExpand = true
|
||||
case m&tcell.ModAlt == 0 && k == tcell.KeyRight:
|
||||
v.ui.moveNextPage(nil)
|
||||
case k == tcell.KeyRune && event.Rune() == 'z':
|
||||
current := v.view.GetCurrentNode()
|
||||
if current == nil {
|
||||
return
|
||||
}
|
||||
v.ui.moveNextPage(NewRecordsView(v.ui, current.GetReference().(*tuiutil.Bucket)))
|
||||
case m&tcell.ModCtrl != 0 && k == tcell.KeyRight:
|
||||
current := v.view.GetCurrentNode()
|
||||
if current == nil {
|
||||
return
|
||||
}
|
||||
bucket := current.GetReference().(*tuiutil.Bucket)
|
||||
v.ui.moveNextPage(NewRecordsView(v.ui, bucket))
|
||||
default:
|
||||
v.view.InputHandler()(event, func(tview.Primitive) {})
|
||||
}
|
||||
|
|
45
cmd/frostfs-lens/internal/tuiutil/new/detailed.go
Normal file
45
cmd/frostfs-lens/internal/tuiutil/new/detailed.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type DetailedView struct {
|
||||
*tview.Box
|
||||
view *tview.TextView
|
||||
}
|
||||
|
||||
func NewDetailedView(detailed string) *DetailedView {
|
||||
v := &DetailedView{
|
||||
Box: tview.NewBox(),
|
||||
view: tview.NewTextView(),
|
||||
}
|
||||
v.view.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) {})
|
||||
})
|
||||
}
|
|
@ -3,90 +3,63 @@ package lib
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type LoadingScreen struct {
|
||||
type LoadingBar struct {
|
||||
*tview.Box
|
||||
view *tview.TextView
|
||||
|
||||
startTime time.Time
|
||||
timeElapsed time.Duration
|
||||
|
||||
onUnmount func()
|
||||
|
||||
onCancel func()
|
||||
onUpdate func()
|
||||
view *tview.TextView
|
||||
secondsElapsed atomic.Int64
|
||||
needDrawFunc func()
|
||||
reset func()
|
||||
}
|
||||
|
||||
func NewLoadingScreen(onCancel func(), onUpdate func()) *LoadingScreen {
|
||||
ls := &LoadingScreen{
|
||||
Box: tview.NewBox(),
|
||||
view: tview.NewTextView(),
|
||||
onCancel: onCancel,
|
||||
onUpdate: onUpdate,
|
||||
func NewLoadingBar(needDrawFunc func()) *LoadingBar {
|
||||
b := &LoadingBar{
|
||||
Box: tview.NewBox(),
|
||||
view: tview.NewTextView(),
|
||||
needDrawFunc: needDrawFunc,
|
||||
}
|
||||
ls.view.SetBackgroundColor(tview.Styles.PrimaryTextColor)
|
||||
ls.view.SetTextColor(ls.GetBackgroundColor())
|
||||
b.view.SetBackgroundColor(tview.Styles.PrimaryTextColor)
|
||||
b.view.SetTextColor(b.GetBackgroundColor())
|
||||
|
||||
return ls
|
||||
return b
|
||||
}
|
||||
|
||||
func (ls *LoadingScreen) Mount(ctx context.Context) error {
|
||||
ctx, ls.onUnmount = context.WithCancel(ctx)
|
||||
onCancel := ls.onCancel
|
||||
|
||||
ls.onCancel = func() {
|
||||
ls.onUnmount()
|
||||
onCancel()
|
||||
}
|
||||
ls.startTime = time.Now()
|
||||
func (b *LoadingBar) Start(ctx context.Context) {
|
||||
ctx, b.reset = context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
b.secondsElapsed.Store(0)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ls.onUpdate()
|
||||
return
|
||||
case <-ticker.C:
|
||||
ls.onUpdate()
|
||||
b.secondsElapsed.Add(1)
|
||||
b.needDrawFunc()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LoadingScreen) Unmount() {
|
||||
ls.onUnmount()
|
||||
func (b *LoadingBar) Stop() {
|
||||
b.reset()
|
||||
}
|
||||
|
||||
func (ls *LoadingScreen) Update(_ context.Context) error {
|
||||
ls.timeElapsed = time.Since(ls.startTime)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LoadingScreen) Draw(screen tcell.Screen) {
|
||||
ls.DrawForSubclass(screen, ls)
|
||||
|
||||
text := fmt.Sprintf(" Loading... %.0fs (press Escape to cancel) ", ls.timeElapsed.Seconds())
|
||||
ls.view.SetText(text)
|
||||
|
||||
x, y, width, height := ls.GetInnerRect()
|
||||
ls.view.SetRect(x, y, min(len(text), width), height)
|
||||
ls.view.Draw(screen)
|
||||
}
|
||||
|
||||
func (ls *LoadingScreen) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
return ls.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
|
||||
if event.Key() == tcell.KeyEsc {
|
||||
ls.onCancel()
|
||||
}
|
||||
})
|
||||
func (b *LoadingBar) Draw(screen tcell.Screen) {
|
||||
b.view.SetText(fmt.Sprintf(
|
||||
" Loading... %ds (press Escape to cancel) ", b.secondsElapsed.Load(),
|
||||
))
|
||||
|
||||
x, y, width, _ := b.GetInnerRect()
|
||||
b.view.SetRect(x, y, width, 1)
|
||||
b.view.Draw(screen)
|
||||
}
|
||||
|
|
|
@ -1 +1,210 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/tuiutil"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type RecordsView struct {
|
||||
*tview.Box
|
||||
ui *UI
|
||||
onUnmount func()
|
||||
|
||||
bucket *tuiutil.Bucket
|
||||
records []*tuiutil.Record
|
||||
buffer chan *tuiutil.Record
|
||||
|
||||
firstRecordIndex int
|
||||
lastRecordIndex int
|
||||
selectedRecordIndex int
|
||||
|
||||
mainTextStyle tcell.Style
|
||||
selectedTextStyle tcell.Style
|
||||
}
|
||||
|
||||
func NewRecordsView(ui *UI, bucket *tuiutil.Bucket) *RecordsView {
|
||||
view := &RecordsView{
|
||||
Box: tview.NewBox(),
|
||||
ui: ui,
|
||||
bucket: bucket,
|
||||
}
|
||||
|
||||
// view.mainTextStyle = tcell.StyleDefault.
|
||||
// Background(ui.GetBackgroundColor()).
|
||||
// Foreground(tview.Styles.PrimaryTextColor)
|
||||
|
||||
// view.selectedTextStyle = tcell.StyleDefault.
|
||||
// Background(tview.Styles.PrimaryTextColor).
|
||||
// Foreground(ui.GetBackgroundColor())
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func (v *RecordsView) Mount(ctx context.Context) error {
|
||||
ctx, v.onUnmount = context.WithCancel(ctx)
|
||||
|
||||
tempBuffer, err := tuiutil.LoadRecords(ctx, v.ui.db, v.bucket.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.buffer = make(chan *tuiutil.Record, 100)
|
||||
go func() {
|
||||
defer close(v.buffer)
|
||||
|
||||
for record := range tempBuffer {
|
||||
result, _, err := v.bucket.NextHandler(record.Key, record.Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
record.Result = result
|
||||
v.buffer <- record
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *RecordsView) Unmount() {
|
||||
if v.onUnmount == nil {
|
||||
panic("try to unmount not mounted component")
|
||||
}
|
||||
v.onUnmount()
|
||||
v.onUnmount = nil
|
||||
}
|
||||
|
||||
func (v *RecordsView) Update(ctx context.Context) error {
|
||||
_, _, _, recordsPerPage := v.GetInnerRect()
|
||||
|
||||
newLastRecordIndex := v.firstRecordIndex + recordsPerPage
|
||||
|
||||
// The terminal's been resized and become shorter.
|
||||
if v.lastRecordIndex > newLastRecordIndex {
|
||||
v.lastRecordIndex = newLastRecordIndex
|
||||
|
||||
// If the selected record is invisible, select the last one visible.
|
||||
v.selectedRecordIndex = min(v.selectedRecordIndex, v.lastRecordIndex-1)
|
||||
return nil
|
||||
}
|
||||
|
||||
for len(v.records) < newLastRecordIndex {
|
||||
record, ok := <-v.buffer
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
v.records = append(v.records, record)
|
||||
}
|
||||
v.lastRecordIndex = min(len(v.records), newLastRecordIndex)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *RecordsView) Draw(screen tcell.Screen) {
|
||||
x, y, width, height := v.GetInnerRect()
|
||||
|
||||
if height == 0 {
|
||||
v.DrawForSubclass(screen, v)
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
for index := v.firstRecordIndex; index < v.lastRecordIndex; index++ {
|
||||
result := v.records[index].Result
|
||||
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.PrimaryTextColor)
|
||||
|
||||
y++
|
||||
}
|
||||
// v.SetTitle(title)
|
||||
}
|
||||
|
||||
func (v *RecordsView) moveToPrevPage() {
|
||||
// On the first page.
|
||||
if v.firstRecordIndex == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, _, recordsPerPage := v.GetInnerRect()
|
||||
v.firstRecordIndex -= recordsPerPage
|
||||
v.lastRecordIndex = v.firstRecordIndex + recordsPerPage
|
||||
|
||||
v.selectFirstRecord()
|
||||
}
|
||||
|
||||
func (v *RecordsView) moveToNextPage() {
|
||||
_, _, _, recordsPerPage := v.GetInnerRect()
|
||||
|
||||
// On the last page.
|
||||
if v.firstRecordIndex+recordsPerPage > v.lastRecordIndex {
|
||||
return
|
||||
}
|
||||
|
||||
v.firstRecordIndex += recordsPerPage
|
||||
v.lastRecordIndex += recordsPerPage
|
||||
|
||||
v.selectFirstRecord()
|
||||
}
|
||||
|
||||
func (v *RecordsView) selectFirstRecord() {
|
||||
v.selectedRecordIndex = v.firstRecordIndex
|
||||
}
|
||||
|
||||
func (v *RecordsView) selectLastRecord() {
|
||||
v.selectedRecordIndex = v.lastRecordIndex - 1
|
||||
}
|
||||
|
||||
func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.Primitive)) {
|
||||
return v.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) {
|
||||
switch _, k := event.Modifiers(), event.Key(); {
|
||||
case k == tcell.KeyLeft:
|
||||
v.moveToPrevPage()
|
||||
case k == tcell.KeyRight:
|
||||
v.moveToNextPage()
|
||||
case k == tcell.KeyDown:
|
||||
// Need to move onto the next page.
|
||||
if v.selectedRecordIndex+1 == v.lastRecordIndex {
|
||||
v.moveToNextPage()
|
||||
// Now is on the next page, otherwise it was the last page.
|
||||
if v.selectedRecordIndex+1 != v.lastRecordIndex {
|
||||
v.selectedRecordIndex++
|
||||
}
|
||||
} else {
|
||||
v.selectedRecordIndex++
|
||||
}
|
||||
case k == tcell.KeyUp:
|
||||
v.selectedRecordIndex = max(v.selectedRecordIndex-1, 0)
|
||||
if v.selectedRecordIndex < v.firstRecordIndex {
|
||||
v.moveToPrevPage()
|
||||
v.selectLastRecord()
|
||||
}
|
||||
case k == tcell.KeyRune && event.Rune() == 'd':
|
||||
current := v.getSelectedItem()
|
||||
if current != nil {
|
||||
v.ui.moveNextPage(NewDetailedView(current.Result.DetailedString()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (v *RecordsView) getSelectedItem() *tuiutil.Record {
|
||||
if v.selectedRecordIndex < len(v.records) {
|
||||
return v.records[v.selectedRecordIndex]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"go.etcd.io/bbolt"
|
||||
|
@ -23,55 +24,58 @@ type Primitive interface {
|
|||
Unmount()
|
||||
}
|
||||
|
||||
const (
|
||||
EventStayPage = iota
|
||||
EventMoveBack
|
||||
EventMoveForth
|
||||
)
|
||||
|
||||
type UI struct {
|
||||
*tview.Box
|
||||
|
||||
ctx context.Context
|
||||
|
||||
app *tview.Application
|
||||
db *bbolt.DB
|
||||
|
||||
pages []Primitive
|
||||
pageHistory []Primitive
|
||||
mountedPage Primitive
|
||||
pageToMount Primitive
|
||||
pageStub tview.Primitive
|
||||
|
||||
needInit bool
|
||||
infoBar *tview.TextView
|
||||
searchBar *tview.InputField
|
||||
loadingBar *LoadingBar
|
||||
|
||||
helpView *tview.TextView
|
||||
isFirstMount bool
|
||||
isSearching bool
|
||||
isLoading atomic.Bool
|
||||
|
||||
isSearching bool
|
||||
searchInput *tview.InputField
|
||||
|
||||
isLoading atomic.Bool
|
||||
loadingScreen *LoadingScreen
|
||||
|
||||
activePage Primitive
|
||||
pageToMove Primitive
|
||||
cancelLoading func()
|
||||
|
||||
filters map[string]func(string) (any, error)
|
||||
}
|
||||
|
||||
func NewUI(app *tview.Application, db *bbolt.DB) *UI {
|
||||
func NewUI(ctx context.Context, app *tview.Application, db *bbolt.DB) *UI {
|
||||
spew.Config.DisableMethods = true
|
||||
|
||||
ui := &UI{
|
||||
Box: tview.NewBox(),
|
||||
app: app,
|
||||
db: db,
|
||||
helpView: tview.NewTextView(),
|
||||
searchInput: 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)),
|
||||
}
|
||||
ui.activePage = NewBucketsView(ui)
|
||||
ui.activePage.Mount(context.Background())
|
||||
|
||||
ui.searchInput.SetFieldBackgroundColor(ui.GetBackgroundColor())
|
||||
ui.searchInput.SetFieldTextColor(tview.Styles.PrimaryTextColor)
|
||||
ui.loadingBar = NewLoadingBar(ui.triggerDraw)
|
||||
|
||||
ui.helpView.SetBackgroundColor(tview.Styles.PrimaryTextColor)
|
||||
ui.helpView.SetTextColor(ui.GetBackgroundColor())
|
||||
ui.pageToMount = NewBucketsView(ui)
|
||||
|
||||
ui.helpView.SetText(fmt.Sprintf(" %s (press h for help, / to search or q to quit) ", db.Path()))
|
||||
ui.searchBar.SetFieldBackgroundColor(ui.GetBackgroundColor())
|
||||
ui.searchBar.SetFieldTextColor(tview.Styles.PrimaryTextColor)
|
||||
|
||||
ui.infoBar.SetBackgroundColor(tview.Styles.PrimaryTextColor)
|
||||
ui.infoBar.SetTextColor(ui.GetBackgroundColor())
|
||||
|
||||
ui.infoBar.SetText(fmt.Sprintf(" %s (press h for help, / to search or q to quit) ", db.Path()))
|
||||
|
||||
return ui
|
||||
}
|
||||
|
@ -90,79 +94,107 @@ func (ui *UI) stopOnError(err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (ui *UI) movePrevPage(page Primitive) {
|
||||
if len(ui.pages) == 0 {
|
||||
return
|
||||
}
|
||||
ui.activePage.Unmount()
|
||||
ui.activePage = ui.pages[len(ui.pages)-1]
|
||||
ui.pages = ui.pages[:len(ui.pages)-1]
|
||||
|
||||
ui.redraw()
|
||||
func (ui *UI) movePrevPage() {
|
||||
ui.mountedPage.Unmount()
|
||||
ui.mountedPage = ui.pageHistory[len(ui.pageHistory)-1]
|
||||
ui.pageHistory = ui.pageHistory[:len(ui.pageHistory)-1]
|
||||
ui.triggerDraw()
|
||||
}
|
||||
|
||||
func (ui *UI) moveNextPage(page Primitive) {
|
||||
ui.pages = append(ui.pages, ui.activePage)
|
||||
ui.activePage = page
|
||||
ui.needInit = true
|
||||
|
||||
ui.redraw()
|
||||
ui.pageToMount = page
|
||||
ui.triggerDraw()
|
||||
}
|
||||
|
||||
func (ui *UI) redraw() {
|
||||
ui.app.QueueUpdateDraw(func() {})
|
||||
func (ui *UI) triggerDraw() {
|
||||
go ui.app.QueueUpdateDraw(func() {})
|
||||
}
|
||||
|
||||
func (ui *UI) Draw(screen tcell.Screen) {
|
||||
if ui.isLoading.Load() {
|
||||
ui.loadingScreen.Update(context.Background())
|
||||
ui.drawLoading(screen)
|
||||
ui.draw(screen)
|
||||
return
|
||||
}
|
||||
|
||||
if ui.needInit {
|
||||
ui.activePage.Mount(context.Background())
|
||||
ui.needInit = false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
ui.isLoading.Store(true)
|
||||
|
||||
ctx, cancel := context.WithCancel(ui.ctx)
|
||||
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer close(ready)
|
||||
|
||||
ui.activePage.Update(ctx)
|
||||
ui.load(ctx)
|
||||
ui.isLoading.Store(false)
|
||||
|
||||
if ui.loadingScreen != nil {
|
||||
ui.loadingScreen.Unmount()
|
||||
ui.loadingScreen = nil
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ready:
|
||||
ui.draw(screen)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
ui.loadingScreen = NewLoadingScreen(cancel, ui.redraw)
|
||||
ui.loadingScreen.Mount(ctx)
|
||||
ui.drawLoading(screen)
|
||||
ui.loadingBar.Start(ctx)
|
||||
ui.cancelLoading = cancel
|
||||
|
||||
go func() {
|
||||
<-ready
|
||||
ui.loadingBar.Stop()
|
||||
ui.triggerDraw()
|
||||
}()
|
||||
}
|
||||
|
||||
ui.draw(screen)
|
||||
}
|
||||
|
||||
func (ui *UI) drawLoading(screen tcell.Screen) {
|
||||
ui.DrawForSubclass(screen, ui)
|
||||
x, y, width, height := ui.GetInnerRect()
|
||||
func (ui *UI) load(ctx context.Context) {
|
||||
if ui.mountedPage == nil && ui.pageToMount == nil {
|
||||
return
|
||||
}
|
||||
// Pending mount either fails w/o retry or succeeds.
|
||||
// Either way the page to mount need to be reset.
|
||||
defer func() {
|
||||
ui.pageToMount = nil
|
||||
ui.isFirstMount = false
|
||||
}()
|
||||
|
||||
ui.activePage.SetRect(x, y, width, height-1)
|
||||
ui.activePage.Draw(screen)
|
||||
pageToUpdate := ui.mountedPage
|
||||
|
||||
if ui.loadingScreen != nil {
|
||||
ui.loadingScreen.SetRect(x, y+height-1, width, 1)
|
||||
ui.loadingScreen.Draw(screen)
|
||||
if ui.pageToMount != nil {
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ready)
|
||||
ui.pageToMount.Mount(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ready:
|
||||
}
|
||||
|
||||
pageToUpdate = ui.pageToMount
|
||||
}
|
||||
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ready)
|
||||
x, y, w, h := ui.GetInnerRect()
|
||||
|
||||
pageToUpdate.SetRect(x, y, w, h-1)
|
||||
|
||||
pageToUpdate.Update(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ready:
|
||||
}
|
||||
|
||||
if ui.pageToMount != nil {
|
||||
if ui.mountedPage != nil {
|
||||
ui.pageHistory = append(ui.pageHistory, ui.mountedPage)
|
||||
}
|
||||
ui.mountedPage = ui.pageToMount
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,34 +202,47 @@ func (ui *UI) draw(screen tcell.Screen) {
|
|||
ui.DrawForSubclass(screen, ui)
|
||||
x, y, width, height := ui.GetInnerRect()
|
||||
|
||||
if ui.isSearching {
|
||||
ui.activePage.SetRect(x, y, width, height-1)
|
||||
ui.activePage.Draw(screen)
|
||||
|
||||
ui.searchInput.SetRect(x, y+height-1, width, 1)
|
||||
ui.searchInput.Draw(screen)
|
||||
var pageToDraw tview.Primitive
|
||||
if ui.mountedPage != nil {
|
||||
pageToDraw = ui.mountedPage
|
||||
} else {
|
||||
ui.activePage.SetRect(x, y, width, height-1)
|
||||
ui.activePage.Draw(screen)
|
||||
|
||||
text := ui.helpView.GetText(true)
|
||||
|
||||
ui.helpView.SetRect(x, y+height-1, min(len(text), width), 1)
|
||||
ui.helpView.Draw(screen)
|
||||
pageToDraw = ui.pageStub
|
||||
}
|
||||
|
||||
pageToDraw.SetRect(x, y, width, height-1)
|
||||
pageToDraw.Draw(screen)
|
||||
|
||||
var barToDraw tview.Primitive
|
||||
switch {
|
||||
case ui.isLoading.Load():
|
||||
barToDraw = ui.loadingBar
|
||||
case ui.isSearching:
|
||||
barToDraw = ui.searchBar
|
||||
default:
|
||||
barToDraw = ui.infoBar
|
||||
}
|
||||
|
||||
barToDraw.SetRect(x, y+height-1, width, 1)
|
||||
barToDraw.Draw(screen)
|
||||
}
|
||||
|
||||
func (ui *UI) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
|
||||
return ui.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
|
||||
if ui.isLoading.Load() {
|
||||
ui.loadingScreen.InputHandler()(event, func(tview.Primitive) {})
|
||||
return
|
||||
k, r := event.Key(), event.Rune()
|
||||
if k != tcell.KeyEsc && r != 'q' {
|
||||
return
|
||||
}
|
||||
ui.cancelLoading()
|
||||
if r == 'q' || ui.isFirstMount {
|
||||
ui.app.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
if ui.isSearching {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEnter:
|
||||
res, err := processPrompt(ui.searchInput.GetText(), ui.filters)
|
||||
res, err := processPrompt(ui.searchBar.GetText()[1:], ui.filters)
|
||||
fmt.Fprintln(os.Stderr, res, err)
|
||||
|
||||
if err != nil {
|
||||
|
@ -205,58 +250,63 @@ func (ui *UI) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.P
|
|||
}
|
||||
|
||||
ui.isSearching = false
|
||||
ui.searchInput.SetText("")
|
||||
ui.searchBar.SetText("")
|
||||
case tcell.KeyEsc:
|
||||
ui.isSearching = false
|
||||
ui.searchInput.SetText("")
|
||||
ui.searchBar.SetText("")
|
||||
default:
|
||||
ui.searchInput.InputHandler()(event, func(tview.Primitive) {})
|
||||
if len(ui.searchInput.GetText()) == 0 {
|
||||
ui.searchBar.InputHandler()(event, func(tview.Primitive) {})
|
||||
if len(ui.searchBar.GetText()) == 0 {
|
||||
ui.isSearching = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch k, r := event.Key(), event.Rune(); {
|
||||
switch m, k, r := event.Modifiers(), event.Key(), event.Rune(); {
|
||||
case k == tcell.KeyRune && r == '/':
|
||||
ui.isSearching = true
|
||||
ui.searchInput.SetText("/")
|
||||
ui.searchBar.SetText("/")
|
||||
case k == tcell.KeyRune && r == 'q':
|
||||
ui.app.Stop()
|
||||
case m&tcell.ModCtrl != 0 && k == tcell.KeyLeft:
|
||||
if len(ui.pageHistory) != 0 {
|
||||
ui.movePrevPage()
|
||||
}
|
||||
default:
|
||||
ui.activePage.InputHandler()(event, func(tview.Primitive) {})
|
||||
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),
|
||||
) (map[string]any, error) {
|
||||
ans := make(map[string]any)
|
||||
|
||||
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, " ") {
|
||||
|
||||
for _, word := range strings.Split(prompt, "+") {
|
||||
word = strings.TrimSpace(word)
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ws := strings.Split(word, ":")
|
||||
if len(ws) != 2 {
|
||||
return nil, fmt.Errorf("invalid filter %s", word)
|
||||
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[ws[0]]
|
||||
f, ok := filters[filterKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown filter %s", word)
|
||||
return nil, fmt.Errorf("unknown filter '%s'", word)
|
||||
}
|
||||
res, err := f(ws[1])
|
||||
if err != nil {
|
||||
if m[filterKey], err = f(filterValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ans[ws[0]] = res
|
||||
}
|
||||
return ans, nil
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ func (a *Application) newRecordsView(ctx context.Context, bkt *Bucket) (tview.Pr
|
|||
defer close(records)
|
||||
|
||||
for record := range temp {
|
||||
res, _, err := bkt.NextHandler(record.key, record.value)
|
||||
res, _, err := bkt.NextHandler(record.Key, record.Value)
|
||||
// if errors.Is(err, handlers.ErrFilter) {
|
||||
// continue
|
||||
// }
|
||||
|
@ -32,7 +32,7 @@ func (a *Application) newRecordsView(ctx context.Context, bkt *Bucket) (tview.Pr
|
|||
// return
|
||||
}
|
||||
// record.nextHandler = next
|
||||
record.result = res
|
||||
record.Result = res
|
||||
records <- record
|
||||
}
|
||||
}()
|
||||
|
@ -114,7 +114,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].Result
|
||||
text := result.String()
|
||||
if index == v.selectedRecordIndex {
|
||||
text = fmt.Sprintf("[:white]%s[:black]", text)
|
||||
|
|
Loading…
Reference in a new issue