aea21d713c
Parsea flags de -help de cada pipeline para mostrar formulario de argumentos antes de ejecutar. Filtra pipelines por tag 'launcher'. Corrige selección en historial delegando enter al list antes de leer item.
220 lines
5.4 KiB
Go
220 lines
5.4 KiB
Go
package views
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
ops "fn-registry/fn_operations"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/lucasdataproyects/devfactory/tui"
|
|
)
|
|
|
|
type historyState int
|
|
|
|
const (
|
|
historyLoading historyState = iota
|
|
historyList
|
|
historyDetail
|
|
)
|
|
|
|
type historyLoadedMsg []ops.Execution
|
|
|
|
// HistoryModel shows execution history.
|
|
type HistoryModel struct {
|
|
state historyState
|
|
list tui.FilteredListModel
|
|
spinner tui.SpinnerModel
|
|
styles tui.Styles
|
|
executions []ops.Execution
|
|
detail string
|
|
scrollOff int
|
|
opsDB *ops.DB
|
|
pipelineNames map[string]string
|
|
}
|
|
|
|
// NewHistoryModel creates a new history view.
|
|
func NewHistoryModel(styles tui.Styles, opsDB *ops.DB, names map[string]string) HistoryModel {
|
|
return HistoryModel{
|
|
state: historyLoading,
|
|
list: tui.NewFilteredList(nil, "Filter executions..."),
|
|
spinner: tui.NewSpinner("Loading history..."),
|
|
styles: styles,
|
|
opsDB: opsDB,
|
|
pipelineNames: names,
|
|
}
|
|
}
|
|
|
|
func (m HistoryModel) Init() tea.Cmd {
|
|
return tea.Batch(
|
|
m.spinner.Init(),
|
|
m.loadHistory(),
|
|
)
|
|
}
|
|
|
|
func (m HistoryModel) loadHistory() tea.Cmd {
|
|
return func() tea.Msg {
|
|
execs, err := m.opsDB.ListExecutions("", "", "")
|
|
if err != nil {
|
|
return historyLoadedMsg(nil)
|
|
}
|
|
return historyLoadedMsg(execs)
|
|
}
|
|
}
|
|
|
|
func (m HistoryModel) Update(msg tea.Msg) (HistoryModel, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case historyLoadedMsg:
|
|
m.executions = []ops.Execution(msg)
|
|
items := make([]tui.ListItem, len(m.executions))
|
|
for i, e := range m.executions {
|
|
icon := "●"
|
|
switch e.Status {
|
|
case ops.ExecSuccess:
|
|
icon = "✓"
|
|
case ops.ExecFailure:
|
|
icon = "✗"
|
|
case ops.ExecPartial:
|
|
icon = "~"
|
|
}
|
|
name := e.PipelineID
|
|
if n, ok := m.pipelineNames[e.PipelineID]; ok {
|
|
name = n
|
|
}
|
|
dur := ""
|
|
if e.DurationMs != nil {
|
|
dur = fmt.Sprintf("%dms", *e.DurationMs)
|
|
}
|
|
items[i] = tui.ListItem{
|
|
Title: fmt.Sprintf("%s %s", icon, name),
|
|
Description: fmt.Sprintf("%s — %s — %s", string(e.Status), dur, e.StartedAt.Format("2006-01-02 15:04:05")),
|
|
Value: e,
|
|
}
|
|
}
|
|
m.list.SetItems(items)
|
|
m.state = historyList
|
|
return m, nil
|
|
|
|
case tea.KeyMsg:
|
|
switch m.state {
|
|
case historyList:
|
|
switch msg.String() {
|
|
case "r":
|
|
m.state = historyLoading
|
|
m.spinner = tui.NewSpinner("Loading history...")
|
|
return m, tea.Batch(m.spinner.Init(), m.loadHistory())
|
|
case "enter":
|
|
// Delegate enter to list first so it selects the cursor item
|
|
updated, _ := m.list.Update(msg)
|
|
m.list = updated.(tui.FilteredListModel)
|
|
if item := m.list.SelectedItem(); item != nil {
|
|
e := item.Value.(ops.Execution)
|
|
m.detail = formatExecution(e)
|
|
m.state = historyDetail
|
|
m.scrollOff = 0
|
|
}
|
|
return m, nil
|
|
}
|
|
case historyDetail:
|
|
switch msg.String() {
|
|
case "j", "down":
|
|
m.scrollOff++
|
|
case "k", "up":
|
|
if m.scrollOff > 0 {
|
|
m.scrollOff--
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
}
|
|
|
|
// Delegate to sub-components
|
|
var cmd tea.Cmd
|
|
switch m.state {
|
|
case historyLoading:
|
|
var spinnerModel tea.Model
|
|
spinnerModel, cmd = m.spinner.Update(msg)
|
|
m.spinner = spinnerModel.(tui.SpinnerModel)
|
|
case historyList:
|
|
var listModel tea.Model
|
|
listModel, cmd = m.list.Update(msg)
|
|
m.list = listModel.(tui.FilteredListModel)
|
|
}
|
|
return m, cmd
|
|
}
|
|
|
|
// HandleBack retrocede un nivel. Retorna true si ya en estado base.
|
|
func (m *HistoryModel) HandleBack() bool {
|
|
switch m.state {
|
|
case historyDetail:
|
|
m.state = historyList
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (m HistoryModel) View() string {
|
|
switch m.state {
|
|
case historyLoading:
|
|
return m.spinner.View()
|
|
case historyList:
|
|
if len(m.executions) == 0 {
|
|
return m.styles.Muted.Render("No executions found. Launch a pipeline first.")
|
|
}
|
|
help := m.styles.Muted.Render(" Enter: details │ r: refresh │ /: filter")
|
|
return m.list.View() + "\n" + help
|
|
case historyDetail:
|
|
return m.renderDetail()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m HistoryModel) renderDetail() string {
|
|
lines := splitLines(m.detail)
|
|
maxLines := 20
|
|
if m.scrollOff >= len(lines) {
|
|
m.scrollOff = max(0, len(lines)-1)
|
|
}
|
|
end := min(m.scrollOff+maxLines, len(lines))
|
|
visible := lines[m.scrollOff:end]
|
|
|
|
header := m.styles.Header.Render("Execution Detail")
|
|
content := lipgloss.JoinVertical(lipgloss.Left, visible...)
|
|
help := m.styles.Muted.Render(" j/k: scroll │ Esc: back")
|
|
|
|
return header + "\n" + content + "\n" + help
|
|
}
|
|
|
|
func formatExecution(e ops.Execution) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(fmt.Sprintf("ID: %s\n", e.ID))
|
|
sb.WriteString(fmt.Sprintf("Pipeline: %s\n", e.PipelineID))
|
|
sb.WriteString(fmt.Sprintf("Status: %s\n", e.Status))
|
|
sb.WriteString(fmt.Sprintf("Started: %s\n", e.StartedAt.Format("2006-01-02 15:04:05")))
|
|
if e.EndedAt != nil {
|
|
sb.WriteString(fmt.Sprintf("Ended: %s\n", e.EndedAt.Format("2006-01-02 15:04:05")))
|
|
}
|
|
if e.DurationMs != nil {
|
|
sb.WriteString(fmt.Sprintf("Duration: %dms\n", *e.DurationMs))
|
|
}
|
|
if e.RecordsIn != nil {
|
|
sb.WriteString(fmt.Sprintf("Records In: %d\n", *e.RecordsIn))
|
|
}
|
|
if e.RecordsOut != nil {
|
|
sb.WriteString(fmt.Sprintf("Records Out: %d\n", *e.RecordsOut))
|
|
}
|
|
if e.Error != "" {
|
|
sb.WriteString(fmt.Sprintf("\n--- Error ---\n%s\n", e.Error))
|
|
}
|
|
if len(e.Metrics) > 0 {
|
|
sb.WriteString("\n--- Metrics ---\n")
|
|
b, _ := json.MarshalIndent(e.Metrics, "", " ")
|
|
sb.WriteString(string(b))
|
|
sb.WriteString("\n")
|
|
}
|
|
return sb.String()
|
|
}
|