fix(api): fix JSON settings marshalling

Fixes #6696.
This commit is contained in:
Lukasz Mierzwa
2026-04-08 08:48:36 +01:00
committed by Łukasz Mierzwa
parent 3c1a2c7c31
commit cae5a4c701
3 changed files with 118 additions and 10 deletions

View File

@@ -2,6 +2,10 @@
## v0.129
### Fixed
- Settings from the config file were not being pushed to the UI - #6696.
### Changed
- Switched logging from zerolog to slog. Log output format has changed.

View File

@@ -3,6 +3,7 @@ package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
@@ -3008,3 +3009,106 @@ func TestLabelSettings(t *testing.T) {
})
}
}
func TestIndexUIDefaultsJSON(t *testing.T) {
// Verifies that the index handler embeds a complete, valid JSON object
// in the <script id="defaults"> tag, with all UI config fields present
// and Refresh serialized as a nanosecond integer.
type uiDefaults struct {
Refresh float64 `json:"Refresh"`
HideFiltersWhenIdle bool `json:"HideFiltersWhenIdle"`
ColorTitlebar bool `json:"ColorTitlebar"`
Theme string `json:"Theme"`
Animations bool `json:"Animations"`
MinimalGroupWidth int `json:"MinimalGroupWidth"`
AlertsPerGroup int `json:"AlertsPerGroup"`
CollapseGroups string `json:"CollapseGroups"`
MultiGridLabel string `json:"MultiGridLabel"`
MultiGridSortReverse bool `json:"MultiGridSortReverse"`
}
type testCaseT struct {
// scenario describes the behaviour being tested
name string
refresh time.Duration
expectedRefreshNs float64
alertsPerGroup int
multiGridLabel string
hideFiltersWhenIdle bool
}
testCases := []testCaseT{
{
// default config values produce correct JSON with 30s refresh
name: "default 30s refresh",
refresh: 30 * time.Second,
expectedRefreshNs: 30000000000,
alertsPerGroup: 5,
multiGridLabel: "",
hideFiltersWhenIdle: true,
},
{
// custom config values are all reflected in the embedded JSON
name: "custom config values",
refresh: 45 * time.Second,
expectedRefreshNs: 45000000000,
alertsPerGroup: 15,
multiGridLabel: "cluster",
hideFiltersWhenIdle: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockConfig(t.Setenv)
config.Config.UI.Refresh = tc.refresh
config.Config.UI.AlertsPerGroup = tc.alertsPerGroup
config.Config.UI.MultiGridLabel = tc.multiGridLabel
config.Config.UI.HideFiltersWhenIdle = tc.hideFiltersWhenIdle
r := testRouter()
setupRouter(r, nil)
mockCache()
req := httptest.NewRequest("GET", "/", nil)
resp := httptest.NewRecorder()
r.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("GET / returned status %d", resp.Code)
}
body := resp.Body.String()
re := regexp.MustCompile(`<script type="text/plain" id="defaults">\s*([A-Za-z0-9+/=]+)\s*</script>`)
matches := re.FindStringSubmatch(body)
if len(matches) < 2 {
t.Fatal("Could not find <script id=\"defaults\"> content in HTML response")
}
decoded, err := base64.StdEncoding.DecodeString(matches[1])
if err != nil {
t.Fatalf("Failed to base64-decode defaults: %v", err)
}
var got uiDefaults
if err := json.Unmarshal(decoded, &got); err != nil {
t.Fatalf("Failed to parse defaults JSON: %v\nraw: %s", err, string(decoded))
}
expected := uiDefaults{
Refresh: tc.expectedRefreshNs,
HideFiltersWhenIdle: tc.hideFiltersWhenIdle,
ColorTitlebar: config.Config.UI.ColorTitlebar,
Theme: config.Config.UI.Theme,
Animations: config.Config.UI.Animations,
MinimalGroupWidth: config.Config.UI.MinimalGroupWidth,
AlertsPerGroup: tc.alertsPerGroup,
CollapseGroups: config.Config.UI.CollapseGroups,
MultiGridLabel: tc.multiGridLabel,
MultiGridSortReverse: config.Config.UI.MultiGridSortReverse,
}
if diff := cmp.Diff(expected, got); diff != "" {
t.Errorf("UI defaults mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -218,15 +218,15 @@ type configSchema struct {
} `yaml:"silenceForm" koanf:"silenceForm"`
// nolint: maligned
UI struct {
Refresh time.Duration
HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" koanf:"hideFiltersWhenIdle"`
ColorTitlebar bool `yaml:"colorTitlebar" koanf:"colorTitlebar"`
Theme string `yaml:"theme" koanf:"theme"`
Animations bool `yaml:"animations" koanf:"animations"`
MinimalGroupWidth int `yaml:"minimalGroupWidth" koanf:"minimalGroupWidth"`
AlertsPerGroup int `yaml:"alertsPerGroup" koanf:"alertsPerGroup"`
CollapseGroups string `yaml:"collapseGroups" koanf:"collapseGroups"`
MultiGridLabel string `yaml:"multiGridLabel" koanf:"multiGridLabel"`
MultiGridSortReverse bool `yaml:"multiGridSortReverse" koanf:"multiGridSortReverse"`
Refresh time.Duration `json:",format:nano"`
HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" koanf:"hideFiltersWhenIdle"`
ColorTitlebar bool `yaml:"colorTitlebar" koanf:"colorTitlebar"`
Theme string `yaml:"theme" koanf:"theme"`
Animations bool `yaml:"animations" koanf:"animations"`
MinimalGroupWidth int `yaml:"minimalGroupWidth" koanf:"minimalGroupWidth"`
AlertsPerGroup int `yaml:"alertsPerGroup" koanf:"alertsPerGroup"`
CollapseGroups string `yaml:"collapseGroups" koanf:"collapseGroups"`
MultiGridLabel string `yaml:"multiGridLabel" koanf:"multiGridLabel"`
MultiGridSortReverse bool `yaml:"multiGridSortReverse" koanf:"multiGridSortReverse"`
}
}