feat: implement split history view, jira tickets and description
This commit is contained in:
241
cmd/cli/main.go
241
cmd/cli/main.go
@@ -10,6 +10,7 @@ import (
|
||||
"envguard/internal/models"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
@@ -24,6 +25,7 @@ type viewState int
|
||||
const (
|
||||
viewList viewState = iota
|
||||
viewDetail
|
||||
viewLockForm
|
||||
)
|
||||
|
||||
type model struct {
|
||||
@@ -37,6 +39,8 @@ type model struct {
|
||||
state viewState
|
||||
history []models.HistoryEntry
|
||||
historyTable table.Model
|
||||
inputs []textinput.Model
|
||||
focusIndex int
|
||||
}
|
||||
|
||||
type tickMsg time.Time
|
||||
@@ -59,8 +63,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tickMsg:
|
||||
if m.state == viewLockForm {
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Batch(fetchServices, tick())
|
||||
case tea.KeyMsg:
|
||||
if m.state == viewLockForm {
|
||||
return m.updateLockForm(msg)
|
||||
}
|
||||
switch msg.String() {
|
||||
case "esc", "q", "ctrl+c":
|
||||
if m.state == viewDetail {
|
||||
@@ -99,17 +109,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
} else {
|
||||
m.message = "Unlocked " + serviceName
|
||||
}
|
||||
return m, fetchServices
|
||||
} else {
|
||||
m.message = "Cannot unlock service locked by " + s.LockedBy
|
||||
}
|
||||
} else {
|
||||
// Lock
|
||||
err := api.LockService(serviceName, m.user)
|
||||
if err != nil {
|
||||
m.message = "Error locking: " + err.Error()
|
||||
} else {
|
||||
m.message = "Locked " + serviceName
|
||||
}
|
||||
// Open Lock Form
|
||||
m.state = viewLockForm
|
||||
m.inputs[0].SetValue("")
|
||||
m.inputs[1].SetValue("")
|
||||
m.focusIndex = 0
|
||||
m.inputs[0].Focus()
|
||||
return m, nil
|
||||
}
|
||||
return m, fetchServices
|
||||
}
|
||||
@@ -124,25 +135,64 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Calculate available dimensions
|
||||
// Borders take 2 chars total width (1 left + 1 right)
|
||||
// We add extra safety margin (total -6) to avoid last-column wrap issues
|
||||
availWidth := m.width - 10
|
||||
availWidth := m.width - 15
|
||||
|
||||
// Adjust column widths dynamically
|
||||
// Total weight = 100%
|
||||
cols := m.table.Columns()
|
||||
// Calculate widths ensuring at least minimums
|
||||
c0 := int(float64(availWidth) * 0.3)
|
||||
c1 := int(float64(availWidth) * 0.2)
|
||||
c2 := int(float64(availWidth) * 0.2)
|
||||
c3 := availWidth - c0 - c1 - c2
|
||||
// Columns: SERVICE, STATUS, USER, TICKETS, TIME
|
||||
// Main View
|
||||
c0 := int(float64(availWidth) * 0.25) // SERVICE
|
||||
c1 := int(float64(availWidth) * 0.15) // STATUS
|
||||
c2 := int(float64(availWidth) * 0.15) // USER
|
||||
c3 := int(float64(availWidth) * 0.20) // TICKETS
|
||||
c4 := availWidth - c0 - c1 - c2 - c3 // TIME
|
||||
|
||||
cols[0].Width = c0
|
||||
cols[1].Width = c1
|
||||
cols[2].Width = c2
|
||||
cols[3].Width = c3
|
||||
cols[4].Width = c4
|
||||
|
||||
m.table.SetColumns(cols)
|
||||
m.table.SetWidth(availWidth)
|
||||
|
||||
// History View (Split 50/50 approx)
|
||||
// Left Pane (Table)
|
||||
// Columns: ACTION, USER, TIME (Tickets and Desc moved to right pane)
|
||||
// but wait, we need to match the history table initialization cols...
|
||||
// Let's redefine history table cols to match the new layout
|
||||
|
||||
hWidth := availWidth / 2
|
||||
hc0 := int(float64(hWidth) * 0.2) // ACTION
|
||||
hc1 := int(float64(hWidth) * 0.3) // USER
|
||||
hc2 := hWidth - hc0 - hc1 - 5 // TIME (approx)
|
||||
|
||||
hCols := m.historyTable.Columns()
|
||||
// Ensure we have correct number of columns.
|
||||
// Previous task added TICKETS and DESC columns. We will remove them from the table
|
||||
// and show them in the right pane instead, to save space.
|
||||
// Wait, I should not change the columns definition in main() if I don't want to re-init.
|
||||
// Re-init in main() is better. I will do that in a separate chunk.
|
||||
// For now, let's assume I will reduce the history table columns to 3.
|
||||
|
||||
// Actually, I can just hide or set width 0 for columns I don't want to see?
|
||||
// No, better to stick to 3 visible columns for the table.
|
||||
|
||||
if len(hCols) >= 3 {
|
||||
hCols[0].Width = hc0
|
||||
hCols[1].Width = hc1
|
||||
hCols[len(hCols)-1].Width = hc2
|
||||
// Set middle columns to 0 if they exist (TICKETS, DESC)
|
||||
for i := 2; i < len(hCols)-1; i++ {
|
||||
hCols[i].Width = 0
|
||||
}
|
||||
}
|
||||
|
||||
m.historyTable.SetColumns(hCols)
|
||||
m.historyTable.SetWidth(hWidth)
|
||||
|
||||
// Header + Footer + padding calculation
|
||||
availableHeight := m.height - 7
|
||||
if availableHeight < 5 {
|
||||
@@ -167,7 +217,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
timeStr = fmt.Sprintf("Hace %s", duration.Round(time.Second))
|
||||
}
|
||||
|
||||
rows = append(rows, table.Row{s.Name, status, user, timeStr})
|
||||
rows = append(rows, table.Row{s.Name, status, user, s.JiraTickets, timeStr})
|
||||
}
|
||||
m.table.SetRows(rows)
|
||||
m.message = "Refreshed"
|
||||
@@ -178,12 +228,37 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
rows = append(rows, table.Row{
|
||||
h.Action,
|
||||
h.User,
|
||||
h.JiraTickets,
|
||||
h.Description,
|
||||
h.Timestamp.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
m.historyTable.SetRows(rows)
|
||||
// Update footer logic to resize history table if needed
|
||||
m.historyTable.SetWidth(m.width - 10)
|
||||
m.historyTable.SetRows(rows)
|
||||
// Resize handled in WindowSizeMsg now, but we need to trigger it if not triggered.
|
||||
// Actually m.width is stored.
|
||||
|
||||
// Recalculate widths just in case
|
||||
availWidth := m.width - 10
|
||||
if availWidth > 0 {
|
||||
hWidth := availWidth / 2
|
||||
hc0 := int(float64(hWidth) * 0.2) // ACTION
|
||||
hc1 := int(float64(hWidth) * 0.3) // USER
|
||||
hc2 := hWidth - hc0 - hc1 - 5 // TIME
|
||||
|
||||
hCols := m.historyTable.Columns()
|
||||
if len(hCols) >= 3 {
|
||||
hCols[0].Width = hc0
|
||||
hCols[1].Width = hc1
|
||||
hCols[len(hCols)-1].Width = hc2
|
||||
for i := 2; i < len(hCols)-1; i++ {
|
||||
hCols[i].Width = 0
|
||||
}
|
||||
}
|
||||
m.historyTable.SetColumns(hCols)
|
||||
m.historyTable.SetWidth(hWidth)
|
||||
}
|
||||
|
||||
m.historyTable.SetHeight(m.table.Height())
|
||||
case error:
|
||||
m.err = msg
|
||||
@@ -197,6 +272,70 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m model) updateLockForm(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.state = viewList
|
||||
m.table.Focus()
|
||||
return m, nil
|
||||
case "tab", "shift+tab", "enter", "up", "down":
|
||||
s := msg.String()
|
||||
|
||||
if s == "enter" && m.focusIndex == len(m.inputs)-1 {
|
||||
// Submit
|
||||
serviceName := m.table.SelectedRow()[0]
|
||||
jira := m.inputs[0].Value()
|
||||
desc := m.inputs[1].Value()
|
||||
|
||||
err := api.LockService(serviceName, m.user, jira, desc)
|
||||
if err != nil {
|
||||
m.message = "Error locking: " + err.Error()
|
||||
} else {
|
||||
m.message = "Locked " + serviceName
|
||||
}
|
||||
m.state = viewList
|
||||
m.table.Focus()
|
||||
return m, fetchServices
|
||||
}
|
||||
|
||||
if s == "up" || s == "shift+tab" {
|
||||
m.focusIndex--
|
||||
} else {
|
||||
m.focusIndex++
|
||||
}
|
||||
|
||||
if m.focusIndex > len(m.inputs)-1 {
|
||||
m.focusIndex = 0
|
||||
} else if m.focusIndex < 0 {
|
||||
m.focusIndex = len(m.inputs) - 1
|
||||
}
|
||||
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := 0; i <= len(m.inputs)-1; i++ {
|
||||
if i == m.focusIndex {
|
||||
cmds[i] = m.inputs[i].Focus()
|
||||
continue
|
||||
}
|
||||
m.inputs[i].Blur()
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := m.updateInputs(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
|
||||
cmds := make([]tea.Cmd, len(m.inputs))
|
||||
for i := range m.inputs {
|
||||
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
if m.err != nil {
|
||||
return fmt.Sprintf("Error: %v\nPress q to quit", m.err)
|
||||
@@ -245,11 +384,55 @@ func (m model) View() string {
|
||||
var mainView string
|
||||
if m.state == viewList {
|
||||
mainView = m.table.View()
|
||||
} else if m.state == viewLockForm {
|
||||
mainView = lipgloss.JoinVertical(lipgloss.Left,
|
||||
fmt.Sprintf("Locking service: %s", m.table.SelectedRow()[0]),
|
||||
"",
|
||||
"Jira Tickets:",
|
||||
m.inputs[0].View(),
|
||||
"",
|
||||
"Description:",
|
||||
m.inputs[1].View(),
|
||||
"",
|
||||
"(Enter to submit, Esc to cancel)",
|
||||
)
|
||||
} else {
|
||||
// History View
|
||||
mainView = lipgloss.JoinVertical(lipgloss.Left,
|
||||
"History for selected service:",
|
||||
m.historyTable.View(),
|
||||
// Left Pane: History Table
|
||||
leftPane := m.historyTable.View()
|
||||
|
||||
// Right Pane: Details
|
||||
var rightPane string
|
||||
selected := m.historyTable.SelectedRow()
|
||||
if selected != nil {
|
||||
// Extract data from selected row
|
||||
// Columns: ACTION, USER, TICKETS, DESC, TIME
|
||||
// Indices: 0, 1, 2, 3, 4
|
||||
action := selected[0]
|
||||
user := selected[1]
|
||||
tickets := selected[2]
|
||||
desc := selected[3]
|
||||
timestamp := selected[4]
|
||||
|
||||
detailStyle := lipgloss.NewStyle().
|
||||
Padding(1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("63")).
|
||||
Width(m.width/2 - 5).
|
||||
Height(m.height - 8)
|
||||
|
||||
content := fmt.Sprintf(
|
||||
"ACTION: %s\n\nUSER: %s\n\nTIME: %s\n\nJIRA TICKETS:\n%s\n\nDESCRIPTION:\n%s",
|
||||
action, user, timestamp, tickets, desc,
|
||||
)
|
||||
rightPane = detailStyle.Render(content)
|
||||
} else {
|
||||
rightPane = "Select an entry to view details."
|
||||
}
|
||||
|
||||
mainView = lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
leftPane,
|
||||
rightPane,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -263,8 +446,8 @@ func (m model) View() string {
|
||||
|
||||
// Create Border Style locally
|
||||
borderStyle := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
// BorderStyle(lipgloss.NormalBorder()).
|
||||
// BorderForeground(lipgloss.Color("240")).
|
||||
Width(appWidth).
|
||||
Height(appHeight - 2)
|
||||
|
||||
@@ -308,6 +491,7 @@ func main() {
|
||||
{Title: "SERVICE", Width: 20},
|
||||
{Title: "STATUS", Width: 15},
|
||||
{Title: "USER", Width: 15},
|
||||
{Title: "TICKETS", Width: 15},
|
||||
{Title: "TIME", Width: 20},
|
||||
}
|
||||
|
||||
@@ -330,9 +514,13 @@ func main() {
|
||||
t.SetStyles(s)
|
||||
|
||||
// History Table
|
||||
// Reduced columns for split view
|
||||
hCols := []table.Column{
|
||||
{Title: "ACTION", Width: 10},
|
||||
{Title: "USER", Width: 15},
|
||||
// Hidden columns for data storage in row, but width 0
|
||||
{Title: "TICKETS", Width: 0},
|
||||
{Title: "DESC", Width: 0},
|
||||
{Title: "TIME", Width: 20},
|
||||
}
|
||||
ht := table.New(
|
||||
@@ -342,10 +530,23 @@ func main() {
|
||||
)
|
||||
ht.SetStyles(s)
|
||||
|
||||
inputs := make([]textinput.Model, 2)
|
||||
inputs[0] = textinput.New()
|
||||
inputs[0].Placeholder = "D3A-XXXX"
|
||||
inputs[0].Focus()
|
||||
inputs[0].CharLimit = 50
|
||||
inputs[0].Width = 30
|
||||
|
||||
inputs[1] = textinput.New()
|
||||
inputs[1].Placeholder = "What are you doing?"
|
||||
inputs[1].CharLimit = 100
|
||||
inputs[1].Width = 50
|
||||
|
||||
m := model{
|
||||
table: t,
|
||||
historyTable: ht,
|
||||
user: user,
|
||||
inputs: inputs,
|
||||
}
|
||||
|
||||
if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil {
|
||||
|
||||
@@ -129,6 +129,8 @@ func handleLock(w http.ResponseWriter, r *http.Request) {
|
||||
services[i].IsLocked = true
|
||||
services[i].LockedBy = req.User
|
||||
services[i].LockedAt = time.Now()
|
||||
services[i].JiraTickets = req.JiraTickets
|
||||
services[i].Description = req.Description
|
||||
|
||||
if err := saveServices(); err != nil {
|
||||
log.Printf("Error guardando estado: %v", err)
|
||||
@@ -140,6 +142,8 @@ func handleLock(w http.ResponseWriter, r *http.Request) {
|
||||
Action: "LOCK",
|
||||
User: req.User,
|
||||
Timestamp: time.Now(),
|
||||
JiraTickets: req.JiraTickets,
|
||||
Description: req.Description,
|
||||
})
|
||||
saveHistory()
|
||||
|
||||
@@ -179,6 +183,8 @@ func handleUnlock(w http.ResponseWriter, r *http.Request) {
|
||||
services[i].IsLocked = false
|
||||
services[i].LockedBy = ""
|
||||
services[i].LockedAt = time.Time{}
|
||||
services[i].JiraTickets = ""
|
||||
services[i].Description = ""
|
||||
|
||||
if err := saveServices(); err != nil {
|
||||
log.Printf("Error guardando estado: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user