diff --git a/apps/cli/internal/app/dashboard.go b/apps/cli/internal/app/dashboard.go new file mode 100644 index 0000000..284f797 --- /dev/null +++ b/apps/cli/internal/app/dashboard.go @@ -0,0 +1,231 @@ +package app + +import ( + "fmt" + "os" + "time" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type Dashboard struct { + table table.Model + spinner spinner.Model + help help.Model + keys keyMap + quitting bool + loading bool + lastUpdate time.Time +} + +type keyMap struct { + refresh key.Binding + subscribers key.Binding + services key.Binding + billing key.Binding + monitoring key.Binding + config key.Binding + deploy key.Binding + help key.Binding + quit key.Binding +} + +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.help, k.quit} +} + +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.refresh, k.subscribers, k.services}, + {k.billing, k.monitoring, k.config}, + {k.deploy, k.help, k.quit}, + } +} + +var defaultKeyMap = keyMap{ + refresh: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "refresh")), + subscribers: key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "subscribers")), + services: key.NewBinding(key.WithKeys("v"), key.WithHelp("v", "services")), + billing: key.NewBinding(key.WithKeys("b"), key.WithHelp("b", "billing")), + monitoring: key.NewBinding(key.WithKeys("m"), key.WithHelp("m", "monitoring")), + config: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "config")), + deploy: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "deploy")), + help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")), + quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")), +} + +func NewDashboard() *Dashboard { + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + + t := table.New() + t.SetColumns([]table.Column{ + {Title: "Service", Width: 20}, + {Title: "Status", Width: 10}, + {Title: "CPU", Width: 8}, + {Title: "Memory", Width: 10}, + {Title: "Uptime", Width: 12}, + {Title: "Version", Width: 10}, + }) + t.SetRows([]table.Row{ + {"API Server", "Running", "45%", "256MB", "2h15m", "v1.0.0"}, + {"Charging Engine", "Running", "23%", "128MB", "2h15m", "v1.0.0"}, + {"Packet Gateway", "Running", "67%", "512MB", "2h15m", "v1.0.0"}, + {"Web Dashboard", "Running", "12%", "64MB", "2h15m", "v1.0.0"}, + {"Prometheus", "Running", "8%", "32MB", "2h15m", "v2.45.0"}, + {"Grafana", "Running", "5%", "24MB", "2h15m", "v9.5.2"}, + }) + + t.SetStyles(table.DefaultStyles()) + + return &Dashboard{ + table: t, + spinner: s, + help: help.New(), + keys: defaultKeyMap, + lastUpdate: time.Now(), + } +} + +func (d *Dashboard) Init() tea.Cmd { + return tea.Batch(d.spinner.Tick) +} + +func (d *Dashboard) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, d.keys.quit): + d.quitting = true + return d, tea.Quit + case key.Matches(msg, d.keys.help): + d.help.ShowAll = !d.help.ShowAll + return d, nil + case key.Matches(msg, d.keys.refresh): + d.loading = true + return d, d.refreshData + case key.Matches(msg, d.keys.subscribers): + // Navigate to subscribers view + return d, tea.Quit + case key.Matches(msg, d.keys.services): + // Navigate to services view + return d, tea.Quit + case key.Matches(msg, d.keys.billing): + // Navigate to billing view + return d, tea.Quit + case key.Matches(msg, d.keys.monitoring): + // Navigate to monitoring view + return d, tea.Quit + case key.Matches(msg, d.keys.config): + // Navigate to config view + return d, tea.Quit + case key.Matches(msg, d.keys.deploy): + // Navigate to deploy view + return d, tea.Quit + } + // Timer removed - manual refresh only + case spinner.TickMsg: + var cmd tea.Cmd + d.spinner, cmd = d.spinner.Update(msg) + return d, cmd + } + + d.table, cmd = d.table.Update(msg) + return d, cmd +} + +func (d *Dashboard) View() string { + title := d.titleView() + table := d.table.View() + footer := d.footerView() + + helpView := "" + if d.help.ShowAll { + helpView = d.help.View(d.keys) + } + + return fmt.Sprintf( + "%s\n\n%s\n\n%s\n%s", + title, + table, + footer, + helpView, + ) +} + +func (d *Dashboard) titleView() string { + title := "Telecom Platform Dashboard" + subtitle := fmt.Sprintf("Last updated: %s", d.lastUpdate.Format("15:04:05")) + + if d.loading { + subtitle += " " + d.spinner.View() + } + + titleStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("205")). + Background(lipgloss.Color("240")). + Padding(0, 2). + Bold(true) + + subtitleStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + Padding(0, 2) + + return titleStyle.Render(title) + "\n" + subtitleStyle.Render(subtitle) +} + +func (d *Dashboard) footerView() string { + info := "Press '?' for help | 'r' to refresh | 'q' to quit" + + // Calculate totals + totalServices := len(d.table.Rows()) + runningServices := 0 + for _, row := range d.table.Rows() { + if len(row) > 1 && row[1] == "Running" { + runningServices++ + } + } + + status := fmt.Sprintf("Services: %d/%d running", runningServices, totalServices) + + footerStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + Padding(0, 2) + + return footerStyle.Render(fmt.Sprintf("%s | %s", info, status)) +} + +func (d *Dashboard) refreshData() tea.Msg { + // Simulate data refresh + time.Sleep(500 * time.Millisecond) + d.loading = false + + // Update with new data + newRows := []table.Row{ + {"API Server", "Running", fmt.Sprintf("%d%%", 40+int(time.Now().Unix()%40)), "256MB", "2h15m", "v1.0.0"}, + {"Charging Engine", "Running", fmt.Sprintf("%d%%", 20+int(time.Now().Unix()%30)), "128MB", "2h15m", "v1.0.0"}, + {"Packet Gateway", "Running", fmt.Sprintf("%d%%", 60+int(time.Now().Unix()%30)), "512MB", "2h15m", "v1.0.0"}, + {"Web Dashboard", "Running", fmt.Sprintf("%d%%", 10+int(time.Now().Unix()%20)), "64MB", "2h15m", "v1.0.0"}, + {"Prometheus", "Running", fmt.Sprintf("%d%%", 5+int(time.Now().Unix()%10)), "32MB", "2h15m", "v2.45.0"}, + {"Grafana", "Running", fmt.Sprintf("%d%%", 3+int(time.Now().Unix()%7)), "24MB", "2h15m", "v9.5.2"}, + } + + d.table.SetRows(newRows) + return nil +} + +func RunDashboard() { + p := tea.NewProgram(NewDashboard()) + if _, err := p.Run(); err != nil { + fmt.Printf("Error running dashboard: %v\n", err) + os.Exit(1) + } +}