ssm-browse: added structed log view

This commit is contained in:
Leon Mika 2022-03-30 15:07:49 +11:00
parent 9752bb41bc
commit b3d0fbfe29
11 changed files with 546 additions and 0 deletions

View file

@ -0,0 +1,65 @@
package fullviewlinedetails
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/slog-view/models"
"github.com/lmika/awstools/internal/slog-view/ui/linedetails"
)
type Model struct {
submodel tea.Model
lineDetails *linedetails.Model
visible bool
}
func NewModel(submodel tea.Model) *Model {
return &Model{
submodel: submodel,
lineDetails: linedetails.New(),
}
}
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)
}
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

@ -0,0 +1,77 @@
package linedetails
import (
"encoding/json"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/slog-view/models"
)
type Model struct {
frameTitle frame.FrameTitle
viewport viewport.Model
w, h int
// model state
focused bool
selectedItem *models.LogLine
}
func New() *Model {
viewport := viewport.New(0, 0)
viewport.SetContent("")
return &Model{
frameTitle: frame.NewFrameTitle("Item", false),
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

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

View file

@ -0,0 +1,98 @@
package loglines
import (
table "github.com/calyptia/go-bubble-table"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/slog-view/models"
"path/filepath"
)
type Model struct {
frameTitle frame.FrameTitle
table table.Model
logFile *models.LogFile
w, h int
}
func New() *Model {
frameTitle := frame.NewFrameTitle("File: ", true)
table := table.New([]string{"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 := []string{"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

@ -0,0 +1,61 @@
package loglines
import (
"fmt"
table "github.com/calyptia/go-bubble-table"
"github.com/lmika/awstools/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

@ -0,0 +1,81 @@
package ui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/lmika/awstools/internal/common/ui/commandctrl"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout"
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt"
"github.com/lmika/awstools/internal/slog-view/controllers"
"github.com/lmika/awstools/internal/slog-view/ui/fullviewlinedetails"
"github.com/lmika/awstools/internal/slog-view/ui/linedetails"
"github.com/lmika/awstools/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 {
logLines := loglines.New()
lineDetails := linedetails.New()
box := layout.NewVBox(layout.LastChildFixedAt(17), logLines, lineDetails)
fullViewLineDetails := fullviewlinedetails.NewModel(box)
statusAndPrompt := statusandprompt.New(fullViewLineDetails, "")
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, 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()
}