Files
cloud-ip-validator/internal/dashboard/handlers_targets.go
T
ayurishchevandClaude Sonnet 5 792b7fbb84 Show check-type usage and stats on the target groups dashboard page
Each target group's relationship to the check types that reference it was
previously invisible without cross-referencing /check-types by hand.
handleTargetsPage/renderTargetsTable now resolve that server-side via
loadTargetsPage (group -> referencing check types, enabled or not) and
render it as a status pill per group, plus a stats row (group count,
total targets, check types using them, unused groups) kept live via an
out-of-band swap on every create/update/delete.

Also auto-sizes the targets textarea as you type (targets.html), and
lets the stat-card grid collapse to however many state cards actually
exist instead of leaving empty background where a fixed repeat(6, ...)
had no card to fill.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 10:52:27 +03:00

145 lines
4.5 KiB
Go

package dashboard
import (
"fmt"
"net/http"
)
// usedByTag names one check type that references a target group, and
// whether that check type is currently enabled — rendered as a status pill
// next to the group so the relationship (invisible before this page had
// this column) is visible without cross-referencing /check-types by hand.
type usedByTag struct {
Name string
Enabled bool
}
// targetGroupView is targetGroupDTO plus the check types that reference it,
// resolved server-side from ListCheckTypes so the template stays a plain
// range with no cross-referencing logic of its own.
type targetGroupView struct {
targetGroupDTO
UsedBy []usedByTag
}
type targetsStats struct {
Groups int
TotalTargets int
CheckTypesUsing int
Unused int
}
type targetsPageData struct {
PageData
Items []targetGroupView
Stats targetsStats
}
// loadTargetsPage fetches target groups and check types together and
// resolves the group -> referencing-check-types relationship server-side.
// A ListCheckTypes failure degrades to "no usage info" rather than hiding
// the group list — the error still surfaces via the banner.
func (s *Server) loadTargetsPage(r *http.Request) (targetsPageData, error) {
groups, err := s.CA.ListTargetGroups(r.Context())
if err != nil {
return targetsPageData{}, err
}
checkTypes, ctErr := s.CA.ListCheckTypes(r.Context())
usedBy := make(map[string][]usedByTag)
checkTypesUsing := 0
for _, ct := range checkTypes {
if len(ct.Targets) > 0 {
checkTypesUsing++
}
for _, g := range ct.Targets {
usedBy[g] = append(usedBy[g], usedByTag{Name: ct.Name, Enabled: ct.Enabled})
}
}
items := make([]targetGroupView, len(groups))
totalTargets := 0
unused := 0
for i, g := range groups {
items[i] = targetGroupView{targetGroupDTO: g, UsedBy: usedBy[g.Name]}
totalTargets += len(g.Targets)
if len(usedBy[g.Name]) == 0 {
unused++
}
}
return targetsPageData{
Items: items,
Stats: targetsStats{
Groups: len(groups),
TotalTargets: totalTargets,
CheckTypesUsing: checkTypesUsing,
Unused: unused,
},
}, ctErr
}
func (s *Server) handleTargetsPage(w http.ResponseWriter, r *http.Request) {
data, err := s.loadTargetsPage(r)
data.ActiveNav = "targets"
data.Banner = bannerFor(err)
s.renderPage(w, "targets_page", data)
}
// renderTargetsTable renders the #targets-table-wrap swap target plus,
// appended to the same response, an out-of-band update of #targets-stats-wrap
// (see targets_stats_oob in targets.html) — that block sits above the create
// form, outside the swap target, so it needs its own refresh to stay live
// after a create/update/delete. Can't use the shared renderFragment helper
// here because of that second OOB block; otherwise mirrors it exactly,
// including always appending error_banner last.
func (s *Server) renderTargetsTable(w http.ResponseWriter, r *http.Request, actionErr error) {
data, listErr := s.loadTargetsPage(r)
if actionErr == nil {
actionErr = listErr
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.tmpl.ExecuteTemplate(w, "targets_table", data); err != nil {
s.Log.Error("render fragment", "template", "targets_table", "err", err)
return
}
if err := s.tmpl.ExecuteTemplate(w, "targets_stats_oob", data); err != nil {
s.Log.Error("render targets stats oob", "err", err)
}
if err := s.tmpl.ExecuteTemplate(w, "error_banner", bannerFor(actionErr)); err != nil {
s.Log.Error("render error banner", "err", err)
}
}
func (s *Server) handleTargetCreate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
s.renderTargetsTable(w, r, fmt.Errorf("invalid form: %w", err))
return
}
name := r.PostFormValue("name")
targets := splitList(r.PostFormValue("targets"))
if name == "" {
s.renderTargetsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "имя группы обязательно"})
return
}
err := s.CA.PutTargetGroup(r.Context(), name, targets)
s.renderTargetsTable(w, r, err)
}
func (s *Server) handleTargetUpdate(w http.ResponseWriter, r *http.Request) {
group := r.PathValue("group")
if err := r.ParseForm(); err != nil {
s.renderTargetsTable(w, r, fmt.Errorf("invalid form: %w", err))
return
}
targets := splitList(r.PostFormValue("targets"))
err := s.CA.PutTargetGroup(r.Context(), group, targets)
s.renderTargetsTable(w, r, err)
}
func (s *Server) handleTargetDelete(w http.ResponseWriter, r *http.Request) {
group := r.PathValue("group")
err := s.CA.DeleteTargetGroup(r.Context(), group)
s.renderTargetsTable(w, r, err)
}