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.
106 lines
2.5 KiB
Go
106 lines
2.5 KiB
Go
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")
|
|
}
|