Files
proxy/internal/tui/dashboard_view.go
T

159 lines
5.1 KiB
Go

package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/viewport"
"github.com/charmbracelet/lipgloss"
"proxyctl/internal/app"
"proxyctl/internal/target"
)
type dashboardRenderState struct {
catalog targetCatalog
selectedTargetID string
focus dashboardPane
detailsOffset int
applying bool
profileTransition bool
progressTarget string
}
func (p terminalPresentation) renderDashboard(state dashboardRenderState, width, height int) string {
if width >= wideWidth && height >= 12 {
leftWidth := (width - 1) * 2 / 5
rightWidth := width - leftWidth - 1
left := p.renderTargetListPane(state, leftWidth, height)
right := p.renderTargetDetailsPane(state, rightWidth, height)
divider := strings.Repeat("│\n", max(0, height-1)) + "│"
return lipgloss.JoinHorizontal(lipgloss.Top, left, divider, right)
}
listHeight := max(5, height/2)
detailsHeight := height - listHeight - 1
if detailsHeight < 4 {
detailsHeight = 4
listHeight = max(3, height-detailsHeight-1)
}
return p.renderTargetListPane(state, width, listHeight) + "\n" +
p.renderTargetDetailsPane(state, width, detailsHeight)
}
func (p terminalPresentation) renderTargetListPane(state dashboardRenderState, width, height int) string {
heading := paneHeading("Targets", state.focus == targetListPane)
bodyHeight := max(1, height-2)
lines := make([]string, 0, len(state.catalog.items))
selectedIndex := state.catalog.index(state.selectedTargetID)
for _, item := range state.catalog.items {
cursor := " "
if item.ID == state.selectedTargetID {
cursor = "> "
}
applying := (state.progressTarget != "" && item.ID == state.progressTarget) ||
(!state.profileTransition && state.progressTarget == "" && state.applying && item.ID == state.selectedTargetID)
badge := p.targetBadge(item, applying)
lines = append(lines, cursor+badge+" "+item.Description.Name)
}
if len(lines) == 0 {
lines = append(lines, "No Targets are currently Available.")
selectedIndex = 0
}
offset := 0
if selectedIndex >= bodyHeight {
offset = selectedIndex - bodyHeight + 1
}
body := fixedViewport(strings.Join(lines, "\n"), width, bodyHeight, offset, false)
return fitPane(heading+"\n"+paneRule(width)+"\n"+body, width, height)
}
func (p terminalPresentation) renderTargetDetailsPane(state dashboardRenderState, width, height int) string {
heading := paneHeading("Target details", state.focus == targetDetailsPane)
bodyHeight := max(1, height-2)
selected, ok := state.catalog.selected(state.selectedTargetID)
content := "Select a Target to review its details."
if ok {
content = p.targetDetails(selected)
}
body := fixedViewport(content, width, bodyHeight, state.detailsOffset, true)
return fitPane(heading+"\n"+paneRule(width)+"\n"+body, width, height)
}
func paneHeading(label string, focused bool) string {
if focused {
return "▶ " + label + " [focused]"
}
return " " + label
}
func paneRule(width int) string {
return strings.Repeat("─", max(1, width))
}
func fitPane(content string, width, height int) string {
return lipgloss.NewStyle().Width(width).Height(height).MaxWidth(width).MaxHeight(height).Render(content)
}
func fixedViewport(content string, width, height, offset int, wrap bool) string {
if wrap {
content = lipgloss.NewStyle().Width(width).Render(content)
}
view := viewport.New(width, height)
view.SetContent(strings.TrimRight(content, "\n"))
view.SetYOffset(max(0, offset))
return view.View()
}
func (p terminalPresentation) targetBadge(item app.TargetSnapshot, applying bool) string {
if !item.Available {
return p.theme.unavailable.Render("○ Unavailable")
}
return p.theme.renderTargetState(item.State, applying)
}
func (p terminalPresentation) targetDetails(item app.TargetSnapshot) string {
state := p.targetBadge(item, false)
evidence := item.Evidence
if !item.Available {
evidence = item.AvailabilityEvidence
}
if evidence == "" {
evidence = "No detection evidence reported."
}
var details strings.Builder
details.WriteString(p.theme.section.Render(item.Description.Name))
fmt.Fprintf(&details, "\nState: %s\nEvidence: %s", state, evidence)
facts := []struct {
label string
value string
}{
{"Affects", item.Description.Scope},
{"Permissions", item.Description.Permissions},
{"Ownership & restoration", item.Description.Ownership},
{"Restart risk", item.Description.RestartRisk},
}
for _, fact := range facts {
if fact.value != "" {
fmt.Fprintf(&details, "\n%s: %s", fact.label, fact.value)
}
}
if item.Description.Summary != "" {
fmt.Fprintf(&details, "\n\n%s", item.Description.Summary)
}
if item.Description.Details != "" {
fmt.Fprintf(&details, "\n\n%s", item.Description.Details)
}
return details.String()
}
func (p terminalPresentation) stateCountSummary(snapshot app.Snapshot) string {
counts := countAvailableTargetStates(snapshot)
return fmt.Sprintf("%s %d %s %d %s %d %s %d",
p.theme.renderTargetState(target.Enabled, false), counts.enabled,
p.theme.renderTargetState(target.Disabled, false), counts.disabled,
p.theme.renderTargetState(target.External, false), counts.external,
p.theme.renderTargetState(target.Drifted, false), counts.drifted)
}