grafana_gen/dashboard.go
2026-02-24 12:45:35 +03:00

254 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"fmt"
"log"
"strings"
)
// generateSingleDashboard создает один дашборд со всеми клиентами.
// Структура, метаданные и логика панелей полностью управляются шаблоном.
func generateSingleDashboard(clients map[string]ClientData, template *DashboardTemplate, config Config) map[string]interface{} {
panels := []interface{}{}
yPos := 0
panelID := 100
datasourceType := template.Datasource["type"].(string)
datasourceUID := template.Datasource["uid"].(string)
useCollapsedRows := template.Layout.UseCollapsedRows
for _, client := range clients {
if len(client.Domains) == 0 {
continue
}
if useCollapsedRows {
row := createCollapsedRow(client, template, datasourceType, datasourceUID, panelID, yPos)
panels = append(panels, row)
panelID += 100
yPos++
} else {
panels = append(panels, createRowPanel(client.ClientTitle, template, panelID, yPos))
panelID++
yPos++
tenantPanels, nextID, nextY := generateLayoutPanels(
template.Layout.TenantPanels, client, nil,
template, datasourceType, datasourceUID, panelID, yPos,
)
panels = append(panels, tenantPanels...)
panelID = nextID
yPos = nextY
for _, domain := range client.Domains {
domainPanels, nextID, nextY := generateLayoutPanels(
template.Layout.DomainPanels, client, &domain,
template, datasourceType, datasourceUID, panelID, yPos,
)
panels = append(panels, domainPanels...)
panelID = nextID
yPos = nextY
}
}
}
// Берём метаданные дашборда из шаблона
dashboardBase := deepCopy(template.Dashboard).(map[string]interface{})
applyDashboardMeta(dashboardBase, template, config.DashboardTitle)
dashboardBase["panels"] = panels
return map[string]interface{}{
"dashboard": dashboardBase,
"overwrite": true,
}
}
// applyDashboardMeta применяет метаданные из секции dashboard_meta шаблона.
// Значения из шаблона используются как дефолты; title всегда берётся из config.
func applyDashboardMeta(dashboardBase map[string]interface{}, template *DashboardTemplate, title string) {
dashboardBase["title"] = title
meta := template.DashboardMeta
if meta == nil {
// Минимальные дефолты если секция отсутствует
dashboardBase["id"] = nil
dashboardBase["version"] = 0
return
}
dashboardBase["id"] = nil // всегда nil для overwrite по uid
if uid, ok := meta["uid"].(string); ok && uid != "" {
dashboardBase["uid"] = uid
}
if tags, ok := meta["tags"]; ok {
dashboardBase["tags"] = tags
}
if version, ok := meta["version"]; ok {
dashboardBase["version"] = version
}
}
// createCollapsedRow создаёт row с панелями внутри (collapsed=true)
func createCollapsedRow(
client ClientData,
template *DashboardTemplate,
datasourceType, datasourceUID string,
startPanelID, yPos int,
) map[string]interface{} {
row := deepCopy(template.Panels["row"]).(map[string]interface{})
row["id"] = startPanelID
row["title"] = client.ClientTitle
row["collapsed"] = true
gridPos := row["gridPos"].(map[string]interface{})
gridPos["y"] = yPos
insidePanels := []interface{}{}
panelID := startPanelID + 1
for _, layout := range template.Layout.TenantPanels {
if !evaluateCondition(layout.Condition, client, nil) {
continue
}
domain := tenantDomain(client)
panel := buildPanel(
layout.PanelKey, template, domain, client,
datasourceType, datasourceUID,
panelID, yOffsetOrDefault(layout.YOffset, 1), layout.XOffset, layout.Width,
)
if panel == nil {
continue
}
applyTitle(panel, layout, client.ClientTitle, "", "")
insidePanels = append(insidePanels, panel)
panelID++
}
for _, domain := range client.Domains {
for _, layout := range template.Layout.DomainPanels {
if !evaluateCondition(layout.Condition, client, &domain) {
continue
}
panel := buildPanel(
layout.PanelKey, template, domain, client,
datasourceType, datasourceUID,
panelID, layout.YOffset, layout.XOffset, layout.Width,
)
if panel == nil {
continue
}
applyTitle(panel, layout, client.ClientTitle, domain.DomainName, domain.SID)
panel["description"] = domain.Aliases
insidePanels = append(insidePanels, panel)
panelID++
}
}
row["panels"] = insidePanels
return row
}
// generateLayoutPanels генерирует список панелей по layouts (режим без collapsed rows)
func generateLayoutPanels(
layouts []PanelLayout,
client ClientData,
domain *DomainInfo,
template *DashboardTemplate,
datasourceType, datasourceUID string,
startPanelID, startY int,
) ([]interface{}, int, int) {
panels := []interface{}{}
panelID := startPanelID
maxHeight := 0
effectiveDomain := tenantDomain(client)
if domain != nil {
effectiveDomain = *domain
}
for _, layout := range layouts {
if !evaluateCondition(layout.Condition, client, domain) {
continue
}
panel := buildPanel(
layout.PanelKey, template, effectiveDomain, client,
datasourceType, datasourceUID,
panelID, startY, layout.XOffset, layout.Width,
)
if panel == nil {
continue
}
applyTitle(panel, layout, client.ClientTitle, effectiveDomain.DomainName, effectiveDomain.SID)
if domain != nil {
panel["description"] = domain.Aliases
}
panels = append(panels, panel)
panelID++
gridPos := panel["gridPos"].(map[string]interface{})
if h, ok := gridPos["h"].(float64); ok && int(h) > maxHeight {
maxHeight = int(h)
}
}
return panels, panelID, startY + maxHeight
}
// applyTitle применяет title_format или title_suffix к панели.
// Поддерживаемые плейсхолдеры: {client}, {domain}, {sid}, %s (fmt.Sprintf для clientTitle).
// title_suffix: результат "<domainName> <suffix>" с подстановкой плейсхолдеров.
// title_format: полный контроль над заголовком через плейсхолдеры.
func applyTitle(panel map[string]interface{}, layout PanelLayout, clientTitle, domainName, sid string) {
applyPlaceholders := func(s string) string {
s = strings.ReplaceAll(s, "{client}", clientTitle)
s = strings.ReplaceAll(s, "{domain}", domainName)
s = strings.ReplaceAll(s, "{sid}", sid)
if strings.Contains(s, "%s") {
s = fmt.Sprintf(s, clientTitle)
}
return s
}
if layout.TitleFormat != "" {
panel["title"] = applyPlaceholders(layout.TitleFormat)
} else if layout.TitleSuffix != "" && domainName != "" {
panel["title"] = fmt.Sprintf("%s %s", domainName, applyPlaceholders(layout.TitleSuffix))
}
}
// tenantDomain создаёт псевдо-домен для tenant-level панелей (все SID клиента через OR)
func tenantDomain(client ClientData) DomainInfo {
return DomainInfo{
SID: buildTenantSIDQuery(client.Domains),
DomainName: client.ClientTitle,
Aliases: fmt.Sprintf("%d domains", len(client.Domains)),
}
}
func buildTenantSIDQuery(domains []DomainInfo) string {
sids := make([]string, 0, len(domains))
for _, d := range domains {
sids = append(sids, d.SID)
}
return strings.Join(sids, " OR ")
}
func createRowPanel(title string, template *DashboardTemplate, id, yPos int) map[string]interface{} {
row := deepCopy(template.Panels["row"]).(map[string]interface{})
row["id"] = id
row["title"] = title
row["collapsed"] = false
row["gridPos"].(map[string]interface{})["y"] = yPos
return row
}
func yOffsetOrDefault(yOffset, defaultVal int) int {
if yOffset > 0 {
return yOffset
}
return defaultVal
}
// Заглушка чтобы компилятор не ругался на неиспользуемый импорт
var _ = log.Printf