feat(ui): Add customizable OIDC login screen branding (#1583)

* feat(ui): Add customizable OIDC login screen branding

As per https://github.com/TwiN/gatus/discussions/1579, this PR
allows for some customizations in the OIDC login screen:

- If a logo is set it will be displayed alongside the Gatus one
- New ui config `ui.login-subtitle`: customises the message in
  the screen
- If set, use `ui.header` will be used to customise the title

I haven't commited the web static assets. I believe that's
triggered with a comment? let me know if I should be doing it.

Also please clarify if I've forgotten about something here,
this is my first pr.

* chore(ui): Regenerate static assets

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
Héctor Luaces
2026-04-01 20:56:47 -04:00
committed by GitHub
co-authored by github-actions[bot] TwiN
parent f925cb4924
commit 12be9facda
8 changed files with 38 additions and 13 deletions
+6
View File
@@ -23,6 +23,7 @@ const (
defaultCustomCSS = ""
defaultSortBy = "name"
defaultFilterBy = "none"
defaultLoginSubtitle = "System Monitoring Dashboard"
)
var (
@@ -48,6 +49,7 @@ type Config struct {
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')
LoginSubtitle string `yaml:"login-subtitle,omitempty"` // LoginSubtitle is the subtitle displayed on the OIDC login page
//////////////////////////////////////////////
// Non-configurable - used for UI rendering //
//////////////////////////////////////////////
@@ -95,6 +97,7 @@ func GetDefaultConfig() *Config {
DarkMode: &defaultDarkMode,
DefaultSortBy: defaultSortBy,
DefaultFilterBy: defaultFilterBy,
LoginSubtitle: defaultLoginSubtitle,
MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults,
Favicon: Favicon{
Default: defaultFavicon,
@@ -143,6 +146,9 @@ func (cfg *Config) ValidateAndSetDefaults() error {
} else if cfg.DefaultFilterBy != "none" && cfg.DefaultFilterBy != "failing" && cfg.DefaultFilterBy != "unstable" {
return ErrInvalidDefaultFilterBy
}
if len(cfg.LoginSubtitle) == 0 {
cfg.LoginSubtitle = defaultLoginSubtitle
}
if len(cfg.Favicon.Default) == 0 {
cfg.Favicon.Default = defaultFavicon
}
+13
View File
@@ -50,6 +50,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
if cfg.Favicon.Size32x32 != defaultFavicon32 {
t.Errorf("expected favicon to be %s, got %s", defaultFavicon32, cfg.Favicon.Size32x32)
}
if cfg.LoginSubtitle != defaultLoginSubtitle {
t.Errorf("expected LoginSubtitle to be %s, got %s", defaultLoginSubtitle, cfg.LoginSubtitle)
}
})
t.Run("custom-values", func(t *testing.T) {
cfg := &Config{
@@ -62,6 +65,7 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
Link: "https://example.com",
DefaultSortBy: "health",
DefaultFilterBy: "failing",
LoginSubtitle: "Welcome",
}
if err := cfg.ValidateAndSetDefaults(); err != nil {
t.Error("expected no error, got", err.Error())
@@ -93,6 +97,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
if cfg.DefaultFilterBy != "failing" {
t.Errorf("expected defaultFilterBy to be preserved, got %s", cfg.DefaultFilterBy)
}
if cfg.LoginSubtitle != "Welcome" {
t.Errorf("expected LoginSubtitle to be preserved, got %s", cfg.LoginSubtitle)
}
})
t.Run("partial-custom-values", func(t *testing.T) {
cfg := &Config{
@@ -119,6 +126,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
if cfg.Description != defaultDescription {
t.Errorf("expected description to use default, got %s", cfg.Description)
}
if cfg.LoginSubtitle != defaultLoginSubtitle {
t.Errorf("expected LoginSubtitle to use default, got %s", cfg.LoginSubtitle)
}
})
}
@@ -181,6 +191,9 @@ func TestGetDefaultConfig(t *testing.T) {
if defaultConfig.DefaultFilterBy != defaultFilterBy {
t.Error("expected GetDefaultConfig() to return defaultFilterBy, got", defaultConfig.DefaultFilterBy)
}
if defaultConfig.LoginSubtitle != defaultLoginSubtitle {
t.Error("expected GetDefaultConfig() to return defaultLoginSubtitle, got", defaultConfig.LoginSubtitle)
}
}
func TestConfig_ValidateAndSetDefaults_DefaultSortBy(t *testing.T) {