fix: simplify dashboard and disable unauthorized actions
hermetic-ci / test (push) Canceled after 0s
hermetic-ci / test (push) Canceled after 0s
This commit is contained in:
@@ -180,7 +180,7 @@ func TestCompleteModelPickerConfirmedActivationContinuesThroughUnifiedOperation(
|
||||
completion <- app.SucceededOperationUpdate(completed, app.OperationStatus{Summary: `Activated Profile "new"`}, nil)
|
||||
close(completion)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
if view := model.View(); !strings.Contains(view, "Active Profile: new") || !strings.Contains(view, `Activated Profile "new"`) {
|
||||
if view := model.View(); !strings.Contains(view, "Profile: new") || !strings.Contains(view, `Activated Profile "new"`) {
|
||||
t.Fatalf("confirmed activation did not finish on the dashboard:\n%s", view)
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func TestCompleteModelPickerPartialFailureKeepsProgressClosesPickerAndFocusesFai
|
||||
close(updates)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
view := model.View()
|
||||
if model.selectedTargetID != "docker" || !strings.Contains(view, "> ! Drifted ! Failed Docker daemon") || !strings.Contains(view, "Current error: Activated Profile new with 1 Target update failure") || !strings.Contains(view, "Active Profile: new") {
|
||||
if model.selectedTargetID != "docker" || !strings.Contains(view, "> ! Drifted ! Failed Docker daemon") || !strings.Contains(view, "Current error: Activated Profile new with 1 Target update failure") || !strings.Contains(view, "Profile: new") {
|
||||
t.Fatalf("partial failure was not retained and focused: selected=%q\n%s", model.selectedTargetID, view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,9 @@ func TestCompleteModelRendersAndDispatchesContextualPrimaryActions(t *testing.T)
|
||||
module := &targetModule{snapshot: initial, result: app.Result{Snapshot: initial, Notice: "done"}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Action: "+test.label) || !strings.Contains(view, "Space: "+test.label) {
|
||||
if !strings.Contains(view, "Space: "+test.label) {
|
||||
t.Fatalf("contextual action not rendered:\n%s", view)
|
||||
}
|
||||
if test.state == target.External && (!strings.Contains(view, "proxyctl will begin managing this configuration") || !strings.Contains(view, "while preserving restorable state")) {
|
||||
t.Fatalf("Apply Profile ownership explanation missing:\n%s", view)
|
||||
}
|
||||
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
@@ -91,7 +88,7 @@ func TestCompleteModelRendersUnknownTargetStateAsStructurallyUnavailable(t *test
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Action: Unavailable") || !strings.Contains(view, "Action unavailable:") || !strings.Contains(view, "state is unknown") || !strings.Contains(view, "Space: Unavailable disabled") {
|
||||
if !strings.Contains(view, "Unavailable:") || !strings.Contains(view, "Target actions are unavailable") || !strings.Contains(view, "unknown") || !strings.Contains(view, "Space: Unavailable unavailable") {
|
||||
t.Fatalf("unknown Target action was not structurally unavailable:\n%s", view)
|
||||
}
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
@@ -108,7 +105,7 @@ func TestCompleteModelUsesStructuredDisabledReasonWithoutInferringFromState(t *t
|
||||
module := &targetModule{snapshot: initial}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "No Active Profile is selected") || !strings.Contains(view, "Space: Enable disabled") {
|
||||
if !strings.Contains(view, "No Active Profile is selected") || !strings.Contains(view, "Space: Enable unavailable") {
|
||||
t.Fatalf("structured disabled reason missing:\n%s", view)
|
||||
}
|
||||
updated, command := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
import "proxyctl/internal/app"
|
||||
|
||||
func targetActionLabel(kind app.TargetActionKind) string {
|
||||
switch kind {
|
||||
@@ -101,40 +98,3 @@ func (c targetCatalog) retain(id string, formerIndex int) string {
|
||||
}
|
||||
return c.items[max(0, min(len(c.items)-1, formerIndex))].ID
|
||||
}
|
||||
|
||||
type targetStateCounts struct {
|
||||
enabled int
|
||||
disabled int
|
||||
external int
|
||||
drifted int
|
||||
checking int
|
||||
failed int
|
||||
}
|
||||
|
||||
func countAvailableTargetStates(snapshot app.Snapshot) targetStateCounts {
|
||||
var counts targetStateCounts
|
||||
for _, item := range snapshot.Targets {
|
||||
switch item.DetectionPhase {
|
||||
case app.TargetChecking:
|
||||
counts.checking++
|
||||
continue
|
||||
case app.TargetDetectionFailed:
|
||||
counts.failed++
|
||||
continue
|
||||
}
|
||||
if !item.Available {
|
||||
continue
|
||||
}
|
||||
switch item.State {
|
||||
case target.Enabled:
|
||||
counts.enabled++
|
||||
case target.Disabled:
|
||||
counts.disabled++
|
||||
case target.External:
|
||||
counts.external++
|
||||
case target.Drifted:
|
||||
counts.drifted++
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
@@ -23,9 +23,8 @@ func TestCompleteModelWideOperationsConsoleShowsStableAvailableTargetDashboard(t
|
||||
|
||||
view := resizedModel(t, module, themeModeNoColor, 120, 36).View()
|
||||
assertUsableFrame(t, view, 120, 36,
|
||||
"proxyctl", "Active Profile: office",
|
||||
"● Enabled 1", "○ Disabled 1", "◇ External 1", "! Drifted 1",
|
||||
"> ● Enabled", "Shell", "State: ● Enabled", "Evidence: matches Active Profile",
|
||||
"proxyctl", "Profile: office",
|
||||
"> ● Enabled", "Shell", "matches Active Profile",
|
||||
)
|
||||
if strings.Contains(view, "APT") || strings.Contains(view, "apt-config was not detected") {
|
||||
t.Fatalf("default dashboard included an unavailable Target:\n%s", view)
|
||||
@@ -71,12 +70,8 @@ func TestCompleteModelDashboardRevealsUnavailableEvidenceAndStructuredDetails(t
|
||||
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 120, 36,
|
||||
"> ○ Unavailable APT", "State: ○ Unavailable", "Evidence: apt-config was not detected",
|
||||
"Affects: package operations across privilege boundaries",
|
||||
"Permissions: pre-existing root write authority",
|
||||
"Ownership & restoration: isolated overlay; disabling reveals the", "previous configuration",
|
||||
"Restart risk: none", "Persistent package proxy.", "Longer APT explanation.",
|
||||
"u: hide unavailable",
|
||||
"> ○ Unavailable APT", "○ Unavailable", "apt-config was not detected",
|
||||
"Persistent package proxy.", "u: Hide unavailable",
|
||||
)
|
||||
if !(strings.Index(view, "Shell") < strings.Index(view, "APT") && strings.Index(view, "APT") < strings.Index(view, "Docker daemon")) {
|
||||
t.Fatalf("revealing unavailable Targets changed product order:\n%s", view)
|
||||
@@ -95,7 +90,7 @@ func TestCompleteModelNarrowDashboardFocusAndIndependentScrollingRetainTargetIde
|
||||
}
|
||||
}
|
||||
targets[22].Description.Summary = "Selected identity summary"
|
||||
targets[22].Description.Details = strings.Repeat("Scrollable detail line. ", 45) + "END OF TARGET DETAILS"
|
||||
targets[22].Evidence = strings.Repeat("Scrollable evidence line. ", 45) + "END OF TARGET EVIDENCE"
|
||||
module := &recordingModule{snapshot: app.Snapshot{ActiveProfile: "office", Targets: targets}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 80, 24)
|
||||
for range 22 {
|
||||
@@ -103,7 +98,7 @@ func TestCompleteModelNarrowDashboardFocusAndIndependentScrollingRetainTargetIde
|
||||
}
|
||||
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 80, 24, "> ○ Disabled Product Target 22", "Selected identity summary", "▶ Targets [focused]")
|
||||
assertUsableFrame(t, view, 80, 24, "> ○ Disabled Product Target 22", "▶ Targets [focused]")
|
||||
|
||||
model = updateKey(model, "tab")
|
||||
for range 70 {
|
||||
@@ -111,16 +106,16 @@ func TestCompleteModelNarrowDashboardFocusAndIndependentScrollingRetainTargetIde
|
||||
}
|
||||
view = model.View()
|
||||
assertUsableFrame(t, view, 80, 24,
|
||||
"> ○ Disabled Product Target 22", "▶ Target details [focused]", "END OF", "TARGET DETAILS",
|
||||
"↑/k ↓/j PgUp/PgDn: scroll", "Tab: focus Targets",
|
||||
"> ○ Disabled Product Target 22", "▶ Target details [focused]", "END OF", "TARGET EVIDENCE",
|
||||
"↑/↓: Scroll", "Tab: Targets",
|
||||
)
|
||||
|
||||
model = resize(model, 120, 36)
|
||||
view = model.View()
|
||||
assertUsableFrame(t, view, 120, 36, "> ○ Disabled Product Target 22", "END OF TARGET DETAILS", "▶ Target details [focused]")
|
||||
assertUsableFrame(t, view, 120, 36, "> ○ Disabled Product Target 22", "END OF TARGET", "EVIDENCE", "▶ Target details [focused]")
|
||||
if !sameLine(view, "Targets", "Target details") {
|
||||
t.Fatalf("resized dashboard did not become side by side:\n%s", view)
|
||||
}
|
||||
model = resize(model, 80, 24)
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ○ Disabled Product Target 22", "END OF", "TARGET DETAILS", "▶ Target details [focused]")
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ○ Disabled Product Target 22", "END OF", "TARGET EVIDENCE", "▶ Target details [focused]")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
|
||||
type dashboardRenderState struct {
|
||||
@@ -149,55 +148,18 @@ func (p terminalPresentation) targetDetails(item app.TargetSnapshot, progress ap
|
||||
|
||||
var details strings.Builder
|
||||
details.WriteString(p.theme.section.Render(item.Description.Name))
|
||||
fmt.Fprintf(&details, "\nState: %s\nEvidence: %s", state, evidence)
|
||||
fmt.Fprintf(&details, "\nAction: %s", targetActionLabel(item.PrimaryAction.Kind))
|
||||
if !item.PrimaryAction.Available && item.PrimaryAction.DisabledReason != "" {
|
||||
fmt.Fprintf(&details, "\nAction unavailable: %s", item.PrimaryAction.DisabledReason)
|
||||
}
|
||||
switch item.PrimaryAction.Kind {
|
||||
case app.ApplyProfileTargetAction:
|
||||
details.WriteString("\nApply Profile means proxyctl will begin managing this configuration while preserving restorable state.")
|
||||
case app.ResolveDriftTargetAction:
|
||||
details.WriteString("\nResolve opens Drift Resolution without choosing a recovery direction for you.")
|
||||
fmt.Fprintf(&details, "\n%s\n%s", state, evidence)
|
||||
action := targetActionLabel(item.PrimaryAction.Kind)
|
||||
if item.PrimaryAction.Available {
|
||||
fmt.Fprintf(&details, "\n\nSpace: %s", action)
|
||||
} else if item.PrimaryAction.DisabledReason != "" {
|
||||
fmt.Fprintf(&details, "\n\nUnavailable: %s", item.PrimaryAction.DisabledReason)
|
||||
}
|
||||
if item.DetectionPhase == app.TargetDetectionFailed && item.DetectionErr != nil {
|
||||
fmt.Fprintf(&details, "\nCheck error: %s", item.DetectionErr)
|
||||
}
|
||||
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)
|
||||
}
|
||||
fmt.Fprintf(&details, "\nCheck failed: %s", item.DetectionErr)
|
||||
}
|
||||
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 protectRenderedText(details.String())
|
||||
}
|
||||
|
||||
func (p terminalPresentation) stateCountSummary(snapshot app.Snapshot) string {
|
||||
counts := countAvailableTargetStates(snapshot)
|
||||
summary := 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)
|
||||
if counts.checking > 0 {
|
||||
summary += fmt.Sprintf(" %s %d", p.theme.checking.Render("… Checking"), counts.checking)
|
||||
}
|
||||
if counts.failed > 0 {
|
||||
summary += fmt.Sprintf(" %s %d", p.theme.checkFailed.Render("! Check failed"), counts.failed)
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestCompleteModelRendersResponsiveTerminalSizeClasses(t *testing.T) {
|
||||
if !sameLine(view, "Targets", "Target details") {
|
||||
t.Fatalf("wide layout did not place list and details side by side:\n%s", view)
|
||||
}
|
||||
assertUsableFrame(t, view, 120, 36, "Shell", "Shell summary", "Active Profile: office")
|
||||
assertUsableFrame(t, view, 120, 36, "Shell", "Shell summary", "Profile: office")
|
||||
})
|
||||
|
||||
t.Run("narrow 80x24", func(t *testing.T) {
|
||||
@@ -33,7 +33,7 @@ func TestCompleteModelRendersResponsiveTerminalSizeClasses(t *testing.T) {
|
||||
if sameLine(view, "Targets", "Target details") || strings.Index(view, "Target details") < strings.Index(view, "Shell") {
|
||||
t.Fatalf("narrow layout did not stack list above details:\n%s", view)
|
||||
}
|
||||
assertUsableFrame(t, view, 80, 24, "Shell", "Shell summary", "Active Profile: office")
|
||||
assertUsableFrame(t, view, 80, 24, "Shell", "Shell summary", "Profile: office")
|
||||
})
|
||||
|
||||
t.Run("below minimum 59x15", func(t *testing.T) {
|
||||
@@ -82,11 +82,11 @@ func TestCompleteModelResizePreservesValidSelectionAndFormFocus(t *testing.T) {
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
model = updateKey(model, "down")
|
||||
model = resize(model, 80, 24)
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Action: Apply Profile") {
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Space: Apply Profile") {
|
||||
t.Fatalf("Target selection did not survive wide-to-narrow resize:\n%s", view)
|
||||
}
|
||||
model = resize(model, 120, 36)
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Action: Apply Profile") {
|
||||
if view := model.View(); !strings.Contains(view, "> ◇ External APT") || !strings.Contains(view, "Space: Apply Profile") {
|
||||
t.Fatalf("Target selection did not survive narrow-to-wide resize:\n%s", view)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestCompleteModelSemanticThemeSupportsDarkLightAndNoColor(t *testing.T) {
|
||||
plain := resizedModel(t, module, themeModeNoColor, 80, 24).View()
|
||||
|
||||
for name, view := range map[string]string{"dark": dark, "light": light, "NO_COLOR": plain} {
|
||||
for _, semantic := range []string{"Active Profile: office", "Disabled", "External", ">"} {
|
||||
for _, semantic := range []string{"Profile: office", "Disabled", "External", ">"} {
|
||||
if !strings.Contains(view, semantic) {
|
||||
t.Fatalf("%s theme lost non-color semantic %q:\n%s", name, semantic, view)
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func TestNewModelUsesInjectedBubbleTeaOutputForTerminalCapabilities(t *testing.T
|
||||
if ansiPattern.MatchString(view) {
|
||||
t.Fatalf("non-terminal Bubble Tea output received ANSI styling: %q", view)
|
||||
}
|
||||
for _, semantic := range []string{"Active Profile: office", "Disabled", "External", ">"} {
|
||||
for _, semantic := range []string{"Profile: office", "Disabled", "External", ">"} {
|
||||
if !strings.Contains(view, semantic) {
|
||||
t.Fatalf("injected-output rendering lost semantic %q:\n%s", semantic, view)
|
||||
}
|
||||
@@ -231,19 +231,19 @@ func TestCompleteModelNarrowShellKeepsLastProductionTargetStatusAndActionsVisibl
|
||||
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"> ! Drifted Docker daemon",
|
||||
"State: ! Drifted",
|
||||
"! Drifted",
|
||||
"Docker daemon selected evidence.",
|
||||
"Status: Refresh completed; selected Target remains actionable.",
|
||||
"Space: Resolve",
|
||||
"p: switch Active Profile",
|
||||
"m: manage Proxy Profiles",
|
||||
"p: Profile",
|
||||
"m: Profiles",
|
||||
)
|
||||
|
||||
model = resize(model, 120, 36)
|
||||
model = resize(model, 80, 24)
|
||||
assertUsableFrame(t, model.View(), 80, 24,
|
||||
"> ! Drifted Docker daemon",
|
||||
"State: ! Drifted",
|
||||
"! Drifted",
|
||||
"Docker daemon selected evidence.",
|
||||
"Space: Resolve",
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestCompleteModelContextualHelpOpensOverEveryPageAndRestoresPageState(t *te
|
||||
model = updateKey(model, "down")
|
||||
model = updateKey(model, "?")
|
||||
view := model.View()
|
||||
assertUsableFrame(t, view, 120, 36, "Operations Console", "Contextual Help", "Dashboard", "Target details focus", "APT", "Disable")
|
||||
assertUsableFrame(t, view, 120, 36, "Profile: office", "Contextual Help", "Dashboard", "Target details focus", "APT", "Disable")
|
||||
|
||||
model = updateKey(model, " ")
|
||||
if len(module.intents) != 0 {
|
||||
@@ -259,9 +259,9 @@ func TestCompleteModelFooterHintsMatchContextAndAcceptedKeys(t *testing.T) {
|
||||
|
||||
t.Run("dashboard and busy details", func(t *testing.T) {
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "Space: Enable", "u: show unavailable", "r: refresh", "q/Ctrl+C: quit")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "Space: Enable", "u: Show hidden", "r: Refresh", "q: Quit")
|
||||
model = updateKey(model, "tab")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "PgUp/PgDn: scroll", "u: show unavailable", "q/Ctrl+C: quit")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "↑/↓: Scroll", "u: Show hidden", "q: Quit")
|
||||
|
||||
updates := make(chan app.OperationUpdate)
|
||||
busyModule := &interactiveScriptModule{snapshot: operationSnapshot(), streams: []chan app.OperationUpdate{updates}}
|
||||
@@ -269,9 +269,12 @@ func TestCompleteModelFooterHintsMatchContextAndAcceptedKeys(t *testing.T) {
|
||||
model = updateKey(model, "tab")
|
||||
updated, _ := model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||
model = updated.(Model)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "u: show unavailable", "q/Ctrl+C: defer safe exit", "mutations disabled")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "q: Stop")
|
||||
model = updateKey(model, "u")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "u: hide unavailable")
|
||||
if !model.showUnavailable {
|
||||
t.Fatal("u did not toggle unavailable Targets while busy")
|
||||
}
|
||||
assertUsableFrame(t, model.View(), 120, 36, "q: Stop")
|
||||
close(updates)
|
||||
})
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestMainAndProfilePagesMapKeysAndObscurePasswords(t *testing.T) {
|
||||
}},
|
||||
}}
|
||||
model := NewModel(module)
|
||||
if view := model.View(); !strings.Contains(view, "Active Profile: office") {
|
||||
if view := model.View(); !strings.Contains(view, "Profile: office") {
|
||||
t.Fatalf("main view missing Active Profile:\n%s", view)
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestRenderedMalformedProxyURLStillObscuresPassword(t *testing.T) {
|
||||
func TestNoProfileStateStaysOnAccessibleMainPage(t *testing.T) {
|
||||
model := NewModel(&recordingModule{})
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "No Active Profile") || !strings.Contains(view, "m: manage Proxy Profiles") {
|
||||
if !strings.Contains(view, "No Active Profile") || !strings.Contains(view, "m: Profiles") {
|
||||
t.Fatalf("no-Profile main view is not actionable:\n%s", view)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-52
@@ -14,12 +14,7 @@ func (p terminalPresentation) mainView(model presentationState, width, height in
|
||||
if activeProfile == "" {
|
||||
activeProfile = "No Active Profile"
|
||||
}
|
||||
header := fmt.Sprintf("%s Operations Console\nActive Profile: %s Targets: %s",
|
||||
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
|
||||
if width < wideWidth {
|
||||
header = fmt.Sprintf("%s Operations Console\nActive Profile: %s\nTargets: %s",
|
||||
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
|
||||
}
|
||||
header := fmt.Sprintf("%s Profile: %s", p.theme.title.Render("proxyctl"), activeProfile)
|
||||
|
||||
catalog := newTargetCatalog(model.snapshot, dashboard.showUnavailable, dashboard.selectedTargetID)
|
||||
footer := p.dashboardFooter(model, catalog)
|
||||
@@ -97,62 +92,30 @@ func (p terminalPresentation) driftResolutionView(selected app.TargetSnapshot) (
|
||||
}
|
||||
|
||||
func (p terminalPresentation) dashboardFooter(model presentationState, catalog targetCatalog) string {
|
||||
dashboard, operation, width := model.dashboard, model.operation, model.terminal.width
|
||||
visibility := "u: show unavailable"
|
||||
dashboard, operation := model.dashboard, model.operation
|
||||
visibility := "u: Show hidden"
|
||||
if dashboard.showUnavailable {
|
||||
visibility = "u: hide unavailable"
|
||||
visibility = "u: Hide unavailable"
|
||||
}
|
||||
if operation.exitDeferred {
|
||||
return "Operation stopping; changes disabled ?: Help"
|
||||
}
|
||||
if dashboard.focus == targetDetailsPane {
|
||||
if operation.exitDeferred {
|
||||
return "↑/k ↓/j PgUp/PgDn: browse Tab: focus Targets " + visibility + " ?: help\nExit pending; new work disabled until safe completion"
|
||||
}
|
||||
if width < wideWidth {
|
||||
if operation.active {
|
||||
return "↑/k ↓/j PgUp/PgDn: browse Tab: focus Targets\n?: help q/Ctrl+C: defer exit; mutations disabled"
|
||||
}
|
||||
if width < 70 {
|
||||
return "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets\np: switch Profile m: manage ?: help q: quit"
|
||||
}
|
||||
return "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets\np: switch Active Profile m: manage Proxy Profiles ?: help q: quit"
|
||||
}
|
||||
footer := "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + "\np: switch Active Profile m: manage Proxy Profiles ?: help q/Ctrl+C: quit"
|
||||
if operation.active {
|
||||
footer = "↑/k ↓/j PgUp/PgDn: scroll Tab: focus Targets " + visibility + " ?: help\nq/Ctrl+C: defer safe exit; mutations disabled"
|
||||
return "↑/↓: Scroll Tab: Targets ?: Help q: Stop"
|
||||
}
|
||||
return footer
|
||||
}
|
||||
actions := "↑/k ↓/j: select Tab: focus details " + visibility
|
||||
if operation.exitDeferred {
|
||||
return actions + " ?: help\nExit pending; new work disabled until safe completion"
|
||||
return "↑/↓: Scroll Tab: Targets " + visibility + "\np: Profile m: Profiles r: Refresh ?: Help q: Quit"
|
||||
}
|
||||
selected, ok := catalog.selected(dashboard.selectedTargetID)
|
||||
availability := targetMutationAvailability(selected, ok, operation.active || operation.exitDeferred)
|
||||
label := targetActionLabel(selected.PrimaryAction.Kind)
|
||||
if availability.available {
|
||||
actions = "Space: " + label + " " + actions
|
||||
} else {
|
||||
actions = "Space: " + label + " disabled (" + availability.compactReason + ") " + actions
|
||||
availability := targetMutationAvailability(selected, ok, operation.active)
|
||||
action := "Space: " + targetActionLabel(selected.PrimaryAction.Kind)
|
||||
if !availability.available {
|
||||
action += " unavailable"
|
||||
}
|
||||
if width < wideWidth {
|
||||
if availability.available {
|
||||
actions = "Space: " + label + " ↑/k ↓/j: select Tab: focus details"
|
||||
} else {
|
||||
actions = "Space: " + label + " disabled ↑/k ↓/j: select Tab: focus details"
|
||||
}
|
||||
|
||||
if operation.active {
|
||||
return actions + "\n?: help q/Ctrl+C: defer exit; mutations disabled"
|
||||
}
|
||||
if width < 70 {
|
||||
return actions + "\np: switch Profile m: manage ?: help q: quit"
|
||||
}
|
||||
return actions + "\np: switch Active Profile m: manage Proxy Profiles ?: help q: quit"
|
||||
}
|
||||
footer := actions + "\np: switch Active Profile m: manage Proxy Profiles r: refresh ?: help q/Ctrl+C: quit"
|
||||
if operation.active {
|
||||
footer = actions + "\n?: help q: defer safe exit; new mutations disabled"
|
||||
return action + " ↑/↓: Select Tab: Details\n?: Help q: Stop"
|
||||
}
|
||||
return footer
|
||||
return action + " ↑/↓: Select Tab: Details " + visibility + "\np: Profile m: Profiles r: Refresh ?: Help q: Quit"
|
||||
}
|
||||
|
||||
func (p terminalPresentation) profilesView(model presentationState) terminalPage {
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestCompleteModelProfileManagementNavigationAndBusyReasonsAreConsistent(t *
|
||||
model = updateKey(model, "m")
|
||||
updated, command := model.Update(back)
|
||||
model = updated.(Model)
|
||||
if command != nil || !strings.Contains(model.View(), "Operations Console") {
|
||||
if command != nil || !strings.Contains(model.View(), "Profile:") {
|
||||
t.Fatalf("%q did not return to the dashboard:\n%s", back.String(), model.View())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestCompleteModelReconcilesAuthoritativeSnapshotWhenDetectionGenerationClos
|
||||
if next != nil || model.operation.active {
|
||||
t.Fatalf("stale closure left detection active: next=%v active=%v", next != nil, model.operation.active)
|
||||
}
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ● Enabled Shell", "Evidence: shell enabled")
|
||||
assertUsableFrame(t, model.View(), 80, 24, "> ● Enabled Shell", "shell enabled")
|
||||
}
|
||||
|
||||
func (m *progressiveDetectionModule) detectionUpdate(id string, phase app.TargetDetectionPhase, available bool, state target.State, evidence string, err error) app.OperationUpdate {
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestCompleteModelFixedSizeCoverageAcrossCompletedWorkflows(t *testing.T) {
|
||||
}
|
||||
return resizedModel(t, &recordingModule{snapshot: snapshot}, themeModeNoColor, width, height)
|
||||
},
|
||||
want: []string{"Operations Console", "Checking", "Target availability"},
|
||||
want: []string{"Profile:", "Checking", "Target availability"},
|
||||
},
|
||||
{
|
||||
name: "refresh",
|
||||
@@ -46,7 +46,7 @@ func TestCompleteModelFixedSizeCoverageAcrossCompletedWorkflows(t *testing.T) {
|
||||
}
|
||||
return model
|
||||
},
|
||||
want: []string{"Operations Console", "Checking Target availability and state"},
|
||||
want: []string{"Profile:", "Checking Target availability and state"},
|
||||
},
|
||||
{
|
||||
name: "Target action",
|
||||
@@ -155,7 +155,7 @@ func TestCompleteModelLightDarkAndNoColorRetainEquivalentSemantics(t *testing.T)
|
||||
model, _ = applyOperationCommand(t, model, command)
|
||||
model = updateKey(model, "e")
|
||||
views[mode] = progressFrame + "--- operation completed ---\n" + model.View()
|
||||
for _, semantic := range []string{"[focused]", "Disabled", "Drifted", "Applying", "mutations disabled", "Current error:", "REDACTED", "Action: Enable"} {
|
||||
for _, semantic := range []string{"[focused]", "Disabled", "Drifted", "Applying", "q: Stop", "Current error:", "REDACTED", "Space: Enable"} {
|
||||
if !strings.Contains(views[mode], semantic) {
|
||||
t.Fatalf("theme %d lost %q:\n%s", mode, semantic, views[mode])
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func TestCompleteModelLightDarkAndNoColorRetainEquivalentSemantics(t *testing.T)
|
||||
}
|
||||
|
||||
plain := views[themeModeNoColor]
|
||||
for _, symbol := range []string{"● Enabled", "○ Disabled", "! Drifted", "▶ Target details [focused]"} {
|
||||
for _, symbol := range []string{"○ Disabled", "! Drifted", "▶ Target details [focused]"} {
|
||||
if !strings.Contains(plain, symbol) {
|
||||
t.Fatalf("standard Unicode/ASCII status strategy lost %q:\n%s", symbol, plain)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func TestCompleteModelBoundsUnicodeAndKeepsLongContentReachable(t *testing.T) {
|
||||
snapshot.Profiles[0].Name = longProfile
|
||||
snapshot.Targets[0].Description.Name = longName
|
||||
snapshot.Targets[0].Evidence = strings.Repeat("証拠 and wrapped evidence ", 80) + " EVIDENCE-END"
|
||||
snapshot.Targets[0].Description.Details = strings.Repeat("詳細 line ", 100) + " DETAILS-END"
|
||||
snapshot.Targets[0].Evidence = strings.Repeat("証拠 and wrapped evidence ", 120) + " EVIDENCE-END"
|
||||
model := resizedModel(t, &recordingModule{snapshot: snapshot}, themeModeNoColor, 80, 24)
|
||||
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24, "Disabled")
|
||||
@@ -196,7 +196,7 @@ func TestCompleteModelBoundsUnicodeAndKeepsLongContentReachable(t *testing.T) {
|
||||
for range 120 {
|
||||
model = updateKey(model, "pgdown")
|
||||
}
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24, "DETAILS-END")
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24, "EVIDENCE-END")
|
||||
|
||||
model = updateKey(model, "m")
|
||||
for range 60 {
|
||||
@@ -318,7 +318,7 @@ func TestCompleteModelLongFieldsAndDialogsRemainReachableAcrossResizeAtNontermin
|
||||
|
||||
func TestCompleteModelResizeStressPreservesDialogFocusSelectionViewportInFlightAndDeferredExit(t *testing.T) {
|
||||
snapshot := hardeningSnapshot()
|
||||
snapshot.Targets[1].Description.Details = strings.Repeat("APT scrollable detail. ", 90) + " APT-VIEWPORT-END"
|
||||
snapshot.Targets[1].Evidence = strings.Repeat("APT scrollable evidence. ", 90) + " APT-VIEWPORT-END"
|
||||
updates := make(chan app.OperationUpdate, 2)
|
||||
module := &interactiveScriptModule{snapshot: snapshot, streams: []chan app.OperationUpdate{updates}}
|
||||
model := resizedModel(t, module, themeModeNoColor, 120, 36)
|
||||
@@ -354,8 +354,8 @@ func TestCompleteModelResizeStressPreservesDialogFocusSelectionViewportInFlightA
|
||||
|
||||
model = updateKey(model, "q")
|
||||
assertCellBoundedFrame(t, model.View(), 80, 24,
|
||||
"> ! Drifted APT", "▶ Target details [focused]", "APT-VIEWPORT-", "END",
|
||||
"Applying", "Exit requested", "new work disabled")
|
||||
"> ! Drifted APT", "▶ Target details [focused]", "VIEWPORT-END",
|
||||
"Applying", "Exit requested", "changes disabled")
|
||||
|
||||
updates <- app.SucceededOperationUpdate(snapshot,
|
||||
app.OperationStatus{Summary: "Shell Target is now Enabled", TargetID: "shell"},
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestTargetPanelShowsOnlyAvailableTargetsAndAppliesAsynchronously(t *testing
|
||||
module.result = app.Result{Snapshot: app.Snapshot{ActiveProfile: "office", Targets: []app.TargetSnapshot{{ID: "shell", Description: target.Description{Name: "Shell", Summary: "Exports for Bash/Zsh"}, Available: true, State: target.Enabled, Evidence: "matches", PrimaryAction: app.TargetAction{Kind: app.DisableTargetAction, Available: true}}}}, Notice: "Shell Target is now Enabled"}
|
||||
model := NewModel(module)
|
||||
view := model.View()
|
||||
if !strings.Contains(view, "Shell") || strings.Contains(view, "APT") || !strings.Contains(view, "Action: Apply Profile") {
|
||||
if !strings.Contains(view, "Shell") || strings.Contains(view, "APT") || !strings.Contains(view, "Space: Apply Profile") {
|
||||
t.Fatalf("Target panel:\n%s", view)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestHelpListsEveryTargetAndMainListsOnlyAvailableTargets(t *testing.T) {
|
||||
}}}
|
||||
model := NewModel(module)
|
||||
view := model.View()
|
||||
if strings.Contains(view, "APT") || !strings.Contains(view, "Action: Enable") || !strings.Contains(view, "integration installed") {
|
||||
if strings.Contains(view, "APT") || !strings.Contains(view, "Unavailable:") || !strings.Contains(view, "integration installed") {
|
||||
t.Fatalf("main Target panel:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "?")
|
||||
@@ -123,7 +123,7 @@ func TestHelpListsEveryTargetAndMainListsOnlyAvailableTargets(t *testing.T) {
|
||||
t.Fatalf("scrollable Target reference:\n%s", view)
|
||||
}
|
||||
model = updateKey(model, "esc")
|
||||
if !strings.Contains(model.View(), "Operations Console") {
|
||||
if !strings.Contains(model.View(), "Profile:") {
|
||||
t.Fatalf("Escape did not return from help:\n%s", model.View())
|
||||
}
|
||||
}
|
||||
@@ -279,12 +279,12 @@ func TestQAndEscapeReturnFromNestedPages(t *testing.T) {
|
||||
for _, key := range []tea.KeyMsg{{Type: tea.KeyRunes, Runes: []rune{'q'}}, {Type: tea.KeyEsc}} {
|
||||
model := updateKey(NewModel(module), "?")
|
||||
updated, command := model.Update(key)
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Operations Console") {
|
||||
t.Fatalf("help key %q did not return to Operations Console", key.String())
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Profile:") {
|
||||
t.Fatalf("help key %q did not return to dashboard", key.String())
|
||||
}
|
||||
model = updateKey(NewModel(module), "m")
|
||||
updated, command = model.Update(key)
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Operations Console") {
|
||||
if command != nil || !strings.Contains(updated.(Model).View(), "Profile:") {
|
||||
t.Fatalf("Profile key %q did not return to Target panel", key.String())
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func TestActiveProfileTransitionRendersPerTargetProgress(t *testing.T) {
|
||||
t.Fatal("Profile transition did not start progressive operation")
|
||||
}
|
||||
model, command = applyOperationCommand(t, model, command)
|
||||
if command == nil || !strings.Contains(model.View(), "Applying") || !strings.Contains(model.View(), "Active Profile: new") {
|
||||
if command == nil || !strings.Contains(model.View(), "Applying") || !strings.Contains(model.View(), "Profile: new") {
|
||||
t.Fatalf("applying progress:\n%s", model.View())
|
||||
}
|
||||
model, command = applyOperationCommand(t, model, command)
|
||||
|
||||
Reference in New Issue
Block a user