Preserve config preview selections and results

This commit is contained in:
tian 2026-04-19 13:15:32 +08:00
parent ec419e87c3
commit 8806985c8b
3 changed files with 151 additions and 14 deletions

View File

@ -49,6 +49,12 @@ type PageData struct {
ConfigStatusErr string
ConfigSources service.ConfigPreviewSources
ConfigPreview *service.ConfigPreviewResult
ResultTitle string
SelectedTemplate string
SelectedProfile string
SelectedOverlays []string
SelectedConfigID string
SelectedVersion string
Tasks []models.Task
Task *models.Task
Templates []service.Template
@ -107,6 +113,14 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic
b, _ := json.MarshalIndent(v, "", " ")
return string(b)
},
"hasString": func(items []string, want string) bool {
for _, item := range items {
if item == want {
return true
}
}
return false
},
"ago": func(ms int64) string {
if ms <= 0 {
return "-"
@ -873,6 +887,11 @@ func (u *UI) actionDeviceConfigPreview(w http.ResponseWriter, r *http.Request) {
preview, err := u.preview.Render(req)
data := u.configPreviewPageData(dev)
data.ConfigPreview = preview
data.SelectedTemplate = req.Template
data.SelectedProfile = req.Profile
data.SelectedOverlays = append([]string(nil), req.Overlays...)
data.SelectedConfigID = req.ConfigID
data.SelectedVersion = req.ConfigVersion
if err != nil {
data.Error = err.Error()
}
@ -900,9 +919,11 @@ func (u *UI) actionDeviceConfigCandidate(w http.ResponseWriter, r *http.Request)
return
}
data.ConfigPreview = previewResultFromJSON(raw)
populateSelectionsFromPreview(&data)
body, code, err := u.agent.Do("PUT", dev.IP, dev.AgentPort, "/v1/config/candidate", []byte(raw))
data.Message = fmt.Sprintf("PUT /v1/config/candidate -> %d", code)
data.RawText = prettyJSON(body)
data.ResultTitle = "候选配置结果"
if err != nil {
data.Error = err.Error()
}
@ -920,10 +941,12 @@ func (u *UI) actionDeviceConfigCandidateApply(w http.ResponseWriter, r *http.Req
raw := strings.TrimSpace(r.FormValue("json"))
if raw != "" {
data.ConfigPreview = previewResultFromJSON(raw)
populateSelectionsFromPreview(&data)
}
body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/candidate/apply", []byte(`{}`))
data.Message = fmt.Sprintf("POST /v1/config/candidate/apply -> %d", code)
data.RawText = prettyJSON(body)
data.ResultTitle = "应用候选配置结果"
if err != nil {
data.Error = err.Error()
}
@ -932,7 +955,15 @@ func (u *UI) actionDeviceConfigCandidateApply(w http.ResponseWriter, r *http.Req
func (u *UI) configPreviewPageData(dev *models.Device) PageData {
sources, err := u.preview.ListSources()
data := PageData{Title: "配置预览", Device: dev, ConfigSources: sources}
data := PageData{
Title: "配置预览",
Device: dev,
ConfigSources: sources,
SelectedTemplate: "workshop_face_shoe_alarm",
SelectedProfile: "local_3588_test",
SelectedOverlays: []string{"face_debug"},
SelectedConfigID: "preview_" + dev.DeviceID,
}
if err != nil {
data.Error = err.Error()
}
@ -972,6 +1003,37 @@ func previewResultFromJSON(raw string) *service.ConfigPreviewResult {
}
}
func populateSelectionsFromPreview(data *PageData) {
if data == nil || data.ConfigPreview == nil {
return
}
if metadata := data.ConfigPreview.Metadata; metadata != nil {
if v, _ := metadata["template"].(string); strings.TrimSpace(v) != "" {
data.SelectedTemplate = v
}
if v, _ := metadata["profile"].(string); strings.TrimSpace(v) != "" {
data.SelectedProfile = v
}
if v, _ := metadata["config_id"].(string); strings.TrimSpace(v) != "" {
data.SelectedConfigID = v
}
if v, _ := metadata["config_version"].(string); strings.TrimSpace(v) != "" {
data.SelectedVersion = v
}
if items, ok := metadata["overlays"].([]any); ok {
overlays := make([]string, 0, len(items))
for _, item := range items {
if s, ok := item.(string); ok && strings.TrimSpace(s) != "" {
overlays = append(overlays, s)
}
}
if len(overlays) > 0 {
data.SelectedOverlays = overlays
}
}
}
}
func (u *UI) actionDeviceConfigUIPlan(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
dev, ok := u.findDevice(id)

View File

@ -19,7 +19,7 @@
<div class="muted small">模板</div>
<select name="template">
{{range .ConfigSources.Templates}}
<option value="{{.Name}}" {{if eq .Name "workshop_face_shoe_alarm"}}selected{{end}}>{{.Name}}</option>
<option value="{{.Name}}" {{if eq .Name $.SelectedTemplate}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
@ -27,17 +27,17 @@
<div class="muted small">Profile</div>
<select name="profile">
{{range .ConfigSources.Profiles}}
<option value="{{.Name}}" {{if eq .Name "local_3588_test"}}selected{{end}}>{{.Name}}</option>
<option value="{{.Name}}" {{if eq .Name $.SelectedProfile}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div>
<div class="muted small">config_id</div>
<input name="config_id" value="preview_{{.Device.DeviceID}}" />
<input name="config_id" value="{{.SelectedConfigID}}" />
</div>
<div>
<div class="muted small">config_version</div>
<input name="config_version" placeholder="留空自动生成" />
<input name="config_version" value="{{.SelectedVersion}}" placeholder="留空自动生成" />
</div>
</div>
<div style="margin-top:12px">
@ -45,7 +45,7 @@
<div class="actions" style="margin-top:6px">
{{range .ConfigSources.Overlays}}
<label class="btn ghost">
<input type="checkbox" name="overlay" value="{{.Name}}" {{if eq .Name "face_debug"}}checked{{end}} />
<input type="checkbox" name="overlay" value="{{.Name}}" {{if hasString $.SelectedOverlays .Name}}checked{{end}} />
{{.Name}}
</label>
{{end}}
@ -55,9 +55,7 @@
<button type="submit">生成预览</button>
<button type="button" disabled>上传为候选配置</button>
{{if and .ConfigStatus .ConfigStatus.Candidate .ConfigStatus.Candidate.Exists}}
<form method="post" action="/ui/devices/{{.Device.DeviceID}}/config-candidate/apply" style="display:inline">
<button type="submit">应用候选配置</button>
</form>
<button type="submit" formaction="/ui/devices/{{.Device.DeviceID}}/config-candidate/apply">应用候选配置</button>
{{else}}
<button type="button" disabled>应用候选配置</button>
{{end}}
@ -90,13 +88,23 @@
<button type="submit" formaction="/ui/devices/{{.Device.DeviceID}}/config-candidate/apply">应用候选配置</button>
<a class="btn ghost" href="/ui/devices/{{.Device.DeviceID}}">查看当前运行配置</a>
</form>
<pre>{{.ConfigPreview.JSON}}</pre>
{{if .RawText}}
<details>
<summary class="muted small">展开完整 JSON</summary>
<pre>{{.ConfigPreview.JSON}}</pre>
</details>
{{else}}
<details open>
<summary class="muted small">展开完整 JSON</summary>
<pre>{{.ConfigPreview.JSON}}</pre>
</details>
{{end}}
</div>
{{end}}
{{if .RawText}}
<div class="card">
<h2>上传结果</h2>
<h2>{{if .ResultTitle}}{{.ResultTitle}}{{else}}执行结果{{end}}</h2>
<pre>{{.RawText}}</pre>
</div>
{{end}}

View File

@ -364,12 +364,13 @@ func TestUI_ConfigPreviewPageKeepsApplyActionAfterUploadResult(t *testing.T) {
},
Size: 123,
},
RawText: `{"ok":true}`,
RawText: `{"ok":true}`,
ResultTitle: "应用候选配置结果",
})
body := rr.Body.String()
for _, want := range []string{
"上传结果",
"应用候选配置结果",
"上传为候选配置",
"应用候选配置",
`formaction="/ui/devices/edge-01/config-candidate/apply"`,
@ -428,7 +429,7 @@ func TestUI_ActionDeviceConfigCandidateKeepsPreviewApplyAction(t *testing.T) {
body := rr.Body.String()
for _, want := range []string{
"上传结果",
"候选配置结果",
"应用候选配置",
`formaction="/ui/devices/edge-01/config-candidate/apply"`,
} {
@ -438,6 +439,72 @@ func TestUI_ActionDeviceConfigCandidateKeepsPreviewApplyAction(t *testing.T) {
}
}
func TestUI_ConfigPreviewKeepsSelectedOverlayAfterPreview(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
rr := httptest.NewRecorder()
ui.render(rr, req, "config_preview", PageData{
Title: "配置预览",
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
ConfigSources: service.ConfigPreviewSources{Templates: []service.ConfigSource{{Name: "workshop_face_shoe_alarm"}}, Profiles: []service.ConfigSource{{Name: "local_3588_test"}}, Overlays: []service.ConfigSource{{Name: "face_debug"}, {Name: "face_test_sensitive"}}},
SelectedTemplate: "workshop_face_shoe_alarm",
SelectedProfile: "local_3588_test",
SelectedOverlays: []string{"face_test_sensitive"},
SelectedConfigID: "preview_edge-01",
ConfigPreview: &service.ConfigPreviewResult{
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v1","template":"workshop_face_shoe_alarm","profile":"local_3588_test","overlays":["face_test_sensitive"]}}`,
Metadata: map[string]any{
"config_id": "preview_edge-01",
"config_version": "v1",
"template": "workshop_face_shoe_alarm",
"profile": "local_3588_test",
"overlays": []any{"face_test_sensitive"},
},
Size: 123,
},
})
body := rr.Body.String()
if !strings.Contains(body, `value="face_test_sensitive" checked`) {
t.Fatalf("expected selected overlay to remain checked, got:\n%s", body)
}
if strings.Contains(body, `value="face_debug" checked`) {
t.Fatalf("did not expect default overlay to stay checked, got:\n%s", body)
}
}
func TestUI_ConfigPreviewCollapsesJSONAfterActionResult(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/devices/edge-01/config-preview", nil)
rr := httptest.NewRecorder()
ui.render(rr, req, "config_preview", PageData{
Title: "配置预览",
Device: &models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100},
ConfigSources: service.ConfigPreviewSources{Templates: []service.ConfigSource{{Name: "workshop_face_shoe_alarm"}}, Profiles: []service.ConfigSource{{Name: "local_3588_test"}}, Overlays: []service.ConfigSource{{Name: "face_debug"}}},
SelectedTemplate: "workshop_face_shoe_alarm",
SelectedProfile: "local_3588_test",
SelectedOverlays: []string{"face_debug"},
SelectedConfigID: "preview_edge-01",
ConfigPreview: &service.ConfigPreviewResult{
JSON: `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[]}`,
Metadata: map[string]any{"config_id": "preview_edge-01"},
Size: 64,
},
ResultTitle: "候选配置结果",
RawText: `{"ok":true}`,
})
body := rr.Body.String()
if !strings.Contains(body, "<details") {
t.Fatalf("expected json panel to use details, got:\n%s", body)
}
if strings.Contains(body, "<details open>") {
t.Fatalf("expected json panel to be collapsed after action result, got:\n%s", body)
}
}
func TestUI_ModelDeploymentPageRendersDeviceActions(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/models", nil)