Added mode line

Also rescanning will maintain the current query
This commit is contained in:
Leon Mika 2022-06-22 11:57:12 +10:00
parent 54fab1b1c3
commit 809f9adfea
21 changed files with 197 additions and 72 deletions

View file

@ -43,3 +43,8 @@ func Confirm(prompt string, onYes func() tea.Cmd) tea.Cmd {
type MessageWithStatus interface {
StatusMessage() string
}
type MessageWithMode interface {
MessageWithStatus
ModeMessage() string
}

View file

@ -10,6 +10,9 @@ type ErrorMsg error
// Message indicates that a message should be shown to the user
type StatusMsg string
// ModeMessage indicates that the mode should be changed to the following
type ModeMessage string
// PromptForInput indicates that the context is requesting a line of input
type PromptForInputMsg struct {
Prompt string

View file

@ -1,18 +1,43 @@
package controllers
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/dynamo-browse/models"
)
type NewResultSet struct {
ResultSet *models.ResultSet
currentFilter string
filteredCount int
statusMessage string
}
func (rs NewResultSet) ModeMessage() string {
var modeLine string
if rs.ResultSet.Query != nil {
modeLine = rs.ResultSet.Query.String()
} else {
modeLine = "All results"
}
if rs.currentFilter != "" {
modeLine = fmt.Sprintf("%v - Filter: '%v'", modeLine, rs.currentFilter)
}
return modeLine
}
func (rs NewResultSet) StatusMessage() string {
//return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items()))
return rs.statusMessage
if rs.statusMessage != "" {
return rs.statusMessage
}
if rs.currentFilter != "" {
return fmt.Sprintf("%d of %d items returned", rs.filteredCount, len(rs.ResultSet.Items()))
} else {
return fmt.Sprintf("%d items returned", len(rs.ResultSet.Items()))
}
}
type SetReadWrite struct {

View file

@ -10,5 +10,5 @@ type TableReadService interface {
Describe(ctx context.Context, table string) (*models.TableInfo, error)
Scan(ctx context.Context, tableInfo *models.TableInfo) (*models.ResultSet, error)
Filter(resultSet *models.ResultSet, filter string) *models.ResultSet
ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, queryExpr string) (*models.ResultSet, error)
ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, query models.Queryable) (*models.ResultSet, error)
}

View file

@ -44,3 +44,19 @@ func (s *State) setResultSetAndFilter(resultSet *models.ResultSet, filter string
s.resultSet = resultSet
s.filter = filter
}
func (s *State) buildNewResultSetMessage(statusMessage string) NewResultSet {
s.mutex.Lock()
defer s.mutex.Unlock()
var filteredCount int = 0
if s.filter != "" {
for i := range s.resultSet.Items() {
if !s.resultSet.Hidden(i) {
filteredCount += 1
}
}
}
return NewResultSet{s.resultSet, s.filter, filteredCount, statusMessage}
}

View file

@ -3,10 +3,10 @@ package controllers
import (
"context"
"encoding/csv"
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/events"
"github.com/lmika/awstools/internal/dynamo-browse/models"
"github.com/lmika/awstools/internal/dynamo-browse/models/queryexpr"
"github.com/pkg/errors"
"os"
"sync"
@ -81,12 +81,20 @@ func (c *TableReadController) PromptForQuery() tea.Cmd {
Prompt: "query: ",
OnDone: func(value string) tea.Cmd {
if value == "" {
return c.Rescan()
return func() tea.Msg {
resultSet := c.state.ResultSet()
return c.doScan(context.Background(), resultSet, nil)
}
}
expr, err := queryexpr.Parse(value)
if err != nil {
return events.SetError(err)
}
return func() tea.Msg {
resultSet := c.state.ResultSet()
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, value)
newResultSet, err := c.tableService.ScanOrQuery(context.Background(), resultSet.TableInfo, expr)
if err != nil {
return events.Error(err)
}
@ -100,7 +108,8 @@ func (c *TableReadController) PromptForQuery() tea.Cmd {
func (c *TableReadController) Rescan() tea.Cmd {
return func() tea.Msg {
return c.doScan(context.Background(), c.state.ResultSet())
resultSet := c.state.ResultSet()
return c.doScan(context.Background(), resultSet, resultSet.Query)
}
}
@ -139,8 +148,8 @@ func (c *TableReadController) ExportCSV(filename string) tea.Cmd {
}
}
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet) tea.Msg {
newResultSet, err := c.tableService.Scan(ctx, resultSet.TableInfo)
func (c *TableReadController) doScan(ctx context.Context, resultSet *models.ResultSet, query models.Queryable) tea.Msg {
newResultSet, err := c.tableService.ScanOrQuery(ctx, resultSet.TableInfo, query)
if err != nil {
return events.Error(err)
}
@ -152,21 +161,7 @@ func (c *TableReadController) doScan(ctx context.Context, resultSet *models.Resu
func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string) tea.Msg {
c.state.setResultSetAndFilter(resultSet, filter)
var statusMessage string
if filter != "" {
var filteredCount int
for i := range resultSet.Items() {
if !resultSet.Hidden(i) {
filteredCount += 1
}
}
statusMessage = fmt.Sprintf("%d of %d items returned", filteredCount, len(resultSet.Items()))
} else {
statusMessage = fmt.Sprintf("%d items returned", len(resultSet.Items()))
}
return NewResultSet{resultSet, statusMessage}
return c.state.buildNewResultSetMessage("")
}
func (c *TableReadController) Unmark() tea.Cmd {

View file

@ -61,7 +61,7 @@ func (twc *TableWriteController) NewItem() tea.Cmd {
Dirty: true,
})
})
return NewResultSet{twc.state.ResultSet(), "New item added"}
return twc.state.buildNewResultSetMessage("New item added")
}
return keyPrompts.next()
@ -161,7 +161,7 @@ func (twc *TableWriteController) NoisyTouchItem(idx int) tea.Cmd {
return events.Error(err)
}
return twc.tableReadControllers.doScan(ctx, resultSet)
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
}
},
}
@ -190,7 +190,7 @@ func (twc *TableWriteController) DeleteMarked() tea.Cmd {
return events.Error(err)
}
return twc.tableReadControllers.doScan(ctx, resultSet)
return twc.tableReadControllers.doScan(ctx, resultSet, resultSet.Query)
}
},
}

View file

@ -2,11 +2,17 @@ package models
type ResultSet struct {
TableInfo *TableInfo
Query Queryable
Columns []string
items []Item
attributes []ItemAttribute
}
type Queryable interface {
String() string
Plan(tableInfo *TableInfo) (*QueryExecutionPlan, error)
}
type ItemAttribute struct {
Marked bool
Hidden bool

View file

@ -16,7 +16,7 @@ type astBinOp struct {
}
type astLiteralValue struct {
String string `parser:"@String"`
StringVal string `parser:"@String"`
}
var parser = participle.MustBuild(&astExpr{})

View file

@ -6,6 +6,10 @@ type QueryExpr struct {
ast *astExpr
}
func (md *QueryExpr) BuildQuery(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
func (md *QueryExpr) Plan(tableInfo *models.TableInfo) (*models.QueryExecutionPlan, error) {
return md.ast.calcQuery(tableInfo)
}
func (md *QueryExpr) String() string {
return md.ast.String()
}

View file

@ -0,0 +1,13 @@
package queryexpr
func (a *astExpr) String() string {
return a.Equality.String()
}
func (a *astBinOp) String() string {
return a.Name + a.Op + a.Value.String()
}
func (a *astLiteralValue) String() string {
return a.StringVal
}

View file

@ -8,7 +8,7 @@ import (
)
func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) {
s, err := strconv.Unquote(a.String)
s, err := strconv.Unquote(a.StringVal)
if err != nil {
return nil, errors.Wrap(err, "cannot unquote string")
}
@ -16,7 +16,7 @@ func (a *astLiteralValue) dynamoValue() (types.AttributeValue, error) {
}
func (a *astLiteralValue) goValue() (any, error) {
s, err := strconv.Unquote(a.String)
s, err := strconv.Unquote(a.StringVal)
if err != nil {
return nil, errors.Wrap(err, "cannot unquote string")
}

View file

@ -3,7 +3,6 @@ package tables
import (
"context"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
"github.com/lmika/awstools/internal/dynamo-browse/models/queryexpr"
"sort"
"strings"
@ -33,7 +32,23 @@ func (s *Service) Scan(ctx context.Context, tableInfo *models.TableInfo) (*model
return s.doScan(ctx, tableInfo, nil)
}
func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, filterExpr *expression.Expression) (*models.ResultSet, error) {
func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) {
var filterExpr *expression.Expression
if expr != nil {
plan, err := expr.Plan(tableInfo)
if err != nil {
return nil, err
}
// TEMP
if plan.CanQuery {
return nil, errors.Errorf("queries not yet supported")
}
filterExpr = &plan.Expression
}
results, err := s.provider.ScanItems(ctx, tableInfo.Name, filterExpr, 1000)
if err != nil {
return nil, errors.Wrapf(err, "unable to scan table %v", tableInfo.Name)
@ -76,6 +91,7 @@ func (s *Service) doScan(ctx context.Context, tableInfo *models.TableInfo, filte
resultSet := &models.ResultSet{
TableInfo: tableInfo,
Query: expr,
Columns: columns,
}
resultSet.SetItems(results)
@ -107,23 +123,8 @@ func (s *Service) Delete(ctx context.Context, tableInfo *models.TableInfo, items
return nil
}
func (s *Service) ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, queryExpr string) (*models.ResultSet, error) {
expr, err := queryexpr.Parse(queryExpr)
if err != nil {
return nil, err
}
plan, err := expr.BuildQuery(tableInfo)
if err != nil {
return nil, err
}
// TEMP
if plan.CanQuery {
return nil, errors.Errorf("queries not yet supported")
}
return s.doScan(ctx, tableInfo, &plan.Expression)
func (s *Service) ScanOrQuery(ctx context.Context, tableInfo *models.TableInfo, expr models.Queryable) (*models.ResultSet, error) {
return s.doScan(ctx, tableInfo, expr)
}
// TODO: move into a new service

View file

@ -9,6 +9,7 @@ import (
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamotableview"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/tableselect"
"github.com/pkg/errors"
)
@ -25,10 +26,12 @@ type Model struct {
}
func NewModel(rc *controllers.TableReadController, wc *controllers.TableWriteController, cc *commandctrl.CommandController) Model {
dtv := dynamotableview.New()
div := dynamoitemview.New()
statusAndPrompt := statusandprompt.New(layout.NewVBox(layout.LastChildFixedAt(17), dtv, div), "")
tableSelect := tableselect.New(statusAndPrompt)
uiStyles := styles.DefaultStyles
dtv := dynamotableview.New(uiStyles)
div := dynamoitemview.New(uiStyles)
statusAndPrompt := statusandprompt.New(layout.NewVBox(layout.LastChildFixedAt(17), dtv, div), "", uiStyles.StatusAndPrompt)
tableSelect := tableselect.New(statusAndPrompt, uiStyles)
cc.AddCommands(&commandctrl.CommandContext{
Commands: map[string]commandctrl.Command{

View file

@ -3,6 +3,7 @@ package dynamoitemview
import (
"fmt"
"github.com/lmika/awstools/internal/dynamo-browse/models/itemrender"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
"io"
"strings"
"text/tabwriter"
@ -38,9 +39,9 @@ type Model struct {
selectedItem models.Item
}
func New() *Model {
func New(uiStyles styles.Styles) *Model {
return &Model{
frameTitle: frame.NewFrameTitle("Item", false, activeHeaderStyle),
frameTitle: frame.NewFrameTitle("Item", false, uiStyles.Frames),
viewport: viewport.New(100, 100),
}
}

View file

@ -9,6 +9,7 @@ import (
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/dynamoitemview"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
table "github.com/lmika/go-bubble-table"
)
@ -54,12 +55,12 @@ func (cm columnModel) Header(index int) string {
return cm.m.resultSet.Columns[cm.m.colOffset+index]
}
func New() *Model {
func New(uiStyles styles.Styles) *Model {
tbl := table.New(table.SimpleColumns([]string{"pk", "sk"}), 100, 100)
rows := make([]table.Row, 0)
tbl.SetRows(rows)
frameTitle := frame.NewFrameTitle("No table", true, activeHeaderStyle)
frameTitle := frame.NewFrameTitle("No table", true, uiStyles.Frames)
return &Model{
frameTitle: frameTitle,

View file

@ -15,14 +15,19 @@ var (
// Frame is a frame that appears in the
type FrameTitle struct {
header string
active bool
activeStyle lipgloss.Style
width int
header string
active bool
style Style
width int
}
func NewFrameTitle(header string, active bool, activeStyle lipgloss.Style) FrameTitle {
return FrameTitle{header, active, activeStyle, 0}
type Style struct {
ActiveTitle lipgloss.Style
InactiveTitle lipgloss.Style
}
func NewFrameTitle(header string, active bool, style Style) FrameTitle {
return FrameTitle{header, active, style, 0}
}
func (f *FrameTitle) SetTitle(title string) {
@ -42,9 +47,9 @@ func (f FrameTitle) HeaderHeight() int {
}
func (f FrameTitle) headerView() string {
style := inactiveHeaderStyle
style := f.style.InactiveTitle
if f.active {
style = f.activeStyle
style = f.style.ActiveTitle
}
titleText := f.header

View file

@ -12,15 +12,21 @@ import (
// event is received, focus will be torn away and the user will be given a prompt the enter text.
type StatusAndPrompt struct {
model layout.ResizingModel
style Style
modeLine string
statusMessage string
pendingInput *events.PromptForInputMsg
textInput textinput.Model
width int
}
func New(model layout.ResizingModel, initialMsg string) *StatusAndPrompt {
type Style struct {
ModeLine lipgloss.Style
}
func New(model layout.ResizingModel, initialMsg string, style Style) *StatusAndPrompt {
textInput := textinput.New()
return &StatusAndPrompt{model: model, statusMessage: initialMsg, textInput: textInput}
return &StatusAndPrompt{model: model, style: style, statusMessage: initialMsg, modeLine: "", textInput: textInput}
}
func (s *StatusAndPrompt) Init() tea.Cmd {
@ -33,7 +39,12 @@ func (s *StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.statusMessage = "Error: " + msg.Error()
case events.StatusMsg:
s.statusMessage = string(msg)
case events.ModeMessage:
s.modeLine = string(msg)
case events.MessageWithStatus:
if hasModeMessage, ok := msg.(events.MessageWithMode); ok {
s.modeLine = hasModeMessage.ModeMessage()
}
s.statusMessage = msg.StatusMessage()
case events.PromptForInputMsg:
if s.pendingInput != nil {
@ -87,8 +98,14 @@ func (s *StatusAndPrompt) Resize(w, h int) layout.ResizingModel {
}
func (s *StatusAndPrompt) viewStatus() string {
modeLine := s.style.ModeLine.Render(lipgloss.PlaceHorizontal(s.width, lipgloss.Left, s.modeLine, lipgloss.WithWhitespaceChars(" ")))
var statusLine string
if s.pendingInput != nil {
return s.textInput.View()
statusLine = s.textInput.View()
} else {
statusLine = s.statusMessage
}
return s.statusMessage
return lipgloss.JoinVertical(lipgloss.Top, modeLine, statusLine)
}

View file

@ -0,0 +1,29 @@
package styles
import (
"github.com/charmbracelet/lipgloss"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
)
type Styles struct {
Frames frame.Style
StatusAndPrompt statusandprompt.Style
}
var DefaultStyles = Styles{
Frames: frame.Style{
ActiveTitle: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#ffffff")).
Background(lipgloss.Color("#4479ff")),
InactiveTitle: lipgloss.NewStyle().
Foreground(lipgloss.Color("#000000")).
Background(lipgloss.Color("#d1d1d1")),
},
StatusAndPrompt: statusandprompt.Style{
ModeLine: lipgloss.NewStyle().
Foreground(lipgloss.Color("#000000")).
Background(lipgloss.Color("#d1d1d1")),
},
}

View file

@ -7,6 +7,7 @@ import (
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/styles"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
)
@ -26,8 +27,8 @@ type Model struct {
w, h int
}
func New(submodel tea.Model) *Model {
frameTitle := frame.NewFrameTitle("Select table", false, activeHeaderStyle)
func New(submodel tea.Model, uiStyles styles.Styles) *Model {
frameTitle := frame.NewFrameTitle("Select table", false, uiStyles.Frames)
return &Model{frameTitle: frameTitle, submodel: submodel}
}