Removed the other tools and fixed the README.md file

This commit is contained in:
Leon Mika 2023-02-23 21:45:50 +11:00
parent ab309084c5
commit 937af987e6
41 changed files with 12 additions and 2006 deletions

View file

@ -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.
![dynamo-browse](https://dynamobrowse.app/images/dynamo-browse/main-item-view.png)
![dynamo-browse](https://audax.tools/images/dynamo-browse/main-item-view.png)
## 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.

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -1,7 +0,0 @@
package controllers
import "github.com/lmika/audax/internal/slog-view/models"
type NewLogFile *models.LogFile
type ViewLogLineFullScreen *models.LogLine

View file

@ -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)
}
}

View file

@ -1,10 +0,0 @@
package models
type LogFile struct {
Filename string
Lines []LogLine
}
type LogLine struct {
JSON interface{}
}

View file

@ -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
}

View file

@ -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")),
},
}

View file

@ -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
}

View file

@ -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
}

View file

@ -1,5 +0,0 @@
package loglines
import "github.com/lmika/audax/internal/slog-view/models"
type NewLogLineSelected *models.LogLine

View file

@ -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
}

View file

@ -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)"
}
}

View file

@ -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()
}

View file

@ -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
})
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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")),
},
}

View file

@ -1,5 +0,0 @@
package ui
import "github.com/lmika/audax/internal/sqs-browse/models"
type NewMessagesEvent []*models.Message

View file

@ -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
}

View file

@ -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())
}
}

View file

@ -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))
}

View file

@ -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,
}
})
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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")),
},
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -1,5 +0,0 @@
package ssmlist
import "github.com/lmika/audax/internal/ssm-browse/models"
type NewSSMParameterSelected *models.SSMParameter

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)
}
}