feat: implement ControlTower TUI for cluster and host monitoring
Add complete TUI application for monitoring Kubernetes clusters and host systems. Features include: Core features: - Collector framework with concurrent scheduling - Host collectors: disk, memory, load, network - Kubernetes collectors: pods, nodes, workloads, events with informers - Issue deduplication, state management, and resolve-after logic - Bubble Tea TUI with table view, details pane, and filtering - JSON export functionality UX improvements: - Help overlay with keybindings - Priority/category filters with visual indicators - Direct priority jump (0/1/2/3) - Bulk acknowledge (Shift+A) - Clipboard copy (y) - Theme toggle (T) - Age format toggle (d) - Wide title toggle (t) - Vi-style navigation (j/k) - Home/End jump (g/G) - Rollup drill-down in details Robustness: - Grace period for unreachable clusters - Rollups for high-volume issues - Flap suppression - RBAC error handling Files: All core application code with tests for host collectors, engine, store, model, and export packages.
This commit is contained in:
105
internal/ui/details.go
Normal file
105
internal/ui/details.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tower/internal/model"
|
||||
)
|
||||
|
||||
// getRollupSamples extracts sample IDs from a rollup issue's evidence.
|
||||
func getRollupSamples(iss model.Issue) []string {
|
||||
samplesStr := iss.Evidence["samples"]
|
||||
if samplesStr == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(samplesStr, " | ")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isRollupIssue checks if an issue is a rollup issue.
|
||||
func isRollupIssue(iss model.Issue) bool {
|
||||
if strings.HasPrefix(iss.ID, "k8s:rollup:") {
|
||||
return true
|
||||
}
|
||||
if iss.Category == model.CategoryKubernetes && strings.Contains(strings.ToLower(iss.Title), "rollup") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func renderIssueDetails(now time.Time, mode AgeMode, iss model.Issue) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "Title: %s\n", oneLine(iss.Title))
|
||||
fmt.Fprintf(&b, "Priority: %s Category: %s State: %s\n", iss.Priority, iss.Category, iss.State)
|
||||
fmt.Fprintf(&b, "FirstSeen: %s\n", fmtTime(iss.FirstSeen))
|
||||
fmt.Fprintf(&b, "LastSeen: %s\n", fmtTime(iss.LastSeen))
|
||||
fmt.Fprintf(&b, "Age: %s\n", formatAgeWithMode(iss.Age(now), mode))
|
||||
|
||||
if strings.TrimSpace(iss.Details) != "" {
|
||||
b.WriteString("\nDetails\n")
|
||||
b.WriteString(indentBlock(strings.TrimSpace(iss.Details), " "))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// Show affected issues for rollup issues
|
||||
if isRollupIssue(iss) {
|
||||
samples := getRollupSamples(iss)
|
||||
if len(samples) > 0 {
|
||||
b.WriteString("\nAffected Issues\n")
|
||||
// Show up to 10 samples
|
||||
maxSamples := 10
|
||||
if len(samples) > maxSamples {
|
||||
samples = samples[:maxSamples]
|
||||
}
|
||||
for _, sample := range samples {
|
||||
fmt.Fprintf(&b, " • %s\n", sample)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(iss.Evidence) > 0 {
|
||||
b.WriteString("\nEvidence\n")
|
||||
keys := make([]string, 0, len(iss.Evidence))
|
||||
for k := range iss.Evidence {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(&b, " %s: %s\n", k, iss.Evidence[k])
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(iss.SuggestedFix) != "" {
|
||||
b.WriteString("\nSuggested Fix\n")
|
||||
b.WriteString(indentBlock(strings.TrimSpace(iss.SuggestedFix), " "))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
func fmtTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
return t.Local().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func indentBlock(s, prefix string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = prefix + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user