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>
This commit is contained in:
1 parent
95f8066eed
commit
792b7fbb84
4 files changed
+244
-18
No files matched your search
@@ -5,25 +5,110 @@ import (
|
||||
"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 []targetGroupDTO
|
||||
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) {
|
||||
items, err := s.CA.ListTargetGroups(r.Context())
|
||||
data := targetsPageData{Items: items}
|
||||
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) {
|
||||
items, listErr := s.CA.ListTargetGroups(r.Context())
|
||||
data, listErr := s.loadTargetsPage(r)
|
||||
if actionErr == nil {
|
||||
actionErr = listErr
|
||||
}
|
||||
s.renderFragment(w, "targets_table", targetsPageData{Items: items}, actionErr)
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user