Removed the other tools and fixed the README.md file
This commit is contained in:
parent
ab309084c5
commit
937af987e6
30
README.md
30
README.md
|
@ -1,31 +1,25 @@
|
|||
# Audax Toolset
|
||||
# Dynamo-Browse
|
||||
|
||||
A set of small, terminal based UI (TUI, sometimes called "Rougelike") tools for
|
||||
administering AWS services.
|
||||
A CLI tool for browsing DynamoDB tables.
|
||||
|
||||
They were built to make it easy to do quick things with
|
||||
common AWS services, such as DynamoDB, without having to learn incantations with the CLI or
|
||||
go to the AWS console itself. This keeps you focused on your task and saves you from
|
||||
breaking concentration, especially if you do a lot in the terminal.
|
||||

|
||||
|
||||

|
||||
|
||||
## The Toolset
|
||||
|
||||
More info about the available tools are available here:
|
||||
|
||||
- [dynamo-browse](https://audax.tools/dynamo-browse): Browse DynamoDB tables
|
||||
This was built to make it easy to quickly view and lightly edit
|
||||
DynamoDB tables, running locally or within AWS, from the Terminal
|
||||
without having to learn incantations with the CLI or
|
||||
go to the AWS console itself. This helps from unnecessary context switching
|
||||
if you tend to use the terminal a lot.
|
||||
|
||||
## Install
|
||||
|
||||
Binary packages can be [download from the release page](https://github.com/lmika/audax/releases/latest).
|
||||
Binary packages can be [download from the release page](https://github.com/lmika/dynamo-browse/releases/latest).
|
||||
|
||||
If you have Go 1.18, you can install using the following command:
|
||||
If you have Go 1.18 or later, you can install using the following command:
|
||||
|
||||
```
|
||||
go install github.com/lmika/audax/cmd/dynamo-browse@v0.1.0
|
||||
go install github.com/lmika/dynamo-browse/cmd/dynamo-browse@v0.1.0
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Audax toolset is released under the MIT License.
|
||||
Dynamo-Browse is released under the MIT License.
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/common/ui/logging"
|
||||
"github.com/lmika/audax/internal/slog-view/controllers"
|
||||
"github.com/lmika/audax/internal/slog-view/services/logreader"
|
||||
"github.com/lmika/audax/internal/slog-view/ui"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var flagDebug = flag.String("debug", "", "file to log debug messages")
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
cli.Fatal("usage: slog-view LOGFILE")
|
||||
}
|
||||
|
||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||
lipgloss.HasDarkBackground()
|
||||
|
||||
closeFn := logging.EnableLogging(*flagDebug)
|
||||
defer closeFn()
|
||||
|
||||
service := logreader.NewService()
|
||||
|
||||
ctrl := controllers.NewLogFileController(service, flag.Arg(0))
|
||||
|
||||
cmdController := commandctrl.NewCommandController(nil)
|
||||
//cmdController.AddCommands(&commandctrl.CommandList{
|
||||
// Commands: map[string]commandctrl.Command{
|
||||
// "cd": func(args []string) tea.Cmd {
|
||||
// return ctrl.ChangePrefix(args[0])
|
||||
// },
|
||||
// },
|
||||
//})
|
||||
|
||||
model := ui.NewModel(ctrl, cmdController)
|
||||
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/dispatcher"
|
||||
"github.com/lmika/audax/internal/sqs-browse/controllers"
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
sqsprovider "github.com/lmika/audax/internal/sqs-browse/providers/sqs"
|
||||
"github.com/lmika/audax/internal/sqs-browse/providers/stormstore"
|
||||
"github.com/lmika/audax/internal/sqs-browse/services/messages"
|
||||
"github.com/lmika/audax/internal/sqs-browse/services/pollmessage"
|
||||
"github.com/lmika/audax/internal/sqs-browse/ui"
|
||||
"github.com/lmika/events"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var flagQueue = flag.String("q", "", "queue to poll")
|
||||
var flagTarget = flag.String("t", "", "target queue to push to")
|
||||
flag.Parse()
|
||||
|
||||
ctx := context.Background()
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot load AWS config: %v", err)
|
||||
}
|
||||
sqsClient := sqs.NewFromConfig(cfg)
|
||||
|
||||
bus := events.New()
|
||||
|
||||
workspaceFile, err := os.CreateTemp("", "sqs-browse*.workspace")
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot create workspace file: %v", err)
|
||||
}
|
||||
workspaceFile.Close() // We just need the filename
|
||||
|
||||
msgStore, err := stormstore.NewStore(workspaceFile.Name())
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot open workspace: %v", err)
|
||||
}
|
||||
defer msgStore.Close()
|
||||
|
||||
sqsProvider := sqsprovider.NewProvider(sqsClient)
|
||||
|
||||
messageService := messages.NewService(sqsProvider)
|
||||
pollService := pollmessage.NewService(msgStore, sqsProvider, *flagQueue, bus)
|
||||
|
||||
msgSendingHandlers := controllers.NewMessageSendingController(messageService, *flagTarget)
|
||||
|
||||
loopback := &msgLoopback{}
|
||||
uiDispatcher := dispatcher.NewDispatcher(loopback)
|
||||
|
||||
uiModel := ui.NewModel(uiDispatcher, msgSendingHandlers)
|
||||
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
||||
loopback.program = p
|
||||
|
||||
bus.On("new-messages", func(m []*models.Message) { p.Send(ui.NewMessagesEvent(m)) })
|
||||
|
||||
f, err := tea.LogToFile("debug.log", "debug")
|
||||
if err != nil {
|
||||
fmt.Println("fatal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("workspace file: %v", workspaceFile.Name())
|
||||
|
||||
go func() {
|
||||
if err := pollService.Poll(context.Background()); err != nil {
|
||||
log.Printf("cannot start poller: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type msgLoopback struct {
|
||||
program *tea.Program
|
||||
}
|
||||
|
||||
func (m *msgLoopback) Send(msg tea.Msg) {
|
||||
m.program.Send(msg)
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flagQueue := flag.String("q", "", "URL of queue to drain")
|
||||
flagDir := flag.String("dir", "", "directory to save messages")
|
||||
flag.Parse()
|
||||
|
||||
if *flagQueue == "" {
|
||||
cli.Fatalf("-q flag needs to be specified")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot load AWS config: %v", err)
|
||||
}
|
||||
|
||||
outDir := *flagDir
|
||||
if outDir == "" {
|
||||
outDir = "out-" + time.Now().Format("20060102150405")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||
cli.Fatalf("unable to create out dir: %v", err)
|
||||
}
|
||||
|
||||
client := sqs.NewFromConfig(cfg)
|
||||
msgCount := 0
|
||||
for {
|
||||
out, err := client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
|
||||
QueueUrl: aws.String(*flagQueue),
|
||||
MaxNumberOfMessages: 10,
|
||||
WaitTimeSeconds: 1,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("error receiving messages: %v", err)
|
||||
break
|
||||
} else if len(out.Messages) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
messagesToDelete := make([]types.DeleteMessageBatchRequestEntry, 0, 10)
|
||||
for _, msg := range out.Messages {
|
||||
if err := handleMessage(ctx, outDir, msg); err == nil {
|
||||
messagesToDelete = append(messagesToDelete, types.DeleteMessageBatchRequestEntry{
|
||||
Id: msg.MessageId,
|
||||
ReceiptHandle: msg.ReceiptHandle,
|
||||
})
|
||||
msgCount += 1
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
if len(messagesToDelete) == 0 {
|
||||
log.Printf("no messages handled, terminating")
|
||||
break
|
||||
}
|
||||
|
||||
if _, err := client.DeleteMessageBatch(ctx, &sqs.DeleteMessageBatchInput{
|
||||
QueueUrl: aws.String(*flagQueue),
|
||||
Entries: messagesToDelete,
|
||||
}); err != nil {
|
||||
log.Printf("error deleting messages from queue: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Handled %v messages", msgCount)
|
||||
}
|
||||
|
||||
func handleMessage(ctx context.Context, outDir string, msg types.Message) error {
|
||||
outFile := filepath.Join(outDir, aws.ToString(msg.MessageId)+".json")
|
||||
msgBody := aws.ToString(msg.Body)
|
||||
|
||||
log.Printf("%v -> %v", aws.ToString(msg.MessageId), outFile)
|
||||
if err := os.WriteFile(outFile, []byte(msgBody), 0644); err != nil {
|
||||
return errors.Wrapf(err, "unable to write message %v to file %v", msg.MessageId, outFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/common/ui/logging"
|
||||
"github.com/lmika/audax/internal/ssm-browse/controllers"
|
||||
"github.com/lmika/audax/internal/ssm-browse/providers/awsssm"
|
||||
"github.com/lmika/audax/internal/ssm-browse/services/ssmparameters"
|
||||
"github.com/lmika/audax/internal/ssm-browse/ui"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var flagLocal = flag.Bool("local", false, "local endpoint")
|
||||
var flagDebug = flag.String("debug", "", "file to log debug messages")
|
||||
flag.Parse()
|
||||
|
||||
// Pre-determine if layout has dark background. This prevents calls for creating a list to hang.
|
||||
lipgloss.HasDarkBackground()
|
||||
|
||||
closeFn := logging.EnableLogging(*flagDebug)
|
||||
defer closeFn()
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(context.Background())
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot load AWS config: %v", err)
|
||||
}
|
||||
|
||||
var ssmClient *ssm.Client
|
||||
if *flagLocal {
|
||||
ssmClient = ssm.NewFromConfig(cfg,
|
||||
ssm.WithEndpointResolver(ssm.EndpointResolverFromURL("http://localhost:4566")))
|
||||
} else {
|
||||
ssmClient = ssm.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
provider := awsssm.NewProvider(ssmClient)
|
||||
service := ssmparameters.NewService(provider)
|
||||
|
||||
ctrl := controllers.New(service)
|
||||
|
||||
cmdController := commandctrl.NewCommandController(nil)
|
||||
cmdController.AddCommands(&commandctrl.CommandList{
|
||||
Commands: map[string]commandctrl.Command{
|
||||
"cd": func(ec commandctrl.ExecContext, args []string) tea.Msg {
|
||||
return ctrl.ChangePrefix(args[0])
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
model := ui.NewModel(ctrl, cmdController)
|
||||
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
|
||||
if err := p.Start(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import "github.com/lmika/audax/internal/slog-view/models"
|
||||
|
||||
type NewLogFile *models.LogFile
|
||||
|
||||
type ViewLogLineFullScreen *models.LogLine
|
|
@ -1,47 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/slog-view/models"
|
||||
"github.com/lmika/audax/internal/slog-view/services/logreader"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type LogFileController struct {
|
||||
logReader *logreader.Service
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
filename string
|
||||
logFile *models.LogFile
|
||||
}
|
||||
|
||||
func NewLogFileController(logReader *logreader.Service, filename string) *LogFileController {
|
||||
return &LogFileController{
|
||||
logReader: logReader,
|
||||
filename: filename,
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
func (lfc *LogFileController) ReadLogFile() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
logFile, err := lfc.logReader.Open(lfc.filename)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return NewLogFile(logFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (lfc *LogFileController) ViewLogLineFullScreen(line *models.LogLine) tea.Cmd {
|
||||
if line == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func() tea.Msg {
|
||||
return ViewLogLineFullScreen(line)
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package models
|
||||
|
||||
type LogFile struct {
|
||||
Filename string
|
||||
Lines []LogLine
|
||||
}
|
||||
|
||||
type LogLine struct {
|
||||
JSON interface{}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package logreader
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"github.com/lmika/audax/internal/slog-view/models"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) Open(filename string) (*models.LogFile, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot open file: %v", filename)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var lines []models.LogLine
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
var data interface{}
|
||||
if err := json.Unmarshal([]byte(line), &data); err != nil {
|
||||
log.Printf("invalid json line: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
lines = append(lines, models.LogLine{JSON: data})
|
||||
}
|
||||
if scanner.Err() != nil {
|
||||
return nil, errors.Wrapf(err, "unable to scan file: %v", filename)
|
||||
}
|
||||
|
||||
return &models.LogFile{Lines: lines}, nil
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/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("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1")),
|
||||
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")),
|
||||
},
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package fullviewlinedetails
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/slog-view/models"
|
||||
"github.com/lmika/audax/internal/slog-view/ui/linedetails"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
submodel tea.Model
|
||||
lineDetails *linedetails.Model
|
||||
|
||||
visible bool
|
||||
}
|
||||
|
||||
func NewModel(submodel tea.Model, style frame.Style) *Model {
|
||||
return &Model{
|
||||
submodel: submodel,
|
||||
lineDetails: linedetails.New(style),
|
||||
}
|
||||
}
|
||||
|
||||
func (*Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.visible = false
|
||||
return m, nil
|
||||
}
|
||||
|
||||
if m.visible {
|
||||
newModel, cmd := m.lineDetails.Update(msg)
|
||||
m.lineDetails = newModel.(*linedetails.Model)
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.submodel, cmd = m.submodel.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) ViewItem(item *models.LogLine) {
|
||||
m.visible = true
|
||||
m.lineDetails.SetSelectedItem(item)
|
||||
m.lineDetails.SetFocused(true)
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
if m.visible {
|
||||
return m.lineDetails.View()
|
||||
}
|
||||
return m.submodel.View()
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.submodel = layout.Resize(m.submodel, w, h)
|
||||
m.lineDetails = layout.Resize(m.lineDetails, w, h).(*linedetails.Model)
|
||||
return m
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package linedetails
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/slog-view/models"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
viewport viewport.Model
|
||||
w, h int
|
||||
|
||||
// model state
|
||||
focused bool
|
||||
selectedItem *models.LogLine
|
||||
}
|
||||
|
||||
func New(style frame.Style) *Model {
|
||||
viewport := viewport.New(0, 0)
|
||||
viewport.SetContent("")
|
||||
return &Model{
|
||||
frameTitle: frame.NewFrameTitle("Item", false, style),
|
||||
viewport: viewport,
|
||||
}
|
||||
}
|
||||
|
||||
func (*Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) SetFocused(newFocused bool) {
|
||||
m.focused = newFocused
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if m.focused {
|
||||
newModel, cmd := m.viewport.Update(msg)
|
||||
m.viewport = newModel
|
||||
return m, cmd
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) SetSelectedItem(item *models.LogLine) {
|
||||
m.selectedItem = item
|
||||
|
||||
if m.selectedItem != nil {
|
||||
if formattedJson, err := json.MarshalIndent(item.JSON, "", " "); err == nil {
|
||||
m.viewport.SetContent(string(formattedJson))
|
||||
} else {
|
||||
m.viewport.SetContent("(not json)")
|
||||
}
|
||||
} else {
|
||||
m.viewport.SetContent("(no line)")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.viewport.View())
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.w, m.h = w, h
|
||||
m.frameTitle.Resize(w, h)
|
||||
m.viewport.Width = w
|
||||
m.viewport.Height = h - m.frameTitle.HeaderHeight()
|
||||
return m
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package loglines
|
||||
|
||||
import "github.com/lmika/audax/internal/slog-view/models"
|
||||
|
||||
type NewLogLineSelected *models.LogLine
|
|
@ -1,97 +0,0 @@
|
|||
package loglines
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/slog-view/models"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
table table.Model
|
||||
|
||||
logFile *models.LogFile
|
||||
|
||||
w, h int
|
||||
}
|
||||
|
||||
func New(style frame.Style) *Model {
|
||||
frameTitle := frame.NewFrameTitle("File: ", true, style)
|
||||
table := table.New(table.SimpleColumns{"level", "error", "message"}, 0, 0)
|
||||
|
||||
return &Model{
|
||||
frameTitle: frameTitle,
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SetLogFile(newLogFile *models.LogFile) {
|
||||
m.logFile = newLogFile
|
||||
m.frameTitle.SetTitle("File: " + filepath.Base(newLogFile.Filename))
|
||||
|
||||
cols := table.SimpleColumns{"level", "error", "message"}
|
||||
|
||||
newTbl := table.New(cols, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||
newRows := make([]table.Row, len(newLogFile.Lines))
|
||||
for i, r := range newLogFile.Lines {
|
||||
newRows[i] = itemTableRow{r}
|
||||
}
|
||||
newTbl.SetRows(newRows)
|
||||
|
||||
m.table = newTbl
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
//var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "i", "up":
|
||||
m.table.GoUp()
|
||||
return m, m.emitNewSelectedParameter()
|
||||
case "k", "down":
|
||||
m.table.GoDown()
|
||||
return m, m.emitNewSelectedParameter()
|
||||
}
|
||||
//m.table, cmd = m.table.Update(msg)
|
||||
//return m, cmd
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) SelectedLogLine() *models.LogLine {
|
||||
if row, ok := m.table.SelectedRow().(itemTableRow); ok {
|
||||
return &(row.item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) emitNewSelectedParameter() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
selectedLogLine := m.SelectedLogLine()
|
||||
if selectedLogLine != nil {
|
||||
return NewLogLineSelected(selectedLogLine)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.table.View())
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.w, m.h = w, h
|
||||
m.frameTitle.Resize(w, h)
|
||||
m.table.SetSize(w, h-m.frameTitle.HeaderHeight())
|
||||
return m
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package loglines
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"github.com/lmika/audax/internal/slog-view/models"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type itemTableRow struct {
|
||||
item models.LogLine
|
||||
}
|
||||
|
||||
func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
|
||||
// TODO: these cols are fixed, they should be dynamic
|
||||
level := mtr.renderFirstLineOfField(mtr.item.JSON, "level")
|
||||
err := mtr.renderFirstLineOfField(mtr.item.JSON, "error")
|
||||
msg := mtr.renderFirstLineOfField(mtr.item.JSON, "message")
|
||||
line := fmt.Sprintf("%s\t%s\t%s", level, err, msg)
|
||||
|
||||
if index == model.Cursor() {
|
||||
fmt.Fprintln(w, model.Styles.SelectedRow.Render(line))
|
||||
} else {
|
||||
fmt.Fprintln(w, line)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this needs to be some form of path expression
|
||||
func (mtr itemTableRow) renderFirstLineOfField(d interface{}, field string) string {
|
||||
switch k := d.(type) {
|
||||
case map[string]interface{}:
|
||||
return mtr.renderFirstLineOfValue(k[field])
|
||||
default:
|
||||
return mtr.renderFirstLineOfValue(k)
|
||||
}
|
||||
}
|
||||
|
||||
func (mtr itemTableRow) renderFirstLineOfValue(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch k := v.(type) {
|
||||
case string:
|
||||
firstLine := strings.SplitN(k, "\n", 2)[0]
|
||||
return firstLine
|
||||
case int:
|
||||
return fmt.Sprint(k)
|
||||
case float64:
|
||||
return fmt.Sprint(k)
|
||||
case bool:
|
||||
return fmt.Sprint(k)
|
||||
case map[string]interface{}:
|
||||
return "{}"
|
||||
case []interface{}:
|
||||
return "[]"
|
||||
default:
|
||||
return "(other)"
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
"github.com/lmika/audax/internal/slog-view/controllers"
|
||||
"github.com/lmika/audax/internal/slog-view/styles"
|
||||
"github.com/lmika/audax/internal/slog-view/ui/fullviewlinedetails"
|
||||
"github.com/lmika/audax/internal/slog-view/ui/linedetails"
|
||||
"github.com/lmika/audax/internal/slog-view/ui/loglines"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
controller *controllers.LogFileController
|
||||
cmdController *commandctrl.CommandController
|
||||
|
||||
root tea.Model
|
||||
logLines *loglines.Model
|
||||
lineDetails *linedetails.Model
|
||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||
fullViewLineDetails *fullviewlinedetails.Model
|
||||
}
|
||||
|
||||
func NewModel(controller *controllers.LogFileController, cmdController *commandctrl.CommandController) Model {
|
||||
defaultStyles := styles.DefaultStyles
|
||||
logLines := loglines.New(defaultStyles.Frames)
|
||||
lineDetails := linedetails.New(defaultStyles.Frames)
|
||||
box := layout.NewVBox(layout.LastChildFixedAt(17), logLines, lineDetails)
|
||||
fullViewLineDetails := fullviewlinedetails.NewModel(box, defaultStyles.Frames)
|
||||
statusAndPrompt := statusandprompt.New(fullViewLineDetails, "", defaultStyles.StatusAndPrompt)
|
||||
|
||||
root := layout.FullScreen(statusAndPrompt)
|
||||
|
||||
return Model{
|
||||
controller: controller,
|
||||
cmdController: cmdController,
|
||||
root: root,
|
||||
statusAndPrompt: statusAndPrompt,
|
||||
logLines: logLines,
|
||||
lineDetails: lineDetails,
|
||||
fullViewLineDetails: fullViewLineDetails,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return m.controller.ReadLogFile()
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case controllers.NewLogFile:
|
||||
m.logLines.SetLogFile(msg)
|
||||
case controllers.ViewLogLineFullScreen:
|
||||
m.fullViewLineDetails.ViewItem(msg)
|
||||
case loglines.NewLogLineSelected:
|
||||
m.lineDetails.SetSelectedItem(msg)
|
||||
|
||||
case tea.KeyMsg:
|
||||
if !m.statusAndPrompt.InPrompt() {
|
||||
switch msg.String() {
|
||||
// TEMP
|
||||
case ":":
|
||||
return m, func() tea.Msg { return m.cmdController.Prompt() }
|
||||
case "w":
|
||||
return m, m.controller.ViewLogLineFullScreen(m.logLines.SelectedLogLine())
|
||||
// END TEMP
|
||||
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newRoot, cmd := m.root.Update(msg)
|
||||
m.root = newRoot
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
return m.root.View()
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lmika/audax/internal/common/ui/uimodels"
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
"github.com/lmika/audax/internal/sqs-browse/services/messages"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type MessageSendingController struct {
|
||||
messageService *messages.Service
|
||||
targetQueue string
|
||||
}
|
||||
|
||||
func NewMessageSendingController(messageService *messages.Service, targetQueue string) *MessageSendingController {
|
||||
return &MessageSendingController{
|
||||
messageService: messageService,
|
||||
targetQueue: targetQueue,
|
||||
}
|
||||
}
|
||||
|
||||
func (msh *MessageSendingController) ForwardMessage(message models.Message) uimodels.Operation {
|
||||
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||
uiCtx := uimodels.Ctx(ctx)
|
||||
|
||||
if msh.targetQueue == "" {
|
||||
return errors.New("target queue not set")
|
||||
}
|
||||
|
||||
messageId, err := msh.messageService.SendTo(ctx, message, msh.targetQueue)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot send message to %v", msh.targetQueue)
|
||||
}
|
||||
|
||||
uiCtx.Message("Message sent to " + msh.targetQueue + ", id = " + messageId)
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Message struct {
|
||||
ID uint64 `storm:"id,increment"`
|
||||
ExtID string `storm:"unique"`
|
||||
Queue string `storm:"index"`
|
||||
Received time.Time
|
||||
Data string
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package sqs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
client *sqs.Client
|
||||
}
|
||||
|
||||
func NewProvider(client *sqs.Client) *Provider {
|
||||
return &Provider{client: client}
|
||||
}
|
||||
|
||||
func (p *Provider) SendMessage(ctx context.Context, msg models.Message, queue string) (string, error) {
|
||||
// TEMP :: queue URL
|
||||
|
||||
out, err := p.client.SendMessage(ctx, &sqs.SendMessageInput{
|
||||
QueueUrl: aws.String(queue),
|
||||
MessageBody: aws.String(msg.Data),
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "unable to send message to %v", queue)
|
||||
}
|
||||
|
||||
return aws.ToString(out.MessageId), nil
|
||||
}
|
||||
|
||||
func (p *Provider) PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error) {
|
||||
out, err := p.client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
|
||||
QueueUrl: aws.String(queue),
|
||||
MaxNumberOfMessages: 10,
|
||||
WaitTimeSeconds: 20,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to receive messages from queue %v", queue)
|
||||
}
|
||||
|
||||
if len(out.Messages) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
messagesToReturn := make([]*models.Message, 0, len(out.Messages))
|
||||
messagesToDelete := make([]types.DeleteMessageBatchRequestEntry, 0, len(out.Messages))
|
||||
for _, msg := range out.Messages {
|
||||
newLocalMessage := &models.Message{
|
||||
Queue: queue,
|
||||
ExtID: aws.ToString(msg.MessageId),
|
||||
Received: time.Now(),
|
||||
Data: aws.ToString(msg.Body),
|
||||
}
|
||||
messagesToReturn = append(messagesToReturn, newLocalMessage)
|
||||
|
||||
// Pull the message from the queue
|
||||
// TODO: should this be determined by the caller?
|
||||
messagesToDelete = append(messagesToDelete, types.DeleteMessageBatchRequestEntry{
|
||||
Id: msg.MessageId,
|
||||
ReceiptHandle: msg.ReceiptHandle,
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := p.client.DeleteMessageBatch(ctx, &sqs.DeleteMessageBatchInput{
|
||||
QueueUrl: aws.String(queue),
|
||||
Entries: messagesToDelete,
|
||||
}); err != nil {
|
||||
log.Printf("error deleting messages from queue: %v", err)
|
||||
}
|
||||
|
||||
return messagesToReturn, nil
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package stormstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/asdine/storm"
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
db *storm.DB
|
||||
}
|
||||
|
||||
// TODO: should probably be a workspace provider
|
||||
func NewStore(filename string) (*Store, error) {
|
||||
db, err := storm.Open(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "cannot open store %v", filename)
|
||||
}
|
||||
|
||||
return &Store{db: db}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
s.db.Close()
|
||||
}
|
||||
|
||||
func (s *Store) Save(ctx context.Context, msg *models.Message) error {
|
||||
return s.db.Save(msg)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
)
|
||||
|
||||
type MessageSender interface {
|
||||
SendMessage(ctx context.Context, msg models.Message, queue string) (string, error)
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
messageSender MessageSender
|
||||
}
|
||||
|
||||
func NewService(messageSender MessageSender) *Service {
|
||||
return &Service{
|
||||
messageSender: messageSender,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SendTo(ctx context.Context, msg models.Message, destQueue string) (string, error) {
|
||||
messageId, err := s.messageSender.SendMessage(ctx, msg, destQueue)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "cannot send message to %v", destQueue)
|
||||
}
|
||||
return messageId, nil
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package pollmessage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
)
|
||||
|
||||
type MessageStore interface {
|
||||
Save(ctx context.Context, msg *models.Message) error
|
||||
}
|
||||
|
||||
type MessagePoller interface {
|
||||
PollForNewMessages(ctx context.Context, queue string) ([]*models.Message, error)
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package pollmessage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/lmika/events"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
store MessageStore
|
||||
poller MessagePoller
|
||||
queue string
|
||||
bus *events.Bus
|
||||
}
|
||||
|
||||
func NewService(store MessageStore, poller MessagePoller, queue string, bus *events.Bus) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
poller: poller,
|
||||
queue: queue,
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
|
||||
// Poll starts polling for new messages and adding them to the message store
|
||||
func (s *Service) Poll(ctx context.Context) error {
|
||||
for ctx.Err() == nil {
|
||||
log.Printf("polling for new messages: %v", s.queue)
|
||||
newMsgs, err := s.poller.PollForNewMessages(ctx, s.queue)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to poll for messages")
|
||||
}
|
||||
|
||||
for _, msg := range newMsgs {
|
||||
if err := s.store.Save(ctx, msg); err != nil {
|
||||
log.Printf("warn: unable to save new message %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
s.bus.Fire("new-messages", newMsgs)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/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")),
|
||||
},
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package ui
|
||||
|
||||
import "github.com/lmika/audax/internal/sqs-browse/models"
|
||||
|
||||
type NewMessagesEvent []*models.Message
|
|
@ -1,232 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/common/ui/dispatcher"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/sqs-browse/controllers"
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
)
|
||||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#eac610"))
|
||||
|
||||
inactiveHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1"))
|
||||
)
|
||||
|
||||
type uiModel struct {
|
||||
table table.Model
|
||||
viewport viewport.Model
|
||||
|
||||
ready bool
|
||||
tableRows []table.Row
|
||||
message string
|
||||
|
||||
pendingInput *events.PromptForInputMsg
|
||||
textInput textinput.Model
|
||||
|
||||
dispatcher *dispatcher.Dispatcher
|
||||
msgSendingHandlers *controllers.MessageSendingController
|
||||
}
|
||||
|
||||
func NewModel(dispatcher *dispatcher.Dispatcher, msgSendingHandlers *controllers.MessageSendingController) tea.Model {
|
||||
tbl := table.New(table.SimpleColumns{"seq", "message"}, 100, 20)
|
||||
rows := make([]table.Row, 0)
|
||||
tbl.SetRows(rows)
|
||||
|
||||
textInput := textinput.New()
|
||||
|
||||
model := uiModel{
|
||||
table: tbl,
|
||||
tableRows: rows,
|
||||
message: "",
|
||||
textInput: textInput,
|
||||
msgSendingHandlers: msgSendingHandlers,
|
||||
dispatcher: dispatcher,
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
func (m uiModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *uiModel) updateViewportToSelectedMessage() {
|
||||
if message, ok := m.selectedMessage(); ok {
|
||||
// TODO: not all messages are JSON
|
||||
formattedJson := new(bytes.Buffer)
|
||||
if err := json.Indent(formattedJson, []byte(message.Data), "", " "); err == nil {
|
||||
m.viewport.SetContent(formattedJson.String())
|
||||
} else {
|
||||
m.viewport.SetContent(message.Data)
|
||||
}
|
||||
} else {
|
||||
m.viewport.SetContent("(no message selected)")
|
||||
}
|
||||
}
|
||||
|
||||
func (m uiModel) selectedMessage() (models.Message, bool) {
|
||||
if m.ready && len(m.tableRows) > 0 {
|
||||
if message, ok := m.table.SelectedRow().(messageTableRow); ok {
|
||||
return models.Message(message), true
|
||||
}
|
||||
}
|
||||
return models.Message{}, false
|
||||
}
|
||||
|
||||
func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var textInputCommands tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
// Shared messages
|
||||
case events.ErrorMsg:
|
||||
m.message = "Error: " + msg.Error()
|
||||
case events.StatusMsg:
|
||||
m.message = string(msg)
|
||||
case events.PromptForInputMsg:
|
||||
// TODO
|
||||
//m.textInput.Focus()
|
||||
//m.textInput.SetValue("")
|
||||
//m.pendingInput = &msg
|
||||
|
||||
// Local messages
|
||||
case NewMessagesEvent:
|
||||
for _, newMsg := range msg {
|
||||
m.tableRows = append(m.tableRows, messageTableRow(*newMsg))
|
||||
}
|
||||
m.table.SetRows(m.tableRows)
|
||||
m.updateViewportToSelectedMessage()
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
fixedViewsHeight := lipgloss.Height(m.headerView()) + lipgloss.Height(m.splitterView()) + lipgloss.Height(m.footerView())
|
||||
|
||||
if !m.ready {
|
||||
tableHeight := msg.Height / 2
|
||||
|
||||
m.table.SetSize(msg.Width, tableHeight)
|
||||
m.viewport = viewport.New(msg.Width, msg.Height-tableHeight-fixedViewsHeight)
|
||||
m.viewport.SetContent("(no message selected)")
|
||||
m.ready = true
|
||||
log.Println("Viewport is now ready")
|
||||
} else {
|
||||
tableHeight := msg.Height / 2
|
||||
|
||||
m.table.SetSize(msg.Width, tableHeight)
|
||||
m.viewport.Width = msg.Width
|
||||
m.viewport.Height = msg.Height - tableHeight - fixedViewsHeight
|
||||
}
|
||||
|
||||
m.textInput.Width = msg.Width
|
||||
|
||||
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||
case tea.KeyMsg:
|
||||
|
||||
// If text input in focus, allow that to accept input messages
|
||||
if m.pendingInput != nil {
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
m.pendingInput = nil
|
||||
case "enter":
|
||||
//m.dispatcher.Start(uimodels.WithPromptValue(context.Background(), m.textInput.Value()), m.pendingInput.OnDone)
|
||||
m.pendingInput = nil
|
||||
default:
|
||||
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Normal focus
|
||||
switch msg.String() {
|
||||
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
case "up", "i":
|
||||
m.table.GoUp()
|
||||
m.updateViewportToSelectedMessage()
|
||||
case "down", "k":
|
||||
m.table.GoDown()
|
||||
m.updateViewportToSelectedMessage()
|
||||
|
||||
// TODO: these should be moved somewhere else
|
||||
case "f":
|
||||
if selectedMessage, ok := m.selectedMessage(); ok {
|
||||
m.dispatcher.Start(context.Background(), m.msgSendingHandlers.ForwardMessage(selectedMessage))
|
||||
}
|
||||
}
|
||||
default:
|
||||
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||
}
|
||||
|
||||
updatedTable, tableMsgs := m.table.Update(msg)
|
||||
updatedViewport, viewportMsgs := m.viewport.Update(msg)
|
||||
|
||||
m.table = updatedTable
|
||||
m.viewport = updatedViewport
|
||||
|
||||
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs)
|
||||
}
|
||||
|
||||
func (m uiModel) View() string {
|
||||
if !m.ready {
|
||||
return "Initializing"
|
||||
}
|
||||
|
||||
if m.pendingInput != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Top,
|
||||
m.headerView(),
|
||||
m.table.View(),
|
||||
m.splitterView(),
|
||||
m.viewport.View(),
|
||||
m.textInput.View(),
|
||||
)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Top,
|
||||
m.headerView(),
|
||||
m.table.View(),
|
||||
m.splitterView(),
|
||||
m.viewport.View(),
|
||||
m.footerView(),
|
||||
)
|
||||
}
|
||||
|
||||
func (m uiModel) headerView() string {
|
||||
title := activeHeaderStyle.Render("Queue: XXX")
|
||||
line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
func (m uiModel) splitterView() string {
|
||||
title := inactiveHeaderStyle.Render("Message")
|
||||
line := inactiveHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
func (m uiModel) footerView() string {
|
||||
title := m.message
|
||||
line := strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title)))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"github.com/lmika/audax/internal/sqs-browse/models"
|
||||
)
|
||||
|
||||
type messageTableRow models.Message
|
||||
|
||||
func (mtr messageTableRow) Render(w io.Writer, model table.Model, index int) {
|
||||
firstLine := strings.SplitN(string(mtr.Data), "\n", 2)[0]
|
||||
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(fmt.Sprintf("%d", mtr.ID))
|
||||
sb.WriteString("\t")
|
||||
sb.WriteString(firstLine)
|
||||
|
||||
if index == model.Cursor() {
|
||||
fmt.Fprintln(w, model.Styles.SelectedRow.Render(sb.String()))
|
||||
} else {
|
||||
fmt.Fprintln(w, sb.String())
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
)
|
||||
|
||||
type NewParameterListMsg struct {
|
||||
Prefix string
|
||||
Parameters *models.SSMParameters
|
||||
}
|
||||
|
||||
func (rs NewParameterListMsg) StatusMessage() string {
|
||||
return fmt.Sprintf("%d items returned", len(rs.Parameters.Items))
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
"github.com/lmika/audax/internal/ssm-browse/services/ssmparameters"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type SSMController struct {
|
||||
service *ssmparameters.Service
|
||||
|
||||
// state
|
||||
mutex *sync.Mutex
|
||||
prefix string
|
||||
}
|
||||
|
||||
func New(service *ssmparameters.Service) *SSMController {
|
||||
return &SSMController{
|
||||
service: service,
|
||||
prefix: "/",
|
||||
mutex: new(sync.Mutex),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SSMController) Fetch() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
res, err := c.service.List(context.Background(), c.prefix)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SSMController) ChangePrefix(newPrefix string) tea.Msg {
|
||||
res, err := c.service.List(context.Background(), newPrefix)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.prefix = newPrefix
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SSMController) Clone(param models.SSMParameter) tea.Msg {
|
||||
return events.PromptForInput("New key: ", nil, func(value string) tea.Msg {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
if err := c.service.Clone(ctx, param, value); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
res, err := c.service.List(context.Background(), c.prefix)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *SSMController) DeleteParameter(param models.SSMParameter) tea.Msg {
|
||||
return events.ConfirmYes("delete parameter? ", func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
if err := c.service.Delete(ctx, param); err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
res, err := c.service.List(context.Background(), c.prefix)
|
||||
if err != nil {
|
||||
return events.Error(err)
|
||||
}
|
||||
|
||||
return NewParameterListMsg{
|
||||
Prefix: c.prefix,
|
||||
Parameters: res,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package models
|
||||
|
||||
import "github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||
|
||||
type SSMParameters struct {
|
||||
Items []SSMParameter
|
||||
}
|
||||
|
||||
type SSMParameter struct {
|
||||
Name string
|
||||
Type types.ParameterType
|
||||
Value string
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package awsssm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
const defaultKMSKeyIDForSecureStrings = "alias/aws/ssm"
|
||||
|
||||
type Provider struct {
|
||||
client *ssm.Client
|
||||
}
|
||||
|
||||
func NewProvider(client *ssm.Client) *Provider {
|
||||
return &Provider{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) List(ctx context.Context, prefix string, maxCount int) (*models.SSMParameters, error) {
|
||||
log.Printf("new prefix: %v", prefix)
|
||||
|
||||
pager := ssm.NewGetParametersByPathPaginator(p.client, &ssm.GetParametersByPathInput{
|
||||
Path: aws.String(prefix),
|
||||
Recursive: true,
|
||||
WithDecryption: true,
|
||||
})
|
||||
|
||||
items := make([]models.SSMParameter, 0)
|
||||
outer:
|
||||
for pager.HasMorePages() {
|
||||
out, err := pager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get parameters from path")
|
||||
}
|
||||
|
||||
for _, p := range out.Parameters {
|
||||
items = append(items, models.SSMParameter{
|
||||
Name: aws.ToString(p.Name),
|
||||
Type: p.Type,
|
||||
Value: aws.ToString(p.Value),
|
||||
})
|
||||
if len(items) >= maxCount {
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &models.SSMParameters{Items: items}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) Put(ctx context.Context, param models.SSMParameter, override bool) error {
|
||||
in := &ssm.PutParameterInput{
|
||||
Name: aws.String(param.Name),
|
||||
Type: param.Type,
|
||||
Value: aws.String(param.Value),
|
||||
Overwrite: override,
|
||||
}
|
||||
if param.Type == types.ParameterTypeSecureString {
|
||||
in.KeyId = aws.String(defaultKMSKeyIDForSecureStrings)
|
||||
}
|
||||
|
||||
_, err := p.client.PutParameter(ctx, in)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to put new SSM parameter")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) Delete(ctx context.Context, param models.SSMParameter) error {
|
||||
_, err := p.client.DeleteParameter(ctx, &ssm.DeleteParameterInput{
|
||||
Name: aws.String(param.Name),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to delete SSM parameter")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package ssmparameters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
)
|
||||
|
||||
type SSMProvider interface {
|
||||
List(ctx context.Context, prefix string, maxCount int) (*models.SSMParameters, error)
|
||||
Put(ctx context.Context, param models.SSMParameter, override bool) error
|
||||
Delete(ctx context.Context, param models.SSMParameter) error
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package ssmparameters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
provider SSMProvider
|
||||
}
|
||||
|
||||
func NewService(provider SSMProvider) *Service {
|
||||
return &Service{
|
||||
provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, prefix string) (*models.SSMParameters, error) {
|
||||
return s.provider.List(ctx, prefix, 100)
|
||||
}
|
||||
|
||||
func (s *Service) Clone(ctx context.Context, param models.SSMParameter, newName string) error {
|
||||
newParam := models.SSMParameter{
|
||||
Name: newName,
|
||||
Type: param.Type,
|
||||
Value: param.Value,
|
||||
}
|
||||
return s.provider.Put(ctx, newParam, false)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, param models.SSMParameter) error {
|
||||
return s.provider.Delete(ctx, param)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/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("#c144ff")),
|
||||
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")),
|
||||
},
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/audax/internal/common/ui/commandctrl"
|
||||
"github.com/lmika/audax/internal/common/ui/events"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/statusandprompt"
|
||||
"github.com/lmika/audax/internal/ssm-browse/controllers"
|
||||
"github.com/lmika/audax/internal/ssm-browse/styles"
|
||||
"github.com/lmika/audax/internal/ssm-browse/ui/ssmdetails"
|
||||
"github.com/lmika/audax/internal/ssm-browse/ui/ssmlist"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
cmdController *commandctrl.CommandController
|
||||
controller *controllers.SSMController
|
||||
statusAndPrompt *statusandprompt.StatusAndPrompt
|
||||
|
||||
root tea.Model
|
||||
ssmList *ssmlist.Model
|
||||
ssmDetails *ssmdetails.Model
|
||||
}
|
||||
|
||||
func NewModel(controller *controllers.SSMController, cmdController *commandctrl.CommandController) Model {
|
||||
defaultStyles := styles.DefaultStyles
|
||||
ssmList := ssmlist.New(defaultStyles.Frames)
|
||||
ssmdDetails := ssmdetails.New(defaultStyles.Frames)
|
||||
statusAndPrompt := statusandprompt.New(
|
||||
layout.NewVBox(layout.LastChildFixedAt(17), ssmList, ssmdDetails), "", defaultStyles.StatusAndPrompt)
|
||||
|
||||
cmdController.AddCommands(&commandctrl.CommandList{
|
||||
Commands: map[string]commandctrl.Command{
|
||||
"clone": func(ec commandctrl.ExecContext, args []string) tea.Msg {
|
||||
if currentParam := ssmList.CurrentParameter(); currentParam != nil {
|
||||
return controller.Clone(*currentParam)
|
||||
}
|
||||
return events.Error(errors.New("no parameter selected"))
|
||||
},
|
||||
"delete": func(ec commandctrl.ExecContext, args []string) tea.Msg {
|
||||
if currentParam := ssmList.CurrentParameter(); currentParam != nil {
|
||||
return controller.DeleteParameter(*currentParam)
|
||||
}
|
||||
return events.Error(errors.New("no parameter selected"))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
root := layout.FullScreen(statusAndPrompt)
|
||||
|
||||
return Model{
|
||||
controller: controller,
|
||||
cmdController: cmdController,
|
||||
root: root,
|
||||
statusAndPrompt: statusAndPrompt,
|
||||
ssmList: ssmList,
|
||||
ssmDetails: ssmdDetails,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return m.controller.Fetch()
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case controllers.NewParameterListMsg:
|
||||
m.ssmList.SetPrefix(msg.Prefix)
|
||||
m.ssmList.SetParameters(msg.Parameters)
|
||||
case ssmlist.NewSSMParameterSelected:
|
||||
m.ssmDetails.SetSelectedItem(msg)
|
||||
case tea.KeyMsg:
|
||||
if !m.statusAndPrompt.InPrompt() {
|
||||
switch msg.String() {
|
||||
// TEMP
|
||||
case ":":
|
||||
return m, func() tea.Msg { return m.cmdController.Prompt() }
|
||||
// END TEMP
|
||||
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newRoot, cmd := m.root.Update(msg)
|
||||
m.root = newRoot
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
return m.root.View()
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package ssmdetails
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
viewport viewport.Model
|
||||
w, h int
|
||||
|
||||
// model state
|
||||
hasSelectedItem bool
|
||||
selectedItem *models.SSMParameter
|
||||
}
|
||||
|
||||
func New(style frame.Style) *Model {
|
||||
viewport := viewport.New(0, 0)
|
||||
viewport.SetContent("")
|
||||
return &Model{
|
||||
frameTitle: frame.NewFrameTitle("Item", false, style),
|
||||
viewport: viewport,
|
||||
}
|
||||
}
|
||||
|
||||
func (*Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) SetSelectedItem(item *models.SSMParameter) {
|
||||
m.selectedItem = item
|
||||
|
||||
if m.selectedItem != nil {
|
||||
var viewportContents strings.Builder
|
||||
fmt.Fprintf(&viewportContents, "Name: %v\n\n", item.Name)
|
||||
fmt.Fprintf(&viewportContents, "Type: TODO\n\n")
|
||||
fmt.Fprintf(&viewportContents, "%v\n", item.Value)
|
||||
|
||||
m.viewport.SetContent(viewportContents.String())
|
||||
} else {
|
||||
m.viewport.SetContent("(no parameter selected)")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.viewport.View())
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.w, m.h = w, h
|
||||
m.frameTitle.Resize(w, h)
|
||||
m.viewport.Width = w
|
||||
m.viewport.Height = h - m.frameTitle.HeaderHeight()
|
||||
return m
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package ssmlist
|
||||
|
||||
import "github.com/lmika/audax/internal/ssm-browse/models"
|
||||
|
||||
type NewSSMParameterSelected *models.SSMParameter
|
|
@ -1,98 +0,0 @@
|
|||
package ssmlist
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/frame"
|
||||
"github.com/lmika/audax/internal/dynamo-browse/ui/teamodels/layout"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
frameTitle frame.FrameTitle
|
||||
table table.Model
|
||||
|
||||
parameters *models.SSMParameters
|
||||
|
||||
w, h int
|
||||
}
|
||||
|
||||
func New(style frame.Style) *Model {
|
||||
frameTitle := frame.NewFrameTitle("SSM: /", true, style)
|
||||
table := table.New(table.SimpleColumns{"name", "type", "value"}, 0, 0)
|
||||
|
||||
return &Model{
|
||||
frameTitle: frameTitle,
|
||||
table: table,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SetPrefix(newPrefix string) {
|
||||
m.frameTitle.SetTitle("SSM: " + newPrefix)
|
||||
}
|
||||
|
||||
func (m *Model) SetParameters(parameters *models.SSMParameters) {
|
||||
m.parameters = parameters
|
||||
cols := table.SimpleColumns{"name", "type", "value"}
|
||||
|
||||
newTbl := table.New(cols, m.w, m.h-m.frameTitle.HeaderHeight())
|
||||
newRows := make([]table.Row, len(parameters.Items))
|
||||
for i, r := range parameters.Items {
|
||||
newRows[i] = itemTableRow{r}
|
||||
}
|
||||
newTbl.SetRows(newRows)
|
||||
|
||||
m.table = newTbl
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
//var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "i", "up":
|
||||
m.table.GoUp()
|
||||
return m, m.emitNewSelectedParameter()
|
||||
case "k", "down":
|
||||
m.table.GoDown()
|
||||
return m, m.emitNewSelectedParameter()
|
||||
}
|
||||
//m.table, cmd = m.table.Update(msg)
|
||||
//return m, cmd
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) emitNewSelectedParameter() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if row, ok := m.table.SelectedRow().(itemTableRow); ok {
|
||||
return NewSSMParameterSelected(&(row.item))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) CurrentParameter() *models.SSMParameter {
|
||||
if row, ok := m.table.SelectedRow().(itemTableRow); ok {
|
||||
return &(row.item)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, m.frameTitle.View(), m.table.View())
|
||||
}
|
||||
|
||||
func (m *Model) Resize(w, h int) layout.ResizingModel {
|
||||
m.w, m.h = w, h
|
||||
m.frameTitle.Resize(w, h)
|
||||
m.table.SetSize(w, h-m.frameTitle.HeaderHeight())
|
||||
return m
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package ssmlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
table "github.com/lmika/go-bubble-table"
|
||||
"github.com/lmika/audax/internal/ssm-browse/models"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type itemTableRow struct {
|
||||
item models.SSMParameter
|
||||
}
|
||||
|
||||
func (mtr itemTableRow) Render(w io.Writer, model table.Model, index int) {
|
||||
firstLine := strings.SplitN(mtr.item.Value, "\n", 2)[0]
|
||||
line := fmt.Sprintf("%s\t%s\t%s", mtr.item.Name, mtr.item.Type, firstLine)
|
||||
|
||||
if index == model.Cursor() {
|
||||
fmt.Fprintln(w, model.Styles.SelectedRow.Render(line))
|
||||
} else {
|
||||
fmt.Fprintln(w, line)
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||
"github.com/lmika/gopkgs/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
cli.Fatalf("cannot load AWS config: %v", err)
|
||||
}
|
||||
|
||||
ssmClient := ssm.NewFromConfig(cfg,
|
||||
ssm.WithEndpointResolver(ssm.EndpointResolverFromURL("http://localhost:4566")))
|
||||
|
||||
if _, err := ssmClient.PutParameter(ctx, &ssm.PutParameterInput{
|
||||
Name: aws.String("/alpha/bravo"),
|
||||
Type: types.ParameterTypeString,
|
||||
Value: aws.String("This is a parameter value"),
|
||||
}); err != nil {
|
||||
cli.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue