diff --git a/cmd/karma/views.go b/cmd/karma/views.go index 3dfa984ed..14bad38df 100644 --- a/cmd/karma/views.go +++ b/cmd/karma/views.go @@ -79,10 +79,17 @@ func index(c *gin.Context) { } filtersB64 := base64.StdEncoding.EncodeToString(filtersJSON) + defaults, err := json.Marshal(config.Config.UI) + if err != nil { + panic(err) + } + defaultsB64 := base64.StdEncoding.EncodeToString(defaults) + c.HTML(http.StatusOK, "ui/build/index.html", gin.H{ "Version": version, "SentryDSN": config.Config.Sentry.Public, "DefaultFilter": filtersB64, + "Defaults": defaultsB64, }) log.Infof("[%s] %s %s took %s", c.ClientIP(), c.Request.Method, c.Request.RequestURI, time.Since(start)) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 562e0b98c..affa87f40 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -615,7 +615,7 @@ sentry: ## Silence form -`silenceForm` section allow customizing silence form behavior. +`silenceForm` section allows customizing silence form behavior. `author:populate_from_header` subsection allows to configure fetching of author name used on the silence form from the request header. It can be used with setups where karma is deployed behind authentication proxy that adds some extra @@ -660,6 +660,52 @@ silenceForm: - job ``` +## UI defaults + +`ui` section allows configuring default values for UI settings controled via the +configuration modal. Those defaults can be overwritten by use via UI controls. + +Syntax: + +```YAML +ui: + refresh: duration + hideFiltersWhenIdle: bool + colorTitlebar: bool + minimalGroupWidth: integer + alertsPerGroup: integer + collapseGroups: string +``` + +- `refresh` - default refresh interval, this tells the UI how often karma API + should be queried +- `hideFiltersWhenIdle` - if enabled filter bar will be hidden after some + user inactivity +- `colorTitlebar` - if enabled alert group title bar color will be set to follow + alerts in that group +- `minimalGroupWidth` - minimal width (in pixels) for each alert group rendered + on the grid. This value is used to calculate the number of columns rendered on + the grid. +- `alertsPerGroup` - default number of alerts to show for each group +- `collapseGroups` - controls if alert groups will default to being rendered + expanded or collapsed (only title bar is visible). Valid values: + - expanded - groups are always expanded + - collapsed - groups are always collapsed + - collapsedOnMobile - groups are expanded on desktop and collapsed on mobile + browsers + +Defaults: + +```YAML +ui: + refresh: 30s + hideFiltersWhenIdle: true + colorTitlebar: false + minimalGroupWidth: 420 + alertsPerGroup: 5 + collapseGroups: collapsedOnMobile +``` + ## Customizing karma In order to keep the core code simple karma doesn't support any way of extending diff --git a/docs/example.yaml b/docs/example.yaml index 7fc9938cc..69b754d7c 100644 --- a/docs/example.yaml +++ b/docs/example.yaml @@ -57,3 +57,10 @@ silenceForm: strip: labels: - job +ui: + refresh: 30s + hideFiltersWhenIdle: true + colorTitlebar: false + minimalGroupWidth: 420 + alertsPerGroup: 5 + collapseGroups: collapsedOnMobile diff --git a/internal/config/config.go b/internal/config/config.go index a6a8dbe79..787283c21 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -90,6 +90,13 @@ func init() { pflag.String("sentry.public", "", "Sentry DSN for Go exceptions") pflag.String("sentry.private", "", "Sentry DSN for JavaScript exceptions") + + pflag.Duration("ui.refresh", time.Second*30, "UI refresh interval") + pflag.Bool("ui.hideFiltersWhenIdle", true, "Hide the filters bar when idle") + pflag.Bool("ui.colorTitlebar", false, "Color alert group titlebar based on alert state") + pflag.Int("ui.minimalGroupWidth", 420, "Minimal width for each alert group on the grid") + pflag.Int("ui.alertsPerGroup", 5, "Default number of alerts to show for each alert group") + pflag.String("ui.collapseGroups", "collapsedOnMobile", "Default state for alert groups") } // ReadConfig will read all sources of configuration, merge all keys and @@ -173,6 +180,12 @@ func (config *configSchema) Read() { config.SilenceForm.Strip.Labels = v.GetStringSlice("silenceform.strip.labels") config.SilenceForm.Author.PopulateFromHeader.Header = v.GetString("silenceform.author.populate_from_header.header") config.SilenceForm.Author.PopulateFromHeader.ValueRegex = v.GetString("silenceform.author.populate_from_header.value_re") + config.UI.Refresh = v.GetDuration("ui.refresh") + config.UI.HideFiltersWhenIdle = v.GetBool("ui.hideFiltersWhenIdle") + config.UI.ColorTitlebar = v.GetBool("ui.colorTitlebar") + config.UI.MinimalGroupWidth = v.GetInt("ui.minimalGroupWidth") + config.UI.AlertsPerGroup = v.GetInt("ui.alertsPerGroup") + config.UI.CollapseGroups = v.GetString("ui.collapseGroups") if config.SilenceForm.Author.PopulateFromHeader.ValueRegex != "" { _, err = regexp.Compile(config.SilenceForm.Author.PopulateFromHeader.ValueRegex) @@ -223,6 +236,10 @@ func (config *configSchema) Read() { log.Fatalf("Invalid grid.sorting.order value '%s', allowed options: disabled, startsAt, label", config.Grid.Sorting.Order) } + if !slices.StringInSlice([]string{"expanded", "collapsed", "collapsedOnMobile"}, config.UI.CollapseGroups) { + log.Fatalf("Invalid ui.collapseGroups value '%s', allowed options: expanded, collapsed, collapsedOnMobile", config.UI.CollapseGroups) + } + // FIXME workaround for https://github.com/prymitive/karma/issues/507 // until https://github.com/spf13/viper/pull/635 is merged // read in raw config file if it's used and override maps where keys are label diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b7d2bfdde..f245713e4 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -129,6 +129,13 @@ silenceForm: value_re: "" strip: labels: [] +ui: + refresh: 30s + hideFiltersWhenIdle: true + colorTitlebar: false + minimalGroupWidth: 420 + alertsPerGroup: 5 + collapseGroups: collapsedOnMobile ` configDump, err := yaml.Marshal(Config) @@ -259,3 +266,35 @@ func TestInvalidSilenceFormRegex(t *testing.T) { t.Error("Invalid silence form regex didn't cause log.Fatal()") } } + +func TestInvalidGridSortingOrder(t *testing.T) { + resetEnv() + os.Setenv("GRID_SORTING_ORDER", "foo") + + log.SetLevel(log.PanicLevel) + defer func() { log.StandardLogger().ExitFunc = nil }() + var wasFatal bool + log.StandardLogger().ExitFunc = func(int) { wasFatal = true } + + Config.Read() + + if !wasFatal { + t.Error("Invalid grid.sorting.order value didn't cause log.Fatal()") + } +} + +func TestInvalidUICollapseGroups(t *testing.T) { + resetEnv() + os.Setenv("UI_COLLAPSEGROUPS", "foo") + + log.SetLevel(log.PanicLevel) + defer func() { log.StandardLogger().ExitFunc = nil }() + var wasFatal bool + log.StandardLogger().ExitFunc = func(int) { wasFatal = true } + + Config.Read() + + if !wasFatal { + t.Error("Invalid ui.collapseGroups value didn't cause log.Fatal()") + } +} diff --git a/internal/config/models.go b/internal/config/models.go index 23c3200a9..0c92cff92 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -105,4 +105,12 @@ type configSchema struct { Labels []string } } `yaml:"silenceForm" mapstructure:"silenceForm"` + UI struct { + Refresh time.Duration + HideFiltersWhenIdle bool `yaml:"hideFiltersWhenIdle" mapstructure:"hideFiltersWhenIdle"` + ColorTitlebar bool `yaml:"colorTitlebar" mapstructure:"colorTitlebar"` + MinimalGroupWidth int `yaml:"minimalGroupWidth" mapstructure:"minimalGroupWidth"` + AlertsPerGroup int `yaml:"alertsPerGroup" mapstructure:"alertsPerGroup"` + CollapseGroups string `yaml:"collapseGroups" mapstructure:"collapseGroups"` + } }