Align batch operations with audit workflow
This commit is contained in:
parent
2eca56e59a
commit
2a994b7220
@ -147,6 +147,72 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic
|
||||
}
|
||||
return v
|
||||
},
|
||||
"taskGroupLabel": func(v any) string {
|
||||
switch fmt.Sprint(v) {
|
||||
case "config_apply":
|
||||
return "批量配置"
|
||||
case "media_start", "media_restart", "media_stop":
|
||||
return "批量服务"
|
||||
case "reload", "rollback":
|
||||
return "设备操作"
|
||||
default:
|
||||
return "其他任务"
|
||||
}
|
||||
},
|
||||
"taskActionLabel": func(v any) string {
|
||||
switch fmt.Sprint(v) {
|
||||
case "config_apply":
|
||||
return "下发识别配置"
|
||||
case "reload":
|
||||
return "重载识别服务"
|
||||
case "rollback":
|
||||
return "回滚识别配置"
|
||||
case "media_start":
|
||||
return "启动视频分析服务"
|
||||
case "media_restart":
|
||||
return "重启视频分析服务"
|
||||
case "media_stop":
|
||||
return "停止视频分析服务"
|
||||
default:
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
},
|
||||
"taskGroupClass": func(v any) string {
|
||||
switch fmt.Sprint(v) {
|
||||
case "config_apply":
|
||||
return "pill run"
|
||||
case "media_start", "media_restart", "media_stop":
|
||||
return "pill ok"
|
||||
case "reload", "rollback":
|
||||
return "pill warn"
|
||||
default:
|
||||
return "pill"
|
||||
}
|
||||
},
|
||||
"taskStatusLabel": func(v any) string {
|
||||
switch fmt.Sprint(v) {
|
||||
case "success":
|
||||
return "成功"
|
||||
case "failed":
|
||||
return "失败"
|
||||
case "running":
|
||||
return "执行中"
|
||||
default:
|
||||
return "待执行"
|
||||
}
|
||||
},
|
||||
"taskStatusClass": func(v any) string {
|
||||
switch fmt.Sprint(v) {
|
||||
case "success":
|
||||
return "pill ok"
|
||||
case "failed":
|
||||
return "pill bad"
|
||||
case "running":
|
||||
return "pill run"
|
||||
default:
|
||||
return "pill"
|
||||
}
|
||||
},
|
||||
"ago": func(ms int64) string {
|
||||
if ms <= 0 {
|
||||
return "-"
|
||||
@ -779,7 +845,7 @@ func (u *UI) actionDeviceMediaServerConfigUploadBatch(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
func (u *UI) pageTasks(w http.ResponseWriter, r *http.Request) {
|
||||
u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices()})
|
||||
u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices()})
|
||||
}
|
||||
|
||||
func (u *UI) taskPageData(task *models.Task) PageData {
|
||||
@ -837,13 +903,13 @@ func (u *UI) actionCreateTask(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var payload any
|
||||
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
|
||||
u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: "高级参数 JSON 无效: " + err.Error(), RawJSON: raw, DeviceIDs: ids})
|
||||
u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: "高级参数 JSON 无效: " + err.Error(), RawJSON: raw, DeviceIDs: ids})
|
||||
return
|
||||
}
|
||||
|
||||
task, err := u.tasks.CreateTask(typeStr, deviceIDs, payload)
|
||||
if err != nil {
|
||||
u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: err.Error(), RawJSON: raw, DeviceIDs: ids})
|
||||
u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: err.Error(), RawJSON: raw, DeviceIDs: ids})
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound)
|
||||
|
||||
@ -18,9 +18,9 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>任务</th>
|
||||
<th>动作</th>
|
||||
<th>目标设备</th>
|
||||
<th>目标设备数</th>
|
||||
<th>结果</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
@ -28,10 +28,15 @@
|
||||
<tbody>
|
||||
{{range .Tasks}}
|
||||
<tr>
|
||||
<td class="mono">{{.ID}}</td>
|
||||
<td>{{.Type}}</td>
|
||||
<td class="mono">{{range $i, $id := .DeviceIDs}}{{if $i}}, {{end}}{{$id}}{{end}}</td>
|
||||
<td><span class="pill">{{.Status}}</span></td>
|
||||
<td>
|
||||
<span class="{{taskGroupClass .Type}}">{{taskGroupLabel .Type}}</span>
|
||||
<div class="muted small" style="margin-top:4px">{{taskActionLabel .Type}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mono">{{.ID}}</div>
|
||||
</td>
|
||||
<td>{{len .DeviceIDs}} 台</td>
|
||||
<td><span class="{{taskStatusClass .Status}}">{{taskStatusLabel .Status}}</span></td>
|
||||
<td class="muted small">{{if .Payload}}已记录任务参数{{else}}无附加参数{{end}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{{define "task"}}
|
||||
<div class="card">
|
||||
<div class="actions">
|
||||
<a class="btn ghost" href="/ui/audit">{{icon "audit"}}<span>返回操作审计</span></a>
|
||||
<a class="btn ghost" href="/ui/tasks">{{icon "devices"}}<span>返回任务列表</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -13,28 +13,20 @@
|
||||
<div class="summary-chip-value mono">{{.Task.ID}}</div>
|
||||
</div>
|
||||
<div class="summary-chip">
|
||||
<div class="summary-chip-label">操作类型</div>
|
||||
<div class="summary-chip-label">任务类型</div>
|
||||
<div class="summary-chip-value">
|
||||
{{if eq .Task.Type "config_apply"}}下发识别配置
|
||||
{{else if eq .Task.Type "reload"}}重载识别服务
|
||||
{{else if eq .Task.Type "rollback"}}回滚识别配置
|
||||
{{else if eq .Task.Type "media_start"}}启动视频分析服务
|
||||
{{else if eq .Task.Type "media_restart"}}重启视频分析服务
|
||||
{{else if eq .Task.Type "media_stop"}}停止视频分析服务
|
||||
{{else}}<span class="mono">{{.Task.Type}}</span>{{end}}
|
||||
<span class="{{taskGroupClass .Task.Type}}">{{taskGroupLabel .Task.Type}}</span>
|
||||
<div class="muted small" style="margin-top:4px">{{taskActionLabel .Task.Type}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-chip">
|
||||
<div class="summary-chip-label">设备数量</div>
|
||||
<div class="summary-chip-value">{{len .Task.DeviceIDs}}</div>
|
||||
<div class="summary-chip-label">目标设备数</div>
|
||||
<div class="summary-chip-value">{{len .Task.DeviceIDs}} 台</div>
|
||||
</div>
|
||||
<div class="summary-chip">
|
||||
<div class="summary-chip-label">当前状态</div>
|
||||
<div class="summary-chip-value" id="task-status-value" data-task-status="{{.Task.Status}}">
|
||||
{{if eq .Task.Status "success"}}<span class="pill ok">成功</span>
|
||||
{{else if eq .Task.Status "failed"}}<span class="pill bad">失败</span>
|
||||
{{else if eq .Task.Status "running"}}<span class="pill run">执行中</span>
|
||||
{{else}}<span class="pill">待执行</span>{{end}}
|
||||
<span class="{{taskStatusClass .Task.Status}}">{{taskStatusLabel .Task.Status}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -63,10 +55,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="st">
|
||||
{{if eq .Status "success"}}<span class="pill ok">成功</span>
|
||||
{{else if eq .Status "failed"}}<span class="pill bad">失败</span>
|
||||
{{else if eq .Status "running"}}<span class="pill run">执行中</span>
|
||||
{{else}}<span class="pill">待执行</span>{{end}}
|
||||
<span class="{{taskStatusClass .Status}}">{{taskStatusLabel .Status}}</span>
|
||||
</td>
|
||||
<td class="pg mono">{{.Progress}}</td>
|
||||
<td class="er">{{.Error}}</td>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{{define "tasks"}}
|
||||
<div class="card">
|
||||
<h2>创建批量运维任务</h2>
|
||||
<h2>创建任务</h2>
|
||||
<div class="muted small">
|
||||
用于向多台边缘节点下发识别配置、重载服务、回滚配置或控制视频分析服务。
|
||||
</div>
|
||||
@ -33,32 +33,26 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>任务中心</h2>
|
||||
<h2>任务列表</h2>
|
||||
<div class="table-wrap" style="margin-top:10px">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>任务标识</th><th>操作</th><th>状态</th><th>节点数</th></tr>
|
||||
<tr><th>任务</th><th>类型</th><th>目标设备数</th><th>状态</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Tasks}}
|
||||
<tr>
|
||||
<td><a class="mono" href="/ui/tasks/{{.ID}}">{{.ID}}</a></td>
|
||||
<td>
|
||||
{{if eq .Type "config_apply"}}下发识别配置
|
||||
{{else if eq .Type "reload"}}重载识别服务
|
||||
{{else if eq .Type "rollback"}}回滚识别配置
|
||||
{{else if eq .Type "media_start"}}启动视频分析服务
|
||||
{{else if eq .Type "media_restart"}}重启视频分析服务
|
||||
{{else if eq .Type "media_stop"}}停止视频分析服务
|
||||
{{else}}<span class="mono">{{.Type}}</span>{{end}}
|
||||
<div><a class="mono" href="/ui/tasks/{{.ID}}">{{.ID}}</a></div>
|
||||
<div class="muted small">{{taskActionLabel .Type}}</div>
|
||||
</td>
|
||||
<td>
|
||||
{{if eq .Status "success"}}<span class="pill ok">成功</span>
|
||||
{{else if eq .Status "failed"}}<span class="pill bad">失败</span>
|
||||
{{else if eq .Status "running"}}<span class="pill run">执行中</span>
|
||||
{{else}}<span class="pill">待执行</span>{{end}}
|
||||
<span class="{{taskGroupClass .Type}}">{{taskGroupLabel .Type}}</span>
|
||||
</td>
|
||||
<td>{{len .DeviceIDs}} 台</td>
|
||||
<td>
|
||||
<span class="{{taskStatusClass .Status}}">{{taskStatusLabel .Status}}</span>
|
||||
</td>
|
||||
<td>{{len .DeviceIDs}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
@ -436,16 +436,20 @@ func writeTestFile(t *testing.T, path string, body string) {
|
||||
func TestUI_TaskPageRendersBatchSummaryAndDeviceResults(t *testing.T) {
|
||||
ui := newTestUI(t)
|
||||
ui.registry.UpdateDevice(&models.Device{DeviceID: "edge-02", DeviceName: "辅助节点", IP: "127.0.0.2", AgentPort: 9100, MediaPort: 9000, Online: true})
|
||||
ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: createBatchConfigMediaRepo(t)})
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("action", "reload")
|
||||
form.Add("device_id", "edge-01")
|
||||
form.Add("device_id", "edge-02")
|
||||
req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-action", strings.NewReader(form.Encode()))
|
||||
form.Set("template", "workshop_face_shoe_alarm")
|
||||
form.Set("profile", "local_3588_test")
|
||||
form.Set("config_id", "batch_edge")
|
||||
form.Set("config_version", "20260420.090000")
|
||||
req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-config", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
ui.actionDevicesBatchAction(rr, req)
|
||||
ui.actionDeviceBatchConfig(rr, req)
|
||||
if rr.Code != http.StatusFound {
|
||||
t.Fatalf("expected redirect, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
@ -466,11 +470,19 @@ func TestUI_TaskPageRendersBatchSummaryAndDeviceResults(t *testing.T) {
|
||||
t.Fatalf("expected task page 200, got %d: %s", rrTask.Code, rrTask.Body.String())
|
||||
}
|
||||
body := rrTask.Body.String()
|
||||
for _, want := range []string{"任务详情", "返回操作审计", "设备结果表", "设备数量", "入口识别节点", "辅助节点", "edge-01", "edge-02", `id="task-status-value"`, "syncTaskStatus()"} {
|
||||
for _, want := range []string{"任务详情", "返回任务列表", "设备结果表", "任务类型", "目标设备数", "批量配置", "下发识别配置", "2 台", "入口识别节点", "辅助节点", "edge-01", "edge-02", `id="task-status-value"`, "syncTaskStatus()", `href="/ui/tasks"`} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("expected task page to contain %q, got:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"任务中心", "节点执行情况"} {
|
||||
if strings.Contains(body, forbidden) {
|
||||
t.Fatalf("task page should not contain %q, got:\n%s", forbidden, body)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, "返回操作审计") {
|
||||
t.Fatalf("task page should not point back to audit, got:\n%s", body)
|
||||
}
|
||||
if strings.Index(body, "edge-01") > strings.Index(body, "edge-02") {
|
||||
t.Fatalf("expected task devices to keep selection order, got:\n%s", body)
|
||||
}
|
||||
@ -1521,17 +1533,32 @@ func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) {
|
||||
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
|
||||
reg := service.NewRegistryService(cfg, nil)
|
||||
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.0", GitSha: "5c04681"})
|
||||
reg.UpdateDevice(&models.Device{DeviceID: "edge-02", DeviceName: "辅助节点", IP: "127.0.0.2", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.1", GitSha: "8eaf213"})
|
||||
tasks := service.NewTaskService(cfg, nil, reg)
|
||||
task, err := tasks.CreateTask("reload", []string{"edge-01"}, nil)
|
||||
taskConfig, err := tasks.CreateTask("config_apply", []string{"edge-01", "edge-02"}, map[string]any{"config": map[string]any{}})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
task.Mu.Lock()
|
||||
task.Status = models.TaskSuccess
|
||||
if ds, ok := task.Devices["edge-01"]; ok && ds != nil {
|
||||
ds.Status = models.TaskSuccess
|
||||
taskService, err := tasks.CreateTask("media_restart", []string{"edge-01", "edge-02"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTask: %v", err)
|
||||
}
|
||||
task.Mu.Unlock()
|
||||
taskConfig.Mu.Lock()
|
||||
taskConfig.Status = models.TaskSuccess
|
||||
for _, did := range taskConfig.DeviceIDs {
|
||||
if ds, ok := taskConfig.Devices[did]; ok && ds != nil {
|
||||
ds.Status = models.TaskSuccess
|
||||
}
|
||||
}
|
||||
taskConfig.Mu.Unlock()
|
||||
taskService.Mu.Lock()
|
||||
taskService.Status = models.TaskSuccess
|
||||
for _, did := range taskService.DeviceIDs {
|
||||
if ds, ok := taskService.Devices[did]; ok && ds != nil {
|
||||
ds.Status = models.TaskSuccess
|
||||
}
|
||||
}
|
||||
taskService.Mu.Unlock()
|
||||
ui, err := NewUI(nil, reg, nil, tasks, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewUI: %v", err)
|
||||
@ -1539,16 +1566,51 @@ func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) {
|
||||
|
||||
rrAudit := httptest.NewRecorder()
|
||||
ui.pageAudit(rrAudit, httptest.NewRequest(http.MethodGet, "/ui/audit", nil))
|
||||
for _, want := range []string{"操作审计", "审计记录", "谁做了什么、对哪台设备做的、结果如何", "reload", "edge-01", task.ID} {
|
||||
for _, want := range []string{"操作审计", "审计记录", "批量配置", "批量服务", "目标设备数", "2 台", "下发识别配置", "重启视频分析服务"} {
|
||||
if !strings.Contains(rrAudit.Body.String(), want) {
|
||||
t.Fatalf("expected audit HTML to contain %q", want)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"框架版", "后续", `disabled`} {
|
||||
for _, forbidden := range []string{"框架版", "后续", "任务中心", "节点执行情况", `disabled`} {
|
||||
if strings.Contains(rrAudit.Body.String(), forbidden) {
|
||||
t.Fatalf("audit HTML should not contain placeholder marker %q", forbidden)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"success", "failed", "running"} {
|
||||
if strings.Contains(rrAudit.Body.String(), forbidden) {
|
||||
t.Fatalf("audit HTML should not leak raw status enum %q", forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
rrTasks := httptest.NewRecorder()
|
||||
ui.pageTasks(rrTasks, httptest.NewRequest(http.MethodGet, "/ui/tasks", nil))
|
||||
for _, want := range []string{"任务列表", "批量配置", "批量服务", "目标设备数"} {
|
||||
if !strings.Contains(rrTasks.Body.String(), want) {
|
||||
t.Fatalf("expected tasks HTML to contain %q", want)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"任务中心", "节点执行情况"} {
|
||||
if strings.Contains(rrTasks.Body.String(), forbidden) {
|
||||
t.Fatalf("tasks HTML should not contain placeholder marker %q", forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
rrTaskConfig := httptest.NewRecorder()
|
||||
reqTask := httptest.NewRequest(http.MethodGet, "/ui/tasks/"+taskConfig.ID, nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", taskConfig.ID)
|
||||
reqTask = reqTask.WithContext(context.WithValue(reqTask.Context(), chi.RouteCtxKey, rctx))
|
||||
ui.pageTask(rrTaskConfig, reqTask)
|
||||
for _, want := range []string{"批量配置", "下发识别配置", "返回任务列表"} {
|
||||
if !strings.Contains(rrTaskConfig.Body.String(), want) {
|
||||
t.Fatalf("expected task detail HTML to contain %q", want)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"任务中心", "返回操作审计"} {
|
||||
if strings.Contains(rrTaskConfig.Body.String(), forbidden) {
|
||||
t.Fatalf("task detail HTML should not contain %q", forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
rrSystem := httptest.NewRecorder()
|
||||
ui.pageSystem(rrSystem, httptest.NewRequest(http.MethodGet, "/ui/system", nil))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user