Skip to content

Commit 588b12e

Browse files
committed
Add interface implementations and TUI with tabbed navigation
- Add compile-time interface checks for analyzer.ResourceAnalyzer in SQL and GKE analyzers - Add compile-time interface checks for analyzer.Baseline in SQL and GKE baselines - Implement TUI package with tabbed interface using Bubble Tea - Add 6 organized tabs: Overview, Critical, High, Medium, Low, All Drifts - Add keyboard navigation with vim-style and arrow key support - Add color-coded severity levels with visual indicators - Integrate TUI mode into gcp sql and gcp gke commands (--output tui) - Add bubbletea and bubbles dependencies - Add TUI-FEATURE.md documentation
1 parent d0ab8d0 commit 588b12e

File tree

10 files changed

+819
-1
lines changed

10 files changed

+819
-1
lines changed

TUI-FEATURE.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# TUI (Terminal User Interface) Feature
2+
3+
The drift-analysis-cli now supports an interactive Terminal User Interface (TUI) mode for viewing drift analysis reports in a more organized and user-friendly way.
4+
5+
## Features
6+
7+
### Tabbed Interface
8+
The TUI provides 6 organized tabs:
9+
10+
1. **Overview** - Summary statistics, compliance rate, and drift counts by severity
11+
2. **Critical** - Only resources with critical severity drifts
12+
3. **High** - Only resources with high severity drifts
13+
4. **Medium** - Only resources with medium severity drifts
14+
5. **Low** - Only resources with low severity drifts
15+
6. **All Drifts** - Complete list of all resources and their drifts
16+
17+
### Navigation
18+
- **Tab / → / l** - Switch to next tab
19+
- **Shift+Tab / ← / h** - Switch to previous tab
20+
- **↑ / k** - Scroll up
21+
- **↓ / j** - Scroll down
22+
- **PgUp / b** - Page up
23+
- **PgDn / f** - Page down
24+
- **u / Ctrl+u** - Half page up
25+
- **d / Ctrl+d** - Half page down
26+
- **q / Esc / Ctrl+c** - Quit
27+
28+
### Visual Design
29+
- Color-coded severity levels (red for critical, orange for high, yellow for medium, gray for low)
30+
- Progress indicator showing scroll position
31+
- Styled headers and resource information
32+
- Clean, organized layout with proper spacing
33+
34+
## Usage
35+
36+
### Cloud SQL Analysis
37+
```bash
38+
# Run with TUI output
39+
drift-analysis-cli gcp sql --config config.yaml --output tui
40+
41+
# Or use short flag
42+
drift-analysis-cli gcp sql -c config.yaml -o tui
43+
```
44+
45+
### GKE Cluster Analysis
46+
```bash
47+
# Run with TUI output
48+
drift-analysis-cli gcp gke --config config.yaml --output tui
49+
50+
# Or use short flag
51+
drift-analysis-cli gcp gke -c config.yaml -o tui
52+
```
53+
54+
## Benefits
55+
56+
1. **Better Organization** - Filter drifts by severity level instantly
57+
2. **Interactive Exploration** - Navigate through results at your own pace
58+
3. **Quick Overview** - See summary statistics at a glance
59+
4. **Focused Analysis** - Jump directly to critical issues
60+
5. **No External Tools** - Everything runs in your terminal
61+
62+
## Comparison with Other Formats
63+
64+
| Format | Use Case |
65+
|--------|----------|
66+
| `tui` | Interactive exploration and analysis |
67+
| `text` | Quick terminal output or piping to files |
68+
| `json` | Programmatic processing and CI/CD integration |
69+
| `yaml` | Configuration-friendly format |
70+
71+
## Example Workflow
72+
73+
1. Run analysis with TUI mode:
74+
```bash
75+
drift-analysis-cli gcp sql -c config.yaml -o tui
76+
```
77+
78+
2. Start in Overview tab to see:
79+
- Total resources analyzed
80+
- Compliance rate percentage
81+
- Drift counts by severity
82+
83+
3. Press `tab` to switch to Critical tab
84+
- Review all critical issues first
85+
- Note which resources need immediate attention
86+
87+
4. Use arrow keys or vim-style navigation (j/k) to scroll through results
88+
89+
5. Press `q` to exit when done
90+
91+
## Technical Details
92+
93+
Built with:
94+
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - TUI framework
95+
- [Bubbles](https://github.com/charmbracelet/bubbles) - TUI components (viewport)
96+
- [Lip Gloss](https://github.com/charmbracelet/lipgloss) - Terminal styling
97+
98+
The TUI implementation is located in `pkg/tui/` and includes:
99+
- `model.go` - Core TUI model and event handling
100+
- `report.go` - Tab building and content formatting
101+
- `converters.go` - Converting SQL/GKE reports to TUI format

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ require (
1515
cloud.google.com/go/compute/metadata v0.9.0 // indirect
1616
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
1717
github.com/cespare/xxhash/v2 v2.3.0 // indirect
18+
github.com/charmbracelet/bubbles v0.21.0 // indirect
19+
github.com/charmbracelet/bubbletea v1.3.10 // indirect
1820
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
19-
github.com/charmbracelet/x/ansi v0.8.0 // indirect
21+
github.com/charmbracelet/x/ansi v0.10.1 // indirect
2022
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
2123
github.com/charmbracelet/x/term v0.2.1 // indirect
24+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
2225
github.com/felixge/httpsnoop v1.0.4 // indirect
2326
github.com/go-logr/logr v1.4.3 // indirect
2427
github.com/go-logr/stdr v1.2.2 // indirect
@@ -29,7 +32,10 @@ require (
2932
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3033
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
3134
github.com/mattn/go-isatty v0.0.20 // indirect
35+
github.com/mattn/go-localereader v0.0.1 // indirect
3236
github.com/mattn/go-runewidth v0.0.16 // indirect
37+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
38+
github.com/muesli/cancelreader v0.2.2 // indirect
3339
github.com/muesli/termenv v0.16.0 // indirect
3440
github.com/rivo/uniseg v0.4.7 // indirect
3541
github.com/spf13/pflag v1.0.9 // indirect

go.sum

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,27 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
88
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
99
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
1010
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
11+
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
12+
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
13+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
14+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
1115
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
1216
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
1317
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
1418
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
1519
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
1620
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
21+
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
22+
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
1723
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
1824
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
1925
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
2026
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
2127
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2228
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2329
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
30+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
31+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
2432
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
2533
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2634
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -50,8 +58,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
5058
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
5159
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
5260
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
61+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
62+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
5363
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
5464
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
65+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
66+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
67+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
68+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
5569
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
5670
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
5771
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -95,6 +109,7 @@ golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
95109
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
96110
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
97111
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
112+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98113
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99114
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
100115
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

pkg/gcp/gke/analyzer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"time"
88

9+
"github.com/jessequinn/drift-analysis-cli/pkg/analyzer"
910
container "google.golang.org/api/container/v1"
1011
)
1112

@@ -130,6 +131,9 @@ func (a *Analyzer) Close() error {
130131
return nil
131132
}
132133

134+
// Compile-time interface implementation check
135+
var _ analyzer.ResourceAnalyzer = (*Analyzer)(nil)
136+
133137
// Analyze performs drift analysis implementing analyzer.ResourceAnalyzer interface
134138
func (a *Analyzer) Analyze(ctx context.Context, projects []string) error {
135139
a.projects = projects

pkg/gcp/gke/command.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/jessequinn/drift-analysis-cli/pkg/analyzer"
1213
"gopkg.in/yaml.v3"
1314
)
1415

@@ -42,6 +43,9 @@ type GKEBaseline struct {
4243
NodePoolConfig *NodePoolConfig `yaml:"nodepool_config,omitempty"`
4344
}
4445

46+
// Compile-time interface implementation check
47+
var _ analyzer.Baseline = (*GKEBaseline)(nil)
48+
4549
// GetName returns the baseline name implementing analyzer.Baseline interface
4650
func (b GKEBaseline) GetName() string {
4751
return b.Name

pkg/gcp/sql/analyzer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"time"
88

9+
"github.com/jessequinn/drift-analysis-cli/pkg/analyzer"
910
"google.golang.org/api/sqladmin/v1"
1011
)
1112

@@ -95,6 +96,9 @@ func (a *Analyzer) Close() error {
9596
return nil
9697
}
9798

99+
// Compile-time interface implementation check
100+
var _ analyzer.ResourceAnalyzer = (*Analyzer)(nil)
101+
98102
// Analyze performs drift analysis implementing analyzer.ResourceAnalyzer interface
99103
func (a *Analyzer) Analyze(ctx context.Context, projects []string) error {
100104
a.projects = projects

pkg/gcp/sql/command.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"strings"
1010

11+
"github.com/jessequinn/drift-analysis-cli/pkg/analyzer"
1112
"gopkg.in/yaml.v3"
1213
)
1314

@@ -39,6 +40,9 @@ type SQLBaseline struct {
3940
Config *DatabaseConfig `yaml:"config"`
4041
}
4142

43+
// Compile-time interface implementation check
44+
var _ analyzer.Baseline = (*SQLBaseline)(nil)
45+
4246
// GetName returns the baseline name implementing analyzer.Baseline interface
4347
func (b SQLBaseline) GetName() string {
4448
return b.Name

pkg/tui/converters.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package tui
2+
3+
import (
4+
"github.com/jessequinn/drift-analysis-cli/pkg/gcp/gke"
5+
"github.com/jessequinn/drift-analysis-cli/pkg/gcp/sql"
6+
)
7+
8+
// FromSQLReport converts a SQL drift report to TUI format
9+
func FromSQLReport(report *sql.DriftReport) ReportData {
10+
items := make([]DriftItem, 0, len(report.Instances))
11+
12+
for _, inst := range report.Instances {
13+
drifts := make([]DriftDetail, 0, len(inst.Drifts))
14+
for _, d := range inst.Drifts {
15+
drifts = append(drifts, DriftDetail{
16+
Field: d.Field,
17+
Expected: d.Expected,
18+
Actual: d.Actual,
19+
Severity: d.Severity,
20+
})
21+
}
22+
23+
items = append(items, DriftItem{
24+
ResourceType: "Cloud SQL",
25+
Project: inst.Project,
26+
Name: inst.Name,
27+
Location: inst.Region,
28+
State: inst.State,
29+
Labels: inst.Labels,
30+
Drifts: drifts,
31+
})
32+
}
33+
34+
return ReportData{
35+
Title: "GCP PostgreSQL Drift Analysis Report",
36+
Timestamp: report.Timestamp,
37+
TotalResources: report.TotalInstances,
38+
DriftedResources: report.DriftedInstances,
39+
Items: items,
40+
}
41+
}
42+
43+
// FromGKEReport converts a GKE drift report to TUI format
44+
func FromGKEReport(report *gke.DriftReport) ReportData {
45+
items := make([]DriftItem, 0, len(report.Instances))
46+
47+
for _, cluster := range report.Instances {
48+
drifts := make([]DriftDetail, 0, len(cluster.Drifts))
49+
for _, d := range cluster.Drifts {
50+
drifts = append(drifts, DriftDetail{
51+
Field: d.Field,
52+
Expected: d.Expected,
53+
Actual: d.Actual,
54+
Severity: d.Severity,
55+
})
56+
}
57+
58+
items = append(items, DriftItem{
59+
ResourceType: "GKE Cluster",
60+
Project: cluster.Project,
61+
Name: cluster.Name,
62+
Location: cluster.Location,
63+
State: cluster.Status,
64+
Labels: cluster.Labels,
65+
Drifts: drifts,
66+
})
67+
}
68+
69+
return ReportData{
70+
Title: "GCP GKE Drift Analysis Report",
71+
Timestamp: report.Timestamp,
72+
TotalResources: report.TotalClusters,
73+
DriftedResources: report.DriftedClusters,
74+
Items: items,
75+
}
76+
}

0 commit comments

Comments
 (0)