Preserve config preview selections and results
This commit is contained in:
parent
ec419e87c3
commit
8806985c8b
@ -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)
|
||||
|
||||
@ -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>
|
||||
{{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}}
|
||||
|
||||
@ -365,11 +365,12 @@ func TestUI_ConfigPreviewPageKeepsApplyActionAfterUploadResult(t *testing.T) {
|
||||
Size: 123,
|
||||
},
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user