fix: finish progressive Target detection
This commit is contained in:
@@ -20,10 +20,14 @@ type targetCatalog struct {
|
||||
items []app.TargetSnapshot
|
||||
}
|
||||
|
||||
func newTargetCatalog(snapshot app.Snapshot, includeUnavailable bool) targetCatalog {
|
||||
func newTargetCatalog(snapshot app.Snapshot, includeUnavailable bool, selectedID string) targetCatalog {
|
||||
items := make([]app.TargetSnapshot, 0, len(snapshot.Targets))
|
||||
for _, item := range snapshot.Targets {
|
||||
if item.Available || includeUnavailable || item.DetectionPhase != app.TargetReady {
|
||||
// Available Targets are the routine default. If progressive detection
|
||||
// proves the selected identity unavailable, keep that one row visible
|
||||
// until the user selects another Target rather than silently moving the
|
||||
// selection to a different product.
|
||||
if item.Available || includeUnavailable || item.ID == selectedID || item.DetectionPhase != app.TargetReady {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
|
||||
+18
-27
@@ -68,6 +68,7 @@ type refreshDone struct {
|
||||
type targetDetectionProgress struct {
|
||||
update app.TargetDetectionUpdate
|
||||
updates <-chan app.TargetDetectionUpdate
|
||||
closed bool
|
||||
}
|
||||
|
||||
type profileOperationProgress struct {
|
||||
@@ -90,7 +91,7 @@ func waitForTargetDetection(updates <-chan app.TargetDetectionUpdate) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
update, ok := <-updates
|
||||
if !ok {
|
||||
return targetDetectionProgress{update: app.TargetDetectionUpdate{Complete: true}, updates: updates}
|
||||
return targetDetectionProgress{updates: updates, closed: true}
|
||||
}
|
||||
return targetDetectionProgress{update: update, updates: updates}
|
||||
}
|
||||
@@ -120,7 +121,7 @@ func NewModel(application app.Module, configure ...ModelOption) Model {
|
||||
application: application,
|
||||
presentation: terminalPresentation{theme: newSemanticTheme(options.theme, options.output)},
|
||||
snapshot: snapshot,
|
||||
selectedTargetID: newTargetCatalog(snapshot, false).firstID(),
|
||||
selectedTargetID: newTargetCatalog(snapshot, false, "").firstID(),
|
||||
width: 80,
|
||||
height: 24,
|
||||
refreshing: detectionUpdates != nil,
|
||||
@@ -173,9 +174,17 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if !m.refreshing || progress.updates != m.detectionUpdates {
|
||||
return m, nil
|
||||
}
|
||||
if progress.update.Result.TargetID != "" {
|
||||
m.applyTargetDetection(progress.update.Result)
|
||||
if progress.closed {
|
||||
// A stale Application generation closes without claiming an
|
||||
// authoritative completion snapshot.
|
||||
m.refreshing = false
|
||||
m.detectionUpdates = nil
|
||||
return m, m.deferredQuit()
|
||||
}
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
formerIndex := formerCatalog.index(m.selectedTargetID)
|
||||
m.snapshot = progress.update.Snapshot
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID).retain(m.selectedTargetID, formerIndex)
|
||||
if progress.update.Complete {
|
||||
m.refreshing = false
|
||||
m.detectionUpdates = nil
|
||||
@@ -189,10 +198,10 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
m.refreshing = false
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable)
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
formerIndex := formerCatalog.index(m.selectedTargetID)
|
||||
m.snapshot = completed.result.Snapshot
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable).retain(m.selectedTargetID, formerIndex)
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID).retain(m.selectedTargetID, formerIndex)
|
||||
if completed.result.Err != nil {
|
||||
m.status = "Error: " + completed.result.Err.Error()
|
||||
} else {
|
||||
@@ -356,7 +365,7 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
catalog := newTargetCatalog(m.snapshot, m.showUnavailable)
|
||||
catalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
switch key.String() {
|
||||
case "p":
|
||||
if !m.busy() {
|
||||
@@ -394,7 +403,7 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.dashboardNavigationAvailable() {
|
||||
formerIndex := catalog.index(m.selectedTargetID)
|
||||
m.showUnavailable = !m.showUnavailable
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable).retain(m.selectedTargetID, formerIndex)
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID).retain(m.selectedTargetID, formerIndex)
|
||||
m.detailsOffset = 0
|
||||
}
|
||||
case "up", "k":
|
||||
@@ -681,28 +690,10 @@ func (m Model) availableTargets() []app.TargetSnapshot {
|
||||
}
|
||||
|
||||
func (m *Model) retainTargetSelection() {
|
||||
catalog := newTargetCatalog(m.snapshot, m.showUnavailable)
|
||||
catalog := newTargetCatalog(m.snapshot, m.showUnavailable, m.selectedTargetID)
|
||||
m.selectedTargetID = catalog.retain(m.selectedTargetID, catalog.index(m.selectedTargetID))
|
||||
}
|
||||
|
||||
func (m *Model) applyTargetDetection(result app.TargetDetectionResult) {
|
||||
formerCatalog := newTargetCatalog(m.snapshot, m.showUnavailable)
|
||||
formerIndex := formerCatalog.index(m.selectedTargetID)
|
||||
for i := range m.snapshot.Targets {
|
||||
if m.snapshot.Targets[i].ID != result.TargetID {
|
||||
continue
|
||||
}
|
||||
m.snapshot.Targets[i].DetectionPhase = result.Phase
|
||||
m.snapshot.Targets[i].DetectionErr = result.Err
|
||||
m.snapshot.Targets[i].Available = result.Availability.Available
|
||||
m.snapshot.Targets[i].AvailabilityEvidence = result.Availability.Evidence
|
||||
m.snapshot.Targets[i].State = result.Inspection.State
|
||||
m.snapshot.Targets[i].Evidence = result.Inspection.Evidence
|
||||
break
|
||||
}
|
||||
m.selectedTargetID = newTargetCatalog(m.snapshot, m.showUnavailable).retain(m.selectedTargetID, formerIndex)
|
||||
}
|
||||
|
||||
func (f formState) labels() []string {
|
||||
if f.profile.Mode == profile.AdvancedMode {
|
||||
return []string{"Name", "HTTP URL", "HTTPS URL", "ALL/SOCKS URL", "Bypass"}
|
||||
|
||||
@@ -20,7 +20,7 @@ func (p terminalPresentation) mainView(model presentationState, width, height in
|
||||
p.theme.title.Render("proxyctl"), activeProfile, p.stateCountSummary(model.snapshot))
|
||||
}
|
||||
|
||||
catalog := newTargetCatalog(model.snapshot, model.showUnavailable)
|
||||
catalog := newTargetCatalog(model.snapshot, model.showUnavailable, model.selectedTargetID)
|
||||
footer := p.dashboardFooter(model, catalog)
|
||||
if model.targetConfirm && !model.applying {
|
||||
footer = "y: confirm n/Esc: cancel"
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/profile"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,9 @@ type progressiveDetectionModule struct {
|
||||
begins int
|
||||
}
|
||||
|
||||
func (m *progressiveDetectionModule) Snapshot() app.Snapshot { return m.snapshot }
|
||||
func (m *progressiveDetectionModule) Snapshot() app.Snapshot {
|
||||
return cloneDetectionSnapshot(m.snapshot)
|
||||
}
|
||||
func (m *progressiveDetectionModule) Dispatch(app.Intent) app.Result {
|
||||
return app.Result{Snapshot: m.snapshot}
|
||||
}
|
||||
@@ -27,7 +30,7 @@ func (m *progressiveDetectionModule) BeginTargetDetection() (app.Snapshot, <-cha
|
||||
m.snapshot.Targets[i].DetectionPhase = app.TargetChecking
|
||||
m.snapshot.Targets[i].DetectionErr = nil
|
||||
}
|
||||
return m.snapshot, m.streams[m.begins-1]
|
||||
return cloneDetectionSnapshot(m.snapshot), m.streams[m.begins-1]
|
||||
}
|
||||
|
||||
func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *testing.T) {
|
||||
@@ -58,7 +61,7 @@ func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *
|
||||
t.Fatal("startup did not wait for progressive detection")
|
||||
}
|
||||
|
||||
startup <- detectionUpdate(module.snapshot, "docker", app.TargetReady, true, target.Enabled, "docker enabled", nil)
|
||||
startup <- module.detectionUpdate("docker", app.TargetReady, true, target.Enabled, "docker enabled", nil)
|
||||
model, command = applyDetectionCommand(t, model, command)
|
||||
view := model.View()
|
||||
assertOrder(t, view, "Shell", "APT", "Docker")
|
||||
@@ -68,21 +71,18 @@ func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *
|
||||
}
|
||||
|
||||
failure := errors.New("apt-config inspection failed")
|
||||
startup <- detectionUpdate(module.snapshot, "shell", app.TargetDetectionFailed, false, "", "", failure)
|
||||
startup <- module.detectionUpdate("shell", app.TargetDetectionFailed, false, "", "", failure)
|
||||
model, command = applyDetectionCommand(t, model, command)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "! Check failed Shell", "> … Checking APT", "● Enabled Docker")
|
||||
|
||||
startup <- detectionUpdate(module.snapshot, "apt", app.TargetReady, true, target.External, "external APT proxy", nil)
|
||||
startup <- module.detectionUpdate("apt", app.TargetReady, false, target.Disabled, "apt-config is unavailable", nil)
|
||||
model, command = applyDetectionCommand(t, model, command)
|
||||
startup <- app.TargetDetectionUpdate{Complete: true}
|
||||
startup <- app.TargetDetectionUpdate{Snapshot: cloneDetectionSnapshot(module.snapshot), Complete: true}
|
||||
model, command = applyDetectionCommand(t, model, command)
|
||||
if command != nil || model.selectedTargetID != "apt" {
|
||||
t.Fatalf("startup completion command=%v selection=%q", command != nil, model.selectedTargetID)
|
||||
}
|
||||
assertUsableFrame(t, model.View(), 120, 36, "! Check failed Shell", "> ◇ External APT", "● Enabled Docker")
|
||||
model = updateKey(model, "up")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "> ! Check failed Shell", "Check error: apt-config inspection failed")
|
||||
model = updateKey(model, "down")
|
||||
assertUsableFrame(t, model.View(), 120, 36, "! Check failed Shell", "> ○ Unavailable APT", "● Enabled Docker", "Evidence: apt-config is unavailable")
|
||||
|
||||
updated, command := model.Update(keyMessage("r"))
|
||||
model = updated.(Model)
|
||||
@@ -91,7 +91,7 @@ func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *
|
||||
}
|
||||
assertUsableFrame(t, model.View(), 120, 36, "… Checking Shell", "> … Checking APT", "… Checking Docker")
|
||||
|
||||
refresh <- detectionUpdate(module.snapshot, "apt", app.TargetReady, true, target.Enabled, "apt enabled", nil)
|
||||
refresh <- module.detectionUpdate("apt", app.TargetReady, true, target.Enabled, "apt enabled", nil)
|
||||
model, command = applyDetectionCommand(t, model, command)
|
||||
assertUsableFrame(t, model.View(), 120, 36, "… Checking Shell", "> ● Enabled APT", "… Checking Docker")
|
||||
if model.selectedTargetID != "apt" {
|
||||
@@ -99,12 +99,26 @@ func TestCompleteModelStreamsStartupAndRefreshByTargetWithoutMovingSelection(t *
|
||||
}
|
||||
}
|
||||
|
||||
func detectionUpdate(snapshot app.Snapshot, id string, phase app.TargetDetectionPhase, available bool, state target.State, evidence string, err error) app.TargetDetectionUpdate {
|
||||
return app.TargetDetectionUpdate{Result: app.TargetDetectionResult{
|
||||
TargetID: id, Phase: phase,
|
||||
Availability: target.Availability{Available: available, Evidence: evidence},
|
||||
Inspection: target.Inspection{State: state, Evidence: evidence}, Err: err,
|
||||
}}
|
||||
func (m *progressiveDetectionModule) detectionUpdate(id string, phase app.TargetDetectionPhase, available bool, state target.State, evidence string, err error) app.TargetDetectionUpdate {
|
||||
for i := range m.snapshot.Targets {
|
||||
if m.snapshot.Targets[i].ID != id {
|
||||
continue
|
||||
}
|
||||
m.snapshot.Targets[i].DetectionPhase = phase
|
||||
m.snapshot.Targets[i].DetectionErr = err
|
||||
m.snapshot.Targets[i].Available = available
|
||||
m.snapshot.Targets[i].AvailabilityEvidence = evidence
|
||||
m.snapshot.Targets[i].State = state
|
||||
m.snapshot.Targets[i].Evidence = evidence
|
||||
break
|
||||
}
|
||||
return app.TargetDetectionUpdate{Snapshot: cloneDetectionSnapshot(m.snapshot), TargetID: id}
|
||||
}
|
||||
|
||||
func cloneDetectionSnapshot(snapshot app.Snapshot) app.Snapshot {
|
||||
snapshot.Targets = append([]app.TargetSnapshot(nil), snapshot.Targets...)
|
||||
snapshot.Profiles = append([]profile.Profile(nil), snapshot.Profiles...)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func applyDetectionCommand(t *testing.T, model Model, command tea.Cmd) (Model, tea.Cmd) {
|
||||
|
||||
Reference in New Issue
Block a user