[#1223] lens/tui: Add error message on invalid search prompt
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
This commit is contained in:
parent
235cc15037
commit
a3e0380e43
3 changed files with 251 additions and 140 deletions
|
@ -2,6 +2,7 @@ package tuiutil
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/metabase"
|
||||
|
@ -35,6 +36,10 @@ func NewBucketsView(ui *UI, filter *Filter) *BucketsView {
|
|||
}
|
||||
|
||||
func (v *BucketsView) Mount(ctx context.Context) error {
|
||||
if v.onUnmount != nil {
|
||||
return errors.New("try to mount already mounted component")
|
||||
}
|
||||
|
||||
ctx, v.onUnmount = context.WithCancel(ctx)
|
||||
|
||||
handler := metabase.MetabaseParser
|
||||
|
|
|
@ -2,6 +2,7 @@ package tuiutil
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
|
||||
|
@ -50,6 +51,10 @@ func NewRecordsView(ui *UI, bucket *Bucket, filter *Filter) *RecordsView {
|
|||
}
|
||||
|
||||
func (v *RecordsView) Mount(ctx context.Context) error {
|
||||
if v.onUnmount != nil {
|
||||
return errors.New("try to mount already mounted component")
|
||||
}
|
||||
|
||||
ctx, v.onUnmount = context.WithCancel(ctx)
|
||||
|
||||
tempBuffer, err := LoadRecords(ctx, v.ui.db, v.bucket.Path)
|
||||
|
|
|
@ -27,9 +27,9 @@ 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.
|
||||
// Need to use context while updating pages those read data from a database.
|
||||
// Context should be shared among all mount and updates. Current TUI library
|
||||
// doesn't use contexts at all, so I do that feature by myself.
|
||||
//nolint:containedctx
|
||||
ctx context.Context
|
||||
|
||||
|
@ -38,16 +38,27 @@ type UI struct {
|
|||
|
||||
pageHistory []Primitive
|
||||
mountedPage Primitive
|
||||
|
||||
pageToMount Primitive
|
||||
pageStub tview.Primitive
|
||||
saveMounted bool
|
||||
|
||||
pageStub tview.Primitive
|
||||
|
||||
infoBar *tview.TextView
|
||||
searchBar *tview.InputField
|
||||
loadingBar *LoadingBar
|
||||
|
||||
isFirstMount bool
|
||||
isSearching bool
|
||||
isLoading atomic.Bool
|
||||
helpPage *tview.TextView
|
||||
|
||||
searchErrorBar *tview.TextView
|
||||
|
||||
isFirstMount bool
|
||||
isSearching bool
|
||||
isLoading atomic.Bool
|
||||
isShowingError bool
|
||||
isShowingHelp bool
|
||||
|
||||
loadingBarDelay time.Duration
|
||||
|
||||
cancelLoading func()
|
||||
|
||||
|
@ -66,21 +77,35 @@ func NewUI(ctx context.Context, app *tview.Application, db *bbolt.DB) *UI {
|
|||
isFirstMount: true,
|
||||
infoBar: tview.NewTextView(),
|
||||
pageStub: tview.NewBox(),
|
||||
helpPage: tview.NewTextView(),
|
||||
searchBar: tview.NewInputField(),
|
||||
searchErrorBar: tview.NewTextView(),
|
||||
filters: make(map[string]func(string) (any, error)),
|
||||
compositeFilters: make(map[string]func(string) (map[string]any, error)),
|
||||
saveMounted: true,
|
||||
loadingBarDelay: 100 * time.Millisecond,
|
||||
}
|
||||
|
||||
ui.loadingBar = NewLoadingBar(ui.triggerDraw)
|
||||
barBackgroundColor := tview.Styles.PrimaryTextColor
|
||||
barTextColor := ui.GetBackgroundColor()
|
||||
barAlertTextColor := tcell.ColorRed
|
||||
|
||||
ui.searchBar.SetFieldBackgroundColor(barTextColor)
|
||||
ui.searchBar.SetFieldTextColor(barBackgroundColor)
|
||||
ui.searchBar.SetLabelColor(barBackgroundColor)
|
||||
|
||||
ui.infoBar.SetBackgroundColor(barBackgroundColor)
|
||||
ui.infoBar.SetTextColor(barTextColor)
|
||||
|
||||
ui.searchErrorBar.SetBackgroundColor(barTextColor)
|
||||
ui.searchErrorBar.SetTextColor(barAlertTextColor)
|
||||
|
||||
ui.loadingBar = NewLoadingBar(ui.triggerDraw)
|
||||
ui.pageToMount = NewBucketsView(ui, NewFilter(nil))
|
||||
|
||||
ui.searchBar.SetFieldBackgroundColor(ui.GetBackgroundColor())
|
||||
ui.searchBar.SetFieldTextColor(tview.Styles.PrimaryTextColor)
|
||||
|
||||
ui.infoBar.SetBackgroundColor(tview.Styles.PrimaryTextColor)
|
||||
ui.infoBar.SetTextColor(ui.GetBackgroundColor())
|
||||
ui.helpPage.SetText("Work In Progress【ツ】")
|
||||
|
||||
ui.searchBar.SetLabel("/")
|
||||
ui.infoBar.SetText(fmt.Sprintf(" %s (press h for help, / to search or q to quit) ", db.Path()))
|
||||
|
||||
return ui
|
||||
|
@ -118,18 +143,20 @@ func (ui *UI) AddCompositeFilter(
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: enhance error handling
|
||||
// func (ui *UI) stopOnError(err error) {
|
||||
// if err != nil {
|
||||
// ui.app.QueueEvent(tcell.NewEventError(err))
|
||||
// }
|
||||
// }
|
||||
// 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]
|
||||
ui.pageHistory = ui.pageHistory[:len(ui.pageHistory)-1]
|
||||
ui.triggerDraw()
|
||||
if len(ui.pageHistory) != 0 {
|
||||
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) {
|
||||
|
@ -153,16 +180,16 @@ func (ui *UI) Draw(screen tcell.Screen) {
|
|||
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer close(ready)
|
||||
|
||||
ui.load(ctx)
|
||||
|
||||
cancel()
|
||||
close(ready)
|
||||
ui.isLoading.Store(false)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ready:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-time.After(ui.loadingBarDelay):
|
||||
ui.loadingBar.Start(ctx)
|
||||
ui.cancelLoading = cancel
|
||||
|
||||
|
@ -180,54 +207,11 @@ 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
|
||||
}()
|
||||
|
||||
pageToUpdate := ui.mountedPage
|
||||
|
||||
if ui.pageToMount != nil {
|
||||
ready := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ready)
|
||||
// TODO: add error handling
|
||||
_ = 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)
|
||||
|
||||
// TODO: add error handling
|
||||
_ = 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
|
||||
ui.mountAndUpdate(ctx)
|
||||
} else {
|
||||
ui.update(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,22 +219,39 @@ func (ui *UI) draw(screen tcell.Screen) {
|
|||
ui.DrawForSubclass(screen, ui)
|
||||
x, y, width, height := ui.GetInnerRect()
|
||||
|
||||
var pageToDraw tview.Primitive
|
||||
if ui.mountedPage != nil {
|
||||
var (
|
||||
pageToDraw tview.Primitive
|
||||
barToDraw tview.Primitive
|
||||
|
||||
needDrawBar = true
|
||||
)
|
||||
|
||||
switch {
|
||||
case ui.isShowingHelp:
|
||||
pageToDraw = ui.helpPage
|
||||
needDrawBar = false
|
||||
case ui.mountedPage != nil:
|
||||
pageToDraw = ui.mountedPage
|
||||
} else {
|
||||
default:
|
||||
pageToDraw = ui.pageStub
|
||||
}
|
||||
|
||||
if !needDrawBar {
|
||||
pageToDraw.SetRect(x, y, width, height)
|
||||
pageToDraw.Draw(screen)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
case ui.isShowingError:
|
||||
barToDraw = ui.searchErrorBar
|
||||
default:
|
||||
barToDraw = ui.infoBar
|
||||
}
|
||||
|
@ -259,76 +260,179 @@ func (ui *UI) draw(screen tcell.Screen) {
|
|||
barToDraw.Draw(screen)
|
||||
}
|
||||
|
||||
func (ui *UI) mountAndUpdate(ctx context.Context) {
|
||||
defer func() {
|
||||
// Operation succeeded or was canceled, either way reset page to mount.
|
||||
ui.pageToMount = nil
|
||||
|
||||
// Not saving previously mounted page in history on new mount is
|
||||
// exceptional, we always want to have previously mounted page saved.
|
||||
ui.saveMounted = true
|
||||
}()
|
||||
|
||||
s := loadOp(ctx, ui.pageToMount.Mount)
|
||||
if s.err != nil {
|
||||
ui.stopOnError(s.err)
|
||||
return
|
||||
}
|
||||
// Mount canceled.
|
||||
if !s.done {
|
||||
return
|
||||
}
|
||||
|
||||
s = loadOp(ctx, ui.pageToMount.Update)
|
||||
if s.err != nil {
|
||||
ui.stopOnError(s.err)
|
||||
return
|
||||
}
|
||||
// Update canceled.
|
||||
if !s.done {
|
||||
ui.pageToMount.Unmount()
|
||||
return
|
||||
}
|
||||
|
||||
if ui.saveMounted {
|
||||
ui.saveOnMount()
|
||||
}
|
||||
ui.swapOnMount()
|
||||
}
|
||||
|
||||
func (ui *UI) update(ctx context.Context) {
|
||||
s := loadOp(ctx, ui.mountedPage.Update)
|
||||
if s.err != nil {
|
||||
ui.stopOnError(s.err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type status struct {
|
||||
done bool
|
||||
err error
|
||||
}
|
||||
|
||||
func loadOp(ctx context.Context, op func(ctx context.Context) error) status {
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
errCh <- op(ctx)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return status{done: false, err: nil}
|
||||
case err := <-errCh:
|
||||
return status{done: true, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UI) saveOnMount() {
|
||||
if ui.mountedPage != nil {
|
||||
ui.pageHistory = append(ui.pageHistory, ui.mountedPage)
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UI) swapOnMount() {
|
||||
ui.mountedPage = ui.pageToMount
|
||||
ui.pageToMount = nil
|
||||
}
|
||||
|
||||
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() {
|
||||
if k != tcell.KeyEsc && r != 'q' {
|
||||
return
|
||||
}
|
||||
ui.cancelLoading()
|
||||
if r == 'q' || ui.isFirstMount {
|
||||
ui.app.Stop()
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case ui.isLoading.Load():
|
||||
ui.handleInputOnLoading(event)
|
||||
case ui.isShowingHelp:
|
||||
ui.handleInputOnShowingHelp(event)
|
||||
case ui.isShowingError:
|
||||
ui.handleInputOnShowingError()
|
||||
case ui.isSearching:
|
||||
ui.handleInputOnSearching(event)
|
||||
default:
|
||||
ui.handleInput(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ui.isSearching {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEnter:
|
||||
prompt := ui.searchBar.GetText()[1:]
|
||||
func (ui *UI) handleInput(event *tcell.EventKey) {
|
||||
m, k, r := event.Modifiers(), event.Key(), event.Rune()
|
||||
|
||||
res, err := ui.processPrompt(prompt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case k == tcell.KeyEsc:
|
||||
ui.movePrevPage()
|
||||
case m == 0 && k == tcell.KeyRune && r == 'h':
|
||||
ui.isShowingHelp = true
|
||||
case m == 0 && k == tcell.KeyRune && r == '/':
|
||||
ui.isSearching = true
|
||||
case m == 0 && k == tcell.KeyRune && r == 'q':
|
||||
ui.app.Stop()
|
||||
default:
|
||||
if ui.mountedPage != nil {
|
||||
ui.mountedPage.InputHandler()(event, func(tview.Primitive) {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (ui *UI) handleInputOnLoading(event *tcell.EventKey) {
|
||||
k, r := event.Key(), event.Rune()
|
||||
if k != tcell.KeyEsc && r != 'q' {
|
||||
return
|
||||
}
|
||||
ui.cancelLoading()
|
||||
if r == 'q' || ui.isFirstMount {
|
||||
ui.app.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ui.mountedPage.Unmount()
|
||||
// ui.mountedPage = nil
|
||||
func (ui *UI) handleInputOnShowingError() {
|
||||
ui.isShowingError = false
|
||||
ui.isSearching = true
|
||||
}
|
||||
|
||||
ui.moveNextPage(NewRecordsView(ui, bucket, res))
|
||||
}
|
||||
func (ui *UI) handleInputOnShowingHelp(event *tcell.EventKey) {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEsc:
|
||||
ui.isShowingHelp = false
|
||||
default:
|
||||
ui.helpPage.InputHandler()(event, func(tview.Primitive) {})
|
||||
}
|
||||
}
|
||||
|
||||
ui.isSearching = false
|
||||
ui.searchBar.SetText("")
|
||||
case tcell.KeyEsc:
|
||||
ui.isSearching = false
|
||||
ui.searchBar.SetText("")
|
||||
default:
|
||||
ui.searchBar.InputHandler()(event, func(tview.Primitive) {})
|
||||
if len(ui.searchBar.GetText()) == 0 {
|
||||
ui.isSearching = false
|
||||
}
|
||||
}
|
||||
func (ui *UI) handleInputOnSearching(event *tcell.EventKey) {
|
||||
m, k := event.Modifiers(), event.Key()
|
||||
|
||||
switch {
|
||||
case k == tcell.KeyEnter:
|
||||
prompt := ui.searchBar.GetText()
|
||||
|
||||
res, err := ui.processPrompt(prompt)
|
||||
if err != nil {
|
||||
ui.isShowingError = true
|
||||
ui.isSearching = false
|
||||
ui.searchErrorBar.SetText(err.Error() + " (press any key to continue)")
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case m == 0 && k == tcell.KeyRune && r == '/':
|
||||
ui.isSearching = true
|
||||
ui.searchBar.SetText("/")
|
||||
case m == 0 && k == tcell.KeyRune && r == 'q':
|
||||
ui.app.Stop()
|
||||
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) {})
|
||||
}
|
||||
// TODO: make two mount strategies.
|
||||
switch ui.mountedPage.(type) {
|
||||
case *BucketsView:
|
||||
ui.saveMounted = false
|
||||
ui.moveNextPage(NewBucketsView(ui, res))
|
||||
case *RecordsView:
|
||||
bucket := ui.mountedPage.(*RecordsView).bucket
|
||||
ui.saveMounted = false
|
||||
ui.moveNextPage(NewRecordsView(ui, bucket, res))
|
||||
}
|
||||
})
|
||||
|
||||
ui.isSearching = false
|
||||
ui.searchBar.SetText("")
|
||||
case k == tcell.KeyEsc:
|
||||
ui.isSearching = false
|
||||
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) {})
|
||||
}
|
||||
}
|
||||
|
||||
func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
|
||||
|
@ -338,7 +442,7 @@ func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
|
|||
|
||||
parts := strings.Split(prompt, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("invalid filter syntax")
|
||||
return nil, errors.New("invalid syntax, expected <filter id>:<filter value>")
|
||||
}
|
||||
|
||||
filterID := strings.TrimSpace(parts[0])
|
||||
|
@ -348,10 +452,7 @@ func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
|
|||
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 nil, fmt.Errorf("can't parse '%s' filter value: %w", filterID, err)
|
||||
}
|
||||
return NewFilter(map[string]any{filterID: filterValue}), nil
|
||||
}
|
||||
|
@ -368,5 +469,5 @@ func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
|
|||
return NewFilter(filterValue), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown filter '%s'", filterID)
|
||||
return nil, fmt.Errorf("unknown filter id '%s'", filterID)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue