Added the rel-picker which can quickly goto related tables
* New rel-picker that can be opened using Shift+O and allows for quickly going to related tables.
This commit is contained in:
parent
12909c89ee
commit
5d95d44a97
|
@ -4,6 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
@ -30,9 +34,6 @@ import (
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
"github.com/lmika/gopkgs/cli"
|
"github.com/lmika/gopkgs/cli"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -118,6 +119,7 @@ func main() {
|
||||||
inputHistoryService,
|
inputHistoryService,
|
||||||
eventBus,
|
eventBus,
|
||||||
pasteboardProvider,
|
pasteboardProvider,
|
||||||
|
scriptManagerService,
|
||||||
*flagTable,
|
*flagTable,
|
||||||
)
|
)
|
||||||
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
tableWriteController := controllers.NewTableWriteController(state, tableService, jobsController, tableReadController, settingStore)
|
||||||
|
@ -125,7 +127,7 @@ func main() {
|
||||||
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
|
exportController := controllers.NewExportController(state, tableService, jobsController, columnsController, pasteboardProvider)
|
||||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||||
keyBindings := keybindings.Default()
|
keyBindings := keybindings.Default()
|
||||||
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, settingsController, eventBus)
|
scriptController := controllers.NewScriptController(scriptManagerService, tableReadController, jobsController, settingsController, eventBus)
|
||||||
|
|
||||||
if *flagQuery != "" {
|
if *flagQuery != "" {
|
||||||
if *flagTable == "" {
|
if *flagTable == "" {
|
||||||
|
|
|
@ -2,10 +2,12 @@ package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SetTableItemView struct {
|
type SetTableItemView struct {
|
||||||
|
@ -89,3 +91,9 @@ func (rs ResultSetUpdated) StatusMessage() string {
|
||||||
|
|
||||||
type ShowColumnOverlay struct{}
|
type ShowColumnOverlay struct{}
|
||||||
type HideColumnOverlay struct{}
|
type HideColumnOverlay struct{}
|
||||||
|
|
||||||
|
type ShowRelatedItemsOverlay struct {
|
||||||
|
Items []relitems.RelatedItem
|
||||||
|
OnSelected func(item relitems.RelatedItem) tea.Msg
|
||||||
|
}
|
||||||
|
type HideRelatedItemsOverlay struct{}
|
||||||
|
|
|
@ -2,10 +2,12 @@ package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
"io/fs"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TableReadService interface {
|
type TableReadService interface {
|
||||||
|
@ -33,3 +35,7 @@ type CustomKeyBindingSource interface {
|
||||||
UnbindKey(key string)
|
UnbindKey(key string)
|
||||||
Rebind(bindingName string, newKey string) error
|
Rebind(bindingName string, newKey string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RelatedItemSupplier interface {
|
||||||
|
RelatedItemOfItem(context.Context, *models.ResultSet, int) ([]relitems.RelatedItem, error)
|
||||||
|
}
|
||||||
|
|
|
@ -3,21 +3,24 @@ package controllers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ScriptController struct {
|
type ScriptController struct {
|
||||||
scriptManager *scriptmanager.Service
|
scriptManager *scriptmanager.Service
|
||||||
tableReadController *TableReadController
|
tableReadController *TableReadController
|
||||||
|
jobController *JobsController
|
||||||
settingsController *SettingsController
|
settingsController *SettingsController
|
||||||
eventBus *bus.Bus
|
eventBus *bus.Bus
|
||||||
sendMsg func(msg tea.Msg)
|
sendMsg func(msg tea.Msg)
|
||||||
|
@ -26,12 +29,14 @@ type ScriptController struct {
|
||||||
func NewScriptController(
|
func NewScriptController(
|
||||||
scriptManager *scriptmanager.Service,
|
scriptManager *scriptmanager.Service,
|
||||||
tableReadController *TableReadController,
|
tableReadController *TableReadController,
|
||||||
|
jobController *JobsController,
|
||||||
settingsController *SettingsController,
|
settingsController *SettingsController,
|
||||||
eventBus *bus.Bus,
|
eventBus *bus.Bus,
|
||||||
) *ScriptController {
|
) *ScriptController {
|
||||||
sc := &ScriptController{
|
sc := &ScriptController{
|
||||||
scriptManager: scriptManager,
|
scriptManager: scriptManager,
|
||||||
tableReadController: tableReadController,
|
tableReadController: tableReadController,
|
||||||
|
jobController: jobController,
|
||||||
settingsController: settingsController,
|
settingsController: settingsController,
|
||||||
eventBus: eventBus,
|
eventBus: eventBus,
|
||||||
}
|
}
|
||||||
|
@ -162,7 +167,6 @@ func (s *sessionImpl) SetResultSet(ctx context.Context, newResultSet *models.Res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||||
|
|
||||||
// Parse the query
|
// Parse the query
|
||||||
expr, err := queryexpr.Parse(query)
|
expr, err := queryexpr.Parse(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -179,11 +183,18 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
||||||
expr = expr.WithIndex(opts.IndexName)
|
expr = expr.WithIndex(opts.IndexName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s.sc.doQuery(ctx, expr, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ScriptController) doQuery(ctx context.Context, expr *queryexpr.QueryExpr, opts scriptmanager.QueryOptions) (*models.ResultSet, error) {
|
||||||
// Get the table info
|
// Get the table info
|
||||||
var tableInfo *models.TableInfo
|
var (
|
||||||
|
tableInfo *models.TableInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
tableName := opts.TableName
|
tableName := opts.TableName
|
||||||
currentResultSet := s.sc.tableReadController.state.ResultSet()
|
currentResultSet := s.tableReadController.state.ResultSet()
|
||||||
|
|
||||||
if tableName != "" {
|
if tableName != "" {
|
||||||
// Table specified. If it's the same as the current table, then use the existing table info
|
// Table specified. If it's the same as the current table, then use the existing table info
|
||||||
|
@ -192,7 +203,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, describe the table
|
// Otherwise, describe the table
|
||||||
tableInfo, err = s.sc.tableReadController.tableService.Describe(ctx, tableName)
|
tableInfo, err = s.tableReadController.tableService.Describe(ctx, tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName)
|
return nil, errors.Wrapf(err, "cannot describe table '%v'", tableName)
|
||||||
}
|
}
|
||||||
|
@ -204,7 +215,7 @@ func (s *sessionImpl) Query(ctx context.Context, query string, opts scriptmanage
|
||||||
tableInfo = currentResultSet.TableInfo
|
tableInfo = currentResultSet.TableInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
newResultSet, err := s.sc.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
newResultSet, err := s.tableReadController.tableService.ScanOrQuery(ctx, tableInfo, expr, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -240,3 +251,31 @@ func (sc *ScriptController) LookupBinding(theKey string) string {
|
||||||
func (sc *ScriptController) UnbindKey(key string) {
|
func (sc *ScriptController) UnbindKey(key string) {
|
||||||
sc.scriptManager.UnbindKey(key)
|
sc.scriptManager.UnbindKey(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ScriptController) LookupRelatedItems(idx int) (res tea.Msg) {
|
||||||
|
rs := c.tableReadController.state.ResultSet()
|
||||||
|
|
||||||
|
relItems, err := c.scriptManager.RelatedItemOfItem(context.Background(), rs, idx)
|
||||||
|
if err != nil {
|
||||||
|
return events.Error(err)
|
||||||
|
} else if len(relItems) == 0 {
|
||||||
|
return events.StatusMsg("No related items available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShowRelatedItemsOverlay{
|
||||||
|
Items: relItems,
|
||||||
|
OnSelected: func(item relitems.RelatedItem) tea.Msg {
|
||||||
|
if item.OnSelect != nil {
|
||||||
|
return item.OnSelect()
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
|
||||||
|
return c.doQuery(ctx, item.Query, scriptmanager.QueryOptions{
|
||||||
|
TableName: item.Table,
|
||||||
|
})
|
||||||
|
}).OnDone(func(rs *models.ResultSet) tea.Msg {
|
||||||
|
return c.tableReadController.setResultSetAndFilter(rs, "", true, resultSetUpdateQuery)
|
||||||
|
}).Submit()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ type TableReadController struct {
|
||||||
tableName string
|
tableName string
|
||||||
loadFromLastView bool
|
loadFromLastView bool
|
||||||
pasteboardProvider services.PasteboardProvider
|
pasteboardProvider services.PasteboardProvider
|
||||||
|
relatedItemSupplier RelatedItemSupplier
|
||||||
|
|
||||||
// state
|
// state
|
||||||
mutex *sync.Mutex
|
mutex *sync.Mutex
|
||||||
|
@ -75,6 +76,7 @@ func NewTableReadController(
|
||||||
inputHistoryService *inputhistory.Service,
|
inputHistoryService *inputhistory.Service,
|
||||||
eventBus *bus.Bus,
|
eventBus *bus.Bus,
|
||||||
pasteboardProvider services.PasteboardProvider,
|
pasteboardProvider services.PasteboardProvider,
|
||||||
|
relatedItemSupplier RelatedItemSupplier,
|
||||||
tableName string,
|
tableName string,
|
||||||
) *TableReadController {
|
) *TableReadController {
|
||||||
return &TableReadController{
|
return &TableReadController{
|
||||||
|
@ -87,6 +89,7 @@ func NewTableReadController(
|
||||||
eventBus: eventBus,
|
eventBus: eventBus,
|
||||||
tableName: tableName,
|
tableName: tableName,
|
||||||
pasteboardProvider: pasteboardProvider,
|
pasteboardProvider: pasteboardProvider,
|
||||||
|
relatedItemSupplier: relatedItemSupplier,
|
||||||
mutex: new(sync.Mutex),
|
mutex: new(sync.Mutex),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -627,13 +627,14 @@ func newService(t *testing.T, cfg serviceConfig) *services {
|
||||||
inputHistoryService,
|
inputHistoryService,
|
||||||
eventBus,
|
eventBus,
|
||||||
pasteboardprovider.NilProvider{},
|
pasteboardprovider.NilProvider{},
|
||||||
|
nil,
|
||||||
cfg.tableName,
|
cfg.tableName,
|
||||||
)
|
)
|
||||||
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
writeController := controllers.NewTableWriteController(state, service, jobsController, readController, settingStore)
|
||||||
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
settingsController := controllers.NewSettingsController(settingStore, eventBus)
|
||||||
columnsController := controllers.NewColumnsController(eventBus)
|
columnsController := controllers.NewColumnsController(eventBus)
|
||||||
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
|
exportController := controllers.NewExportController(state, service, jobsController, columnsController, pasteboardprovider.NilProvider{})
|
||||||
scriptController := controllers.NewScriptController(scriptService, readController, settingsController, eventBus)
|
scriptController := controllers.NewScriptController(scriptService, readController, jobsController, settingsController, eventBus)
|
||||||
|
|
||||||
commandController := commandctrl.NewCommandController(inputHistoryService)
|
commandController := commandctrl.NewCommandController(inputHistoryService)
|
||||||
commandController.AddCommandLookupExtension(scriptController)
|
commandController.AddCommandLookupExtension(scriptController)
|
||||||
|
|
12
internal/dynamo-browse/models/relitems/relitem.go
Normal file
12
internal/dynamo-browse/models/relitems/relitem.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package relitems
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RelatedItem struct {
|
||||||
|
Name string
|
||||||
|
Table string
|
||||||
|
Query *queryexpr.QueryExpr
|
||||||
|
OnSelect func() error
|
||||||
|
}
|
|
@ -8,8 +8,10 @@ package scriptmanager
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/risor-io/risor/object"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/risor-io/risor/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printBuiltin(ctx context.Context, args ...object.Object) object.Object {
|
func printBuiltin(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
@ -70,3 +72,31 @@ func require(funcName string, count int, args []object.Object) *object.Error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bindArgs(funcName string, args []object.Object, bindArgs ...any) *object.Error {
|
||||||
|
if err := require(funcName, len(bindArgs), args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, bindArg := range bindArgs {
|
||||||
|
switch t := bindArg.(type) {
|
||||||
|
case *string:
|
||||||
|
str, err := object.AsString(args[i])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = str
|
||||||
|
case **object.Function:
|
||||||
|
fnRes, isFnRes := args[i].(*object.Function)
|
||||||
|
if !isFnRes {
|
||||||
|
return object.NewError(errors.Errorf("expected arg %v to be a function, was %T", i, bindArg))
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = fnRes
|
||||||
|
default:
|
||||||
|
return object.NewError(errors.Errorf("unhandled arg type %v", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,9 +3,13 @@ package scriptmanager
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/risor-io/risor/object"
|
"github.com/risor-io/risor/object"
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -18,8 +22,9 @@ type extModule struct {
|
||||||
|
|
||||||
func (m *extModule) register() *object.Module {
|
func (m *extModule) register() *object.Module {
|
||||||
return object.NewBuiltinsModule("ext", map[string]object.Object{
|
return object.NewBuiltinsModule("ext", map[string]object.Object{
|
||||||
"command": object.NewBuiltin("command", m.command),
|
"command": object.NewBuiltin("command", m.command),
|
||||||
"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
|
"key_binding": object.NewBuiltin("key_binding", m.keyBinding),
|
||||||
|
"related_items": object.NewBuiltin("related_items", m.relatedItem),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,3 +141,130 @@ func (m *extModule) keyBinding(ctx context.Context, args ...object.Object) objec
|
||||||
m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
|
m.scriptPlugin.keyToKeyBinding[defaultKey] = fullBindingName
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *extModule) relatedItem(ctx context.Context, args ...object.Object) object.Object {
|
||||||
|
thisEnv := scriptEnvFromCtx(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tableName string
|
||||||
|
callbackFn *object.Function
|
||||||
|
)
|
||||||
|
if err := bindArgs("ext.related_items", args, &tableName, &callbackFn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
callFn, hasCallFn := object.GetCallFunc(ctx)
|
||||||
|
if !hasCallFn {
|
||||||
|
return object.NewError(errors.New("no callFn found in context"))
|
||||||
|
}
|
||||||
|
|
||||||
|
newHandler := func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error) {
|
||||||
|
newEnv := thisEnv
|
||||||
|
ctx = ctxWithScriptEnv(ctx, newEnv)
|
||||||
|
|
||||||
|
res, err := callFn(ctx, callbackFn, []object.Object{
|
||||||
|
newItemProxy(newResultSetProxy(rs), index),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, err)
|
||||||
|
} else if object.IsError(res) {
|
||||||
|
errObj := res.(*object.Error)
|
||||||
|
return nil, errors.Errorf("script error '%v':related_item - %v", m.scriptPlugin.name, errObj.Inspect())
|
||||||
|
}
|
||||||
|
|
||||||
|
itr, objErr := object.AsIterator(res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, objErr.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
var relItems []relatedItem
|
||||||
|
for next, hasNext := itr.Next(ctx); hasNext; next, hasNext = itr.Next(ctx) {
|
||||||
|
var newRelItem relatedItem
|
||||||
|
|
||||||
|
itemMap, objErr := object.AsMap(next)
|
||||||
|
if err != nil {
|
||||||
|
return nil, objErr.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
labelName, objErr := object.AsString(itemMap.Get("label"))
|
||||||
|
if objErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newRelItem.label = labelName
|
||||||
|
|
||||||
|
var tableStr = ""
|
||||||
|
if itemMap.Get("table") != object.Nil {
|
||||||
|
tableStr, objErr = object.AsString(itemMap.Get("table"))
|
||||||
|
if objErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newRelItem.table = tableStr
|
||||||
|
|
||||||
|
if selectFn, ok := itemMap.Get("on_select").(*object.Function); ok {
|
||||||
|
newRelItem.onSelect = func() error {
|
||||||
|
thisNewEnv := thisEnv
|
||||||
|
ctx = ctxWithScriptEnv(ctx, thisNewEnv)
|
||||||
|
|
||||||
|
res, err := callFn(ctx, selectFn, []object.Object{})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, err)
|
||||||
|
} else if object.IsError(res) {
|
||||||
|
errObj := res.(*object.Error)
|
||||||
|
return errors.Errorf("rel error '%v' - %v", m.scriptPlugin.name, errObj.Inspect())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queryExprStr, objErr := object.AsString(itemMap.Get("query"))
|
||||||
|
if objErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := queryexpr.Parse(queryExprStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholders
|
||||||
|
if argsVal, isArgsValMap := object.AsMap(itemMap.Get("args")); isArgsValMap == nil {
|
||||||
|
namePlaceholders := make(map[string]string)
|
||||||
|
valuePlaceholders := make(map[string]types.AttributeValue)
|
||||||
|
|
||||||
|
for k, val := range argsVal.Value() {
|
||||||
|
switch v := val.(type) {
|
||||||
|
case *object.String:
|
||||||
|
namePlaceholders[k] = v.Value()
|
||||||
|
valuePlaceholders[k] = &types.AttributeValueMemberS{Value: v.Value()}
|
||||||
|
case *object.Int:
|
||||||
|
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||||
|
case *object.Float:
|
||||||
|
valuePlaceholders[k] = &types.AttributeValueMemberN{Value: fmt.Sprint(v.Value())}
|
||||||
|
case *object.Bool:
|
||||||
|
valuePlaceholders[k] = &types.AttributeValueMemberBOOL{Value: v.Value()}
|
||||||
|
case *object.NilType:
|
||||||
|
valuePlaceholders[k] = &types.AttributeValueMemberNULL{Value: true}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.WithNameParams(namePlaceholders).WithValueParams(valuePlaceholders)
|
||||||
|
}
|
||||||
|
newRelItem.query = query
|
||||||
|
}
|
||||||
|
|
||||||
|
relItems = append(relItems, newRelItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return relItems, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.scriptPlugin.relatedItems = append(m.scriptPlugin.relatedItems, &relatedItemBuilder{
|
||||||
|
table: tableName,
|
||||||
|
itemProduction: newHandler,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
151
internal/dynamo-browse/services/scriptmanager/modext_test.go
Normal file
151
internal/dynamo-browse/services/scriptmanager/modext_test.go
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package scriptmanager_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/services/scriptmanager"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtModule_RelatedItems(t *testing.T) {
|
||||||
|
t.Run("should register a function which will return related items for an item", func(t *testing.T) {
|
||||||
|
scenarios := []struct {
|
||||||
|
desc string
|
||||||
|
code string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "single function, table name match",
|
||||||
|
code: `
|
||||||
|
ext.related_items("test-table", func(item) {
|
||||||
|
print("Hello")
|
||||||
|
return [
|
||||||
|
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||||
|
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "single function, table prefix match",
|
||||||
|
code: `
|
||||||
|
ext.related_items("test-*", func(item) {
|
||||||
|
print("Hello")
|
||||||
|
return [
|
||||||
|
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||||
|
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multi function, table name match",
|
||||||
|
code: `
|
||||||
|
ext.related_items("test-table", func(item) {
|
||||||
|
print("Hello")
|
||||||
|
return [
|
||||||
|
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
ext.related_items("test-table", func(item) {
|
||||||
|
return [
|
||||||
|
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multi function, table name prefix",
|
||||||
|
code: `
|
||||||
|
ext.related_items("test-*", func(item) {
|
||||||
|
print("Hello")
|
||||||
|
return [
|
||||||
|
{"label": "Customer", "query": "pk=$foo", "args": {"foo": "foo"}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
ext.related_items("test-*", func(item) {
|
||||||
|
return [
|
||||||
|
{"label": "Payment", "query": "fla=$daa", "args": {"daa": "Hello"}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.desc, func(t *testing.T) {
|
||||||
|
// Load the script
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", scenario.code)))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
plugin, err := srv.LoadScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, plugin)
|
||||||
|
|
||||||
|
// Get related items of result set
|
||||||
|
rs := &models.ResultSet{
|
||||||
|
TableInfo: &models.TableInfo{
|
||||||
|
Name: "test-table",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, relItems, 2)
|
||||||
|
|
||||||
|
assert.Equal(t, "Customer", relItems[0].Name)
|
||||||
|
assert.Equal(t, "pk=$foo", relItems[0].Query.String())
|
||||||
|
assert.Equal(t, "foo", relItems[0].Query.ValueParamOrNil("foo").(*types.AttributeValueMemberS).Value)
|
||||||
|
|
||||||
|
assert.Equal(t, "Payment", relItems[1].Name)
|
||||||
|
assert.Equal(t, "fla=$daa", relItems[1].Query.String())
|
||||||
|
assert.Equal(t, "Hello", relItems[1].Query.ValueParamOrNil("daa").(*types.AttributeValueMemberS).Value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should support rel_items with on select", func(t *testing.T) {
|
||||||
|
// Load the script
|
||||||
|
srv := scriptmanager.New(scriptmanager.WithFS(testScriptFile(t, "test.tm", `
|
||||||
|
ext.related_items("test-table", func(item) {
|
||||||
|
print("Hello")
|
||||||
|
return [
|
||||||
|
{"label": "Customer", "on_select": func() {
|
||||||
|
print("Selected")
|
||||||
|
}},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
`)))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
plugin, err := srv.LoadScript(ctx, "test.tm")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, plugin)
|
||||||
|
|
||||||
|
// Get related items of result set
|
||||||
|
rs := &models.ResultSet{
|
||||||
|
TableInfo: &models.TableInfo{
|
||||||
|
Name: "test-table",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rs.SetItems([]models.Item{
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "abc"}},
|
||||||
|
{"pk": &types.AttributeValueMemberS{Value: "1232"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
relItems, err := srv.RelatedItemOfItem(context.Background(), rs, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, relItems, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, "Customer", relItems[0].Name)
|
||||||
|
assert.NoError(t, relItems[0].OnSelect())
|
||||||
|
})
|
||||||
|
}
|
57
internal/dynamo-browse/services/scriptmanager/relitem.go
Normal file
57
internal/dynamo-browse/services/scriptmanager/relitem.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package scriptmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/queryexpr"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||||
|
)
|
||||||
|
|
||||||
|
type relatedItem struct {
|
||||||
|
label string
|
||||||
|
table string
|
||||||
|
query *queryexpr.QueryExpr
|
||||||
|
onSelect func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type relatedItemBuilder struct {
|
||||||
|
table string
|
||||||
|
itemProduction func(ctx context.Context, rs *models.ResultSet, index int) ([]relatedItem, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RelatedItemOfItem(ctx context.Context, rs *models.ResultSet, index int) ([]relitems.RelatedItem, error) {
|
||||||
|
riModels := []relitems.RelatedItem{}
|
||||||
|
|
||||||
|
for _, plugin := range s.plugins {
|
||||||
|
for _, rb := range plugin.relatedItems {
|
||||||
|
// TODO: should support matching
|
||||||
|
match, _ := tableMatchesGlob(rb.table, rs.TableInfo.Name)
|
||||||
|
log.Printf("RelatedItemOfItem: table = '%v', pattern = '%v', match = '%v'", rb.table, rs.TableInfo.Name, match)
|
||||||
|
if match {
|
||||||
|
relatedItems, err := rb.itemProduction(ctx, rs, index)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: should probably return error if no rel items were found and an error was raised
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make this nicer
|
||||||
|
for _, ri := range relatedItems {
|
||||||
|
riModels = append(riModels, relitems.RelatedItem{
|
||||||
|
Name: ri.label,
|
||||||
|
Query: ri.query,
|
||||||
|
Table: ri.table,
|
||||||
|
OnSelect: ri.onSelect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return riModels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableMatchesGlob(tableName, pattern string) (bool, error) {
|
||||||
|
return path.Match(tableName, pattern)
|
||||||
|
}
|
|
@ -17,6 +17,10 @@ type resultSetProxy struct {
|
||||||
resultSet *models.ResultSet
|
resultSet *models.ResultSet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newResultSetProxy(rs *models.ResultSet) *resultSetProxy {
|
||||||
|
return &resultSetProxy{resultSet: rs}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *resultSetProxy) SetAttr(name string, value object.Object) error {
|
func (r *resultSetProxy) SetAttr(name string, value object.Object) error {
|
||||||
return errors.Errorf("attribute error: %v", name)
|
return errors.Errorf("attribute error: %v", name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@ import (
|
||||||
"github.com/risor-io/risor/object"
|
"github.com/risor-io/risor/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
relPrefix = "." + string(filepath.Separator)
|
||||||
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
lookupPaths []fs.FS
|
lookupPaths []fs.FS
|
||||||
ifaces Ifaces
|
ifaces Ifaces
|
||||||
|
@ -150,7 +154,7 @@ func (s *Service) readScript(filename string, allowCwd bool) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(filename, string(filepath.Separator)) {
|
if strings.HasPrefix(filename, string(filepath.Separator)) || strings.HasPrefix(filename, relPrefix) {
|
||||||
code, err := os.ReadFile(filename)
|
code, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package scriptmanager
|
package scriptmanager
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
type ScriptPlugin struct {
|
type ScriptPlugin struct {
|
||||||
scriptService *Service
|
scriptService *Service
|
||||||
|
@ -8,6 +10,7 @@ type ScriptPlugin struct {
|
||||||
definedCommands map[string]*Command
|
definedCommands map[string]*Command
|
||||||
definedKeyBindings map[string]*Command
|
definedKeyBindings map[string]*Command
|
||||||
keyToKeyBinding map[string]string
|
keyToKeyBinding map[string]string
|
||||||
|
relatedItems []*relatedItemBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sp *ScriptPlugin) Name() string {
|
func (sp *ScriptPlugin) Name() string {
|
||||||
|
@ -26,3 +29,7 @@ func (c *Command) Invoke(ctx context.Context, args []string, errChan chan error)
|
||||||
errChan <- c.cmdFn(ctx, args)
|
errChan <- c.cmdFn(ctx, args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//func (c *Command) LookupRelevantItems(ctx context.Context, table *models.TableInfo, item *models.Item) error {
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
|
@ -37,6 +37,7 @@ func Default() *KeyBindings {
|
||||||
CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")),
|
CycleLayoutBackwards: key.NewBinding(key.WithKeys("W"), key.WithHelp("W", "cycle layout backward")),
|
||||||
PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")),
|
PromptForCommand: key.NewBinding(key.WithKeys(":"), key.WithHelp(":", "prompt for command")),
|
||||||
ShowColumnOverlay: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "show column overlay")),
|
ShowColumnOverlay: key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "show column overlay")),
|
||||||
|
ShowRelItemsOverlay: key.NewBinding(key.WithKeys("O"), key.WithHelp("O", "show related items overlay")),
|
||||||
CancelRunningJob: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "cancel running job or quit")),
|
CancelRunningJob: key.NewBinding(key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "cancel running job or quit")),
|
||||||
Quit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
|
Quit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "quit")),
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,6 +45,7 @@ type ViewKeyBindings struct {
|
||||||
CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"`
|
CycleLayoutBackwards key.Binding `keymap:"cycle-layout-backwards"`
|
||||||
PromptForCommand key.Binding `keymap:"prompt-for-command"`
|
PromptForCommand key.Binding `keymap:"prompt-for-command"`
|
||||||
ShowColumnOverlay key.Binding `keymap:"show-fields-popup"`
|
ShowColumnOverlay key.Binding `keymap:"show-fields-popup"`
|
||||||
|
ShowRelItemsOverlay key.Binding `keymap:"show-rel-items-popup"`
|
||||||
CancelRunningJob key.Binding `keymap:"cancel-running-job"`
|
CancelRunningJob key.Binding `keymap:"cancel-running-job"`
|
||||||
Quit key.Binding `keymap:"quit"`
|
Quit key.Binding `keymap:"quit"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
"github.com/lmika/dynamo-browse/internal/common/ui/commandctrl"
|
||||||
|
@ -16,15 +20,13 @@ import (
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemview"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamoitemview"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamotableview"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/dynamotableview"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/relselector"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/styles"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/tableselect"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
bus "github.com/lmika/events"
|
bus "github.com/lmika/events"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -48,6 +50,7 @@ type Model struct {
|
||||||
scriptController *controllers.ScriptController
|
scriptController *controllers.ScriptController
|
||||||
jobController *controllers.JobsController
|
jobController *controllers.JobsController
|
||||||
colSelector *colselector.Model
|
colSelector *colselector.Model
|
||||||
|
relSelector *relselector.Model
|
||||||
itemEdit *dynamoitemedit.Model
|
itemEdit *dynamoitemedit.Model
|
||||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||||
tableSelect *tableselect.Model
|
tableSelect *tableselect.Model
|
||||||
|
@ -85,7 +88,8 @@ func NewModel(
|
||||||
mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div)
|
mainView := layout.NewVBox(layout.LastChildFixedAt(14), dtv, div)
|
||||||
|
|
||||||
colSelector := colselector.New(mainView, defaultKeyMap, columnsController)
|
colSelector := colselector.New(mainView, defaultKeyMap, columnsController)
|
||||||
itemEdit := dynamoitemedit.NewModel(colSelector)
|
relSelector := relselector.New(colSelector)
|
||||||
|
itemEdit := dynamoitemedit.NewModel(relSelector)
|
||||||
statusAndPrompt := statusandprompt.New(itemEdit, pasteboardProvider, "", uiStyles.StatusAndPrompt)
|
statusAndPrompt := statusandprompt.New(itemEdit, pasteboardProvider, "", uiStyles.StatusAndPrompt)
|
||||||
dialogPrompt := dialogprompt.New(statusAndPrompt)
|
dialogPrompt := dialogprompt.New(statusAndPrompt)
|
||||||
tableSelect := tableselect.New(dialogPrompt, uiStyles)
|
tableSelect := tableselect.New(dialogPrompt, uiStyles)
|
||||||
|
@ -244,6 +248,7 @@ func NewModel(
|
||||||
jobController: jobController,
|
jobController: jobController,
|
||||||
itemEdit: itemEdit,
|
itemEdit: itemEdit,
|
||||||
colSelector: colSelector,
|
colSelector: colSelector,
|
||||||
|
relSelector: relSelector,
|
||||||
statusAndPrompt: statusAndPrompt,
|
statusAndPrompt: statusAndPrompt,
|
||||||
tableSelect: tableSelect,
|
tableSelect: tableSelect,
|
||||||
root: root,
|
root: root,
|
||||||
|
@ -267,7 +272,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
)
|
)
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
// TODO: use modes here
|
// TODO: use modes here
|
||||||
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() && !m.colSelector.ColSelectorVisible() {
|
if !m.statusAndPrompt.InPrompt() && !m.tableSelect.Visible() && !m.colSelector.ColSelectorVisible() && !m.relSelector.SelectorVisible() {
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, m.keyMap.Mark):
|
case key.Matches(msg, m.keyMap.Mark):
|
||||||
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||||
|
@ -302,6 +307,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
// return m, nil
|
// return m, nil
|
||||||
case key.Matches(msg, m.keyMap.ShowColumnOverlay):
|
case key.Matches(msg, m.keyMap.ShowColumnOverlay):
|
||||||
return m, events.SetTeaMessage(controllers.ShowColumnOverlay{})
|
return m, events.SetTeaMessage(controllers.ShowColumnOverlay{})
|
||||||
|
case key.Matches(msg, m.keyMap.ShowRelItemsOverlay):
|
||||||
|
if idx := m.tableView.SelectedItemIndex(); idx >= 0 {
|
||||||
|
return m, events.SetTeaMessage(m.scriptController.LookupRelatedItems(idx))
|
||||||
|
}
|
||||||
case key.Matches(msg, m.keyMap.PromptForCommand):
|
case key.Matches(msg, m.keyMap.PromptForCommand):
|
||||||
return m, m.commandController.Prompt
|
return m, m.commandController.Prompt
|
||||||
case key.Matches(msg, m.keyMap.PromptForTable):
|
case key.Matches(msg, m.keyMap.PromptForTable):
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/keybindings"
|
||||||
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
table "github.com/lmika/go-bubble-table"
|
table "github.com/lmika/go-bubble-table"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,7 +48,6 @@ func (m *colListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case controllers.SetSelectedColumnInColSelector:
|
case controllers.SetSelectedColumnInColSelector:
|
||||||
// HACK: this needs to work for all cases
|
// HACK: this needs to work for all cases
|
||||||
log.Printf("%d == %d?", int(msg), m.table.Cursor()+1)
|
|
||||||
if int(msg) == m.table.Cursor()+1 {
|
if int(msg) == m.table.Cursor()+1 {
|
||||||
m.table.GoDown()
|
m.table.GoDown()
|
||||||
}
|
}
|
||||||
|
|
17
internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go
Normal file
17
internal/dynamo-browse/ui/teamodels/relselector/itemmdl.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package relselector
|
||||||
|
|
||||||
|
type relItemModel struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti relItemModel) FilterValue() string {
|
||||||
|
return ti.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti relItemModel) Title() string {
|
||||||
|
return ti.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti relItemModel) Description() string {
|
||||||
|
return ti.name
|
||||||
|
}
|
132
internal/dynamo-browse/ui/teamodels/relselector/listmdl.go
Normal file
132
internal/dynamo-browse/ui/teamodels/relselector/listmdl.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package relselector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/common/sliceutils"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/common/ui/events"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/models/relitems"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
frameColor = lipgloss.Color("63")
|
||||||
|
|
||||||
|
frameStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(frameColor)
|
||||||
|
style = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(frameColor)
|
||||||
|
|
||||||
|
keyEsc = key.NewBinding(key.WithKeys(tea.KeyEsc.String()))
|
||||||
|
keyEnter = key.NewBinding(key.WithKeys(tea.KeyEnter.String()))
|
||||||
|
)
|
||||||
|
|
||||||
|
type listModel struct {
|
||||||
|
event controllers.ShowRelatedItemsOverlay
|
||||||
|
list list.Model
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListModel() *listModel {
|
||||||
|
items := []list.Item{}
|
||||||
|
|
||||||
|
delegate := list.NewDefaultDelegate()
|
||||||
|
delegate.ShowDescription = false
|
||||||
|
delegate.Styles.SelectedTitle = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
|
BorderForeground(lipgloss.Color("#2c5fb7")).
|
||||||
|
Foreground(lipgloss.Color("#2c5fb7")).
|
||||||
|
Padding(0, 0, 0, 1)
|
||||||
|
delegate.Styles.SelectedDesc = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.NormalBorder(), false, false, false, true).
|
||||||
|
BorderForeground(lipgloss.Color("#2c5fb7")).
|
||||||
|
Foreground(lipgloss.Color("#5277b7")).
|
||||||
|
Padding(0, 0, 0, 1)
|
||||||
|
|
||||||
|
list := list.New(items, delegate, overlayWidth, overlayHeight-4)
|
||||||
|
list.KeyMap.CursorUp = key.NewBinding(
|
||||||
|
key.WithKeys("up", "i"),
|
||||||
|
key.WithHelp("↑/i", "up"),
|
||||||
|
)
|
||||||
|
list.KeyMap.CursorDown = key.NewBinding(
|
||||||
|
key.WithKeys("down", "k"),
|
||||||
|
key.WithHelp("↓/k", "down"),
|
||||||
|
)
|
||||||
|
list.KeyMap.PrevPage = key.NewBinding(
|
||||||
|
key.WithKeys("left", "j", "pgup", "b", "u"),
|
||||||
|
key.WithHelp("←/j/pgup", "prev page"),
|
||||||
|
)
|
||||||
|
list.KeyMap.NextPage = key.NewBinding(
|
||||||
|
key.WithKeys("right", "l", "pgdown", "f", "d"),
|
||||||
|
key.WithHelp("→/l/pgdn", "next page"),
|
||||||
|
)
|
||||||
|
list.SetShowTitle(false)
|
||||||
|
list.SetShowHelp(false)
|
||||||
|
//list.DisableQuitKeybindings()
|
||||||
|
|
||||||
|
return &listModel{
|
||||||
|
list: list,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *listModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *listModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cc utils.CmdCollector
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, keyEnter):
|
||||||
|
if onSel := m.event.OnSelected; onSel != nil {
|
||||||
|
cc.Add(events.SetTeaMessage(onSel(m.event.Items[m.list.Index()])))
|
||||||
|
}
|
||||||
|
return m, events.SetTeaMessage(controllers.HideColumnOverlay{})
|
||||||
|
case key.Matches(msg, keyEsc):
|
||||||
|
return m, events.SetTeaMessage(controllers.HideColumnOverlay{})
|
||||||
|
default:
|
||||||
|
m.list = cc.Collect(m.list.Update(msg)).(list.Model)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m.list = cc.Collect(m.list.Update(msg)).(list.Model)
|
||||||
|
}
|
||||||
|
return m, cc.Cmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *listModel) View() string {
|
||||||
|
innerView := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Top,
|
||||||
|
lipgloss.PlaceHorizontal(overlayWidth-2, lipgloss.Center, "Related Items"),
|
||||||
|
frameStyle.Render(strings.Repeat(lipgloss.NormalBorder().Top, overlayWidth-2)),
|
||||||
|
m.list.View(),
|
||||||
|
)
|
||||||
|
|
||||||
|
view := style.Width(overlayWidth - 2).Height(m.height - 2).Render(innerView)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *listModel) Resize(w, h int) layout.ResizingModel {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *listModel) setItems(event controllers.ShowRelatedItemsOverlay, newHeight int) {
|
||||||
|
listItems := sliceutils.Map(event.Items, func(item relitems.RelatedItem) list.Item {
|
||||||
|
return relItemModel{name: item.Name}
|
||||||
|
})
|
||||||
|
m.event = event
|
||||||
|
m.list.SetItems(listItems)
|
||||||
|
m.list.Select(0)
|
||||||
|
m.list.SetHeight(newHeight - 4)
|
||||||
|
|
||||||
|
m.height = newHeight
|
||||||
|
}
|
73
internal/dynamo-browse/ui/teamodels/relselector/model.go
Normal file
73
internal/dynamo-browse/ui/teamodels/relselector/model.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package relselector
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/controllers"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/layout"
|
||||||
|
"github.com/lmika/dynamo-browse/internal/dynamo-browse/ui/teamodels/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
overlayWidth = 50
|
||||||
|
|
||||||
|
overlayHeight = 8
|
||||||
|
overlayHeightExtra2 = 2
|
||||||
|
maxItems = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
subModel tea.Model
|
||||||
|
compositor *layout.Compositor
|
||||||
|
listModel *listModel
|
||||||
|
w, h int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(subModel tea.Model) *Model {
|
||||||
|
compositor := layout.NewCompositor(subModel)
|
||||||
|
listModel := newListModel()
|
||||||
|
|
||||||
|
return &Model{
|
||||||
|
subModel: subModel,
|
||||||
|
listModel: listModel,
|
||||||
|
compositor: compositor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Init() tea.Cmd {
|
||||||
|
return m.compositor.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cc utils.CmdCollector
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case controllers.ShowRelatedItemsOverlay:
|
||||||
|
newHeight := overlayHeight + utils.Min(len(msg.Items), maxItems)*overlayHeightExtra2
|
||||||
|
|
||||||
|
m.listModel.setItems(msg, newHeight)
|
||||||
|
m.compositor.SetOverlay(m.listModel, m.w/2-overlayWidth/2, m.h/2-newHeight/2, overlayWidth, newHeight)
|
||||||
|
case controllers.HideColumnOverlay:
|
||||||
|
m.compositor.ClearOverlay()
|
||||||
|
case tea.KeyMsg:
|
||||||
|
m.compositor = cc.Collect(m.compositor.Update(msg)).(*layout.Compositor)
|
||||||
|
default:
|
||||||
|
m.subModel = cc.Collect(m.subModel.Update(msg)).(tea.Model)
|
||||||
|
}
|
||||||
|
return m, cc.Cmd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) View() string {
|
||||||
|
return m.compositor.View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||||
|
m.w, m.h = w, h
|
||||||
|
m.compositor.MoveOverlay(m.w/2-overlayWidth/2, m.h/2-overlayHeight/2)
|
||||||
|
m.listModel.Resize(w, h)
|
||||||
|
m.subModel = layout.Resize(m.subModel, w, h)
|
||||||
|
m.listModel = layout.Resize(m.listModel, w, h).(*listModel)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) SelectorVisible() bool {
|
||||||
|
return m.compositor.HasOverlay()
|
||||||
|
}
|
|
@ -1,5 +1,12 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
|
func Min(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
func Max(x, y int) int {
|
func Max(x, y int) int {
|
||||||
if x > y {
|
if x > y {
|
||||||
return x
|
return x
|
||||||
|
|
8
test.tm
Normal file
8
test.tm
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
ext.related_items("business-addresses", func(item) {
|
||||||
|
print("Hello")
|
||||||
|
return [
|
||||||
|
{"label": "Customer", "query": `city="Austin"`, "args": {"foo": "foo"}},
|
||||||
|
{"label": "Payment", "query": `officeOpened=false`, "args": {"daa": "Hello"}},
|
||||||
|
{"label": "Thing", "query": `colors.door^="P"`, "args": {"daa": "Hello"}},
|
||||||
|
]
|
||||||
|
})
|
Loading…
Reference in a new issue