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: результат " " с подстановкой плейсхолдеров. // 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