From d51e7ec7efbffa89a27506822ac421c44636ced2 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Mon, 30 Jan 2023 09:51:51 +0000 Subject: [PATCH] Preferences: Add theme preference to match system theme (#61986) * user essentials mob! :trident: lastFile:pkg/api/preferences.go * user essentials mob! :trident: * user essentials mob! :trident: lastFile:packages/grafana-data/src/types/config.ts * user essentials mob! :trident: lastFile:public/app/core/services/echo/utils.test.ts * user essentials mob! :trident: * user essentials mob! :trident: lastFile:public/views/index-template.html * user essentials mob! :trident: * Restore currentUser.lightTheme for backwards compat * fix types * Apply suggestions from code review Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * cleanup * cleanup --------- Co-authored-by: Ashley Harrison Co-authored-by: Joao Silva Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --- conf/defaults.ini | 2 +- .../setup-grafana/configure-grafana/_index.md | 4 +++- packages/grafana-data/src/types/config.ts | 5 +++- packages/grafana-runtime/src/config.ts | 17 ++++++++++++++ pkg/api/dtos/models.go | 3 ++- pkg/api/dtos/prefs.go | 2 +- pkg/api/index.go | 15 ++++++------ pkg/api/preferences.go | 5 ++-- .../SharedPreferences/SharedPreferences.tsx | 1 + public/app/core/services/context_srv.ts | 6 ++--- public/app/core/services/echo/utils.test.ts | 3 ++- public/locales/de-DE/grafana.json | 3 ++- public/locales/en-US/grafana.json | 3 ++- public/locales/es-ES/grafana.json | 3 ++- public/locales/fr-FR/grafana.json | 3 ++- public/locales/pseudo-LOCALE/grafana.json | 5 ++-- public/locales/zh-Hans/grafana.json | 3 ++- public/views/index-template.html | 23 ++++++++++++++++++- 18 files changed, 79 insertions(+), 27 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index d67385d1db5..d4028ef65ca 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -423,7 +423,7 @@ verify_email_enabled = false login_hint = email or username password_hint = password -# Default UI theme ("dark" or "light") +# Default UI theme ("dark" or "light" or "system") default_theme = dark # Default UI language (supported IETF language tag, such as en-US) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 8e3415c9af5..dc04190ddd1 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -772,7 +772,9 @@ Text used as placeholder text on login page for password input. ### default_theme -Set the default UI theme: `dark` or `light`. Default is `dark`. +Sets the default UI theme: `dark`, `light`, or `system`. The default theme is `dark`. + +`system` matches the user's system theme. ### default_language diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 804fa0b2601..155c1acc198 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -117,7 +117,7 @@ export interface CurrentUserDTO { login: string; email: string; name: string; - lightTheme: boolean; + theme: string; // dark | light | system orgCount: number; orgId: number; orgName: string; @@ -129,6 +129,9 @@ export interface CurrentUserDTO { locale: string; language: string; permissions?: Record; + + /** @deprecated Use theme instead */ + lightTheme: boolean; } /** Contains essential user and config info diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index c6614026f04..c53b35d0fbb 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -149,6 +149,7 @@ export class GrafanaBootConfig implements GrafanaConfig { constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; + this.bootData.user.lightTheme = getThemeMode(options) === 'light'; this.isPublicDashboardView = options.bootData.settings.isPublicDashboardView; const defaults = { @@ -189,8 +190,24 @@ export class GrafanaBootConfig implements GrafanaConfig { } } +function getThemeMode(config: GrafanaBootConfig) { + let mode: 'light' | 'dark' = 'dark'; + const themePref = config.bootData.user.theme; + + if (themePref === 'light' || themePref === 'dark') { + mode = themePref; + } else if (themePref === 'system') { + const mediaResult = window.matchMedia('(prefers-color-scheme: dark)'); + mode = mediaResult.matches ? 'dark' : 'light'; + } + + return mode; +} + function getThemeCustomizations(config: GrafanaBootConfig) { + // if/when we remove CurrentUserDTO.lightTheme, change this to use getThemeMode instead const mode = config.bootData.user.lightTheme ? 'light' : 'dark'; + const themeOptions: NewThemeOptions = { colors: { mode }, }; diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 2b65fa0e4b5..9394ca0c877 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -33,7 +33,8 @@ type CurrentUser struct { Login string `json:"login"` Email string `json:"email"` Name string `json:"name"` - LightTheme bool `json:"lightTheme"` + Theme string `json:"theme"` + LightTheme bool `json:"lightTheme"` // deprecated, use theme instead OrgCount int `json:"orgCount"` OrgId int64 `json:"orgId"` OrgName string `json:"orgName"` diff --git a/pkg/api/dtos/prefs.go b/pkg/api/dtos/prefs.go index b92ae22f221..c8f80e40cc8 100644 --- a/pkg/api/dtos/prefs.go +++ b/pkg/api/dtos/prefs.go @@ -6,7 +6,7 @@ import ( // swagger:model type UpdatePrefsCmd struct { - // Enum: light,dark + // Enum: light,dark,system Theme string `json:"theme"` // The numerical :id of a favorited dashboard // Default:0 diff --git a/pkg/api/index.go b/pkg/api/index.go index a8b5888c2b6..2aee274b792 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -17,8 +17,9 @@ import ( const ( // Themes - lightName = "light" - darkName = "dark" + lightName = "light" + darkName = "dark" + systemName = "system" ) func (hs *HTTPServer) editorInAnyFolder(c *contextmodel.ReqContext) bool { @@ -100,6 +101,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, + Theme: prefs.Theme, LightTheme: prefs.Theme == lightName, Timezone: prefs.Timezone, WeekStart: weekStart, @@ -150,12 +152,9 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV } themeURLParam := c.Query("theme") - if themeURLParam == lightName { - data.User.LightTheme = true - data.Theme = lightName - } else if themeURLParam == darkName { - data.User.LightTheme = false - data.Theme = darkName + if themeURLParam == lightName || themeURLParam == darkName || themeURLParam == systemName { + data.User.Theme = themeURLParam + data.Theme = themeURLParam } hs.HooksService.RunIndexDataHooks(&data, c) diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index f008270c444..9ec93d64631 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -17,6 +17,7 @@ const ( defaultTheme string = "" darkTheme string = "dark" lightTheme string = "light" + systemTheme string = "system" ) // POST /api/preferences/set-home-dash @@ -134,7 +135,7 @@ func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response } func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) response.Response { - if dtoCmd.Theme != lightTheme && dtoCmd.Theme != darkTheme && dtoCmd.Theme != defaultTheme { + if dtoCmd.Theme != lightTheme && dtoCmd.Theme != darkTheme && dtoCmd.Theme != defaultTheme && dtoCmd.Theme != systemTheme { return response.Error(400, "Invalid theme", nil) } @@ -191,7 +192,7 @@ func (hs *HTTPServer) PatchUserPreferences(c *contextmodel.ReqContext) response. } func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.PatchPrefsCmd) response.Response { - if dtoCmd.Theme != nil && *dtoCmd.Theme != lightTheme && *dtoCmd.Theme != darkTheme && *dtoCmd.Theme != defaultTheme { + if dtoCmd.Theme != nil && *dtoCmd.Theme != lightTheme && *dtoCmd.Theme != darkTheme && *dtoCmd.Theme != defaultTheme && *dtoCmd.Theme != systemTheme { return response.Error(400, "Invalid theme", nil) } diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 73b7ee09290..6b4d6904ac7 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -71,6 +71,7 @@ export class SharedPreferences extends PureComponent { { value: '', label: t('shared-preferences.theme.default-label', 'Default') }, { value: 'dark', label: t('shared-preferences.theme.dark-label', 'Dark') }, { value: 'light', label: t('shared-preferences.theme.light-label', 'Light') }, + { value: 'system', label: t('shared-preferences.theme.system-label', 'System') }, ]; } diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 97d2564a3e6..4a7a5fddbe4 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -7,14 +7,14 @@ import { CurrentUserInternal } from 'app/types/config'; import config from '../../core/config'; -export class User implements CurrentUserInternal { +export class User implements Omit { isSignedIn: boolean; id: number; login: string; email: string; name: string; externalUserId: string; - lightTheme: boolean; + theme: string; orgCount: number; orgId: number; orgName: string; @@ -43,7 +43,7 @@ export class User implements CurrentUserInternal { this.timezone = ''; this.fiscalYearStartMonth = 0; this.helpFlags1 = 0; - this.lightTheme = false; + this.theme = 'dark'; this.hasEditPermissionInFolders = false; this.email = ''; this.name = ''; diff --git a/public/app/core/services/echo/utils.test.ts b/public/app/core/services/echo/utils.test.ts index 353f32ec936..5d8e1c26491 100644 --- a/public/app/core/services/echo/utils.test.ts +++ b/public/app/core/services/echo/utils.test.ts @@ -8,7 +8,8 @@ const baseUser: CurrentUserDTO = { login: 'myUsername', email: 'email@example.com', name: 'My Name', - lightTheme: false, + theme: 'dark', + lightTheme: false, // deprecated orgCount: 1, orgId: 1, orgName: 'Main Org.', diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index 03a1590e4e1..c0625a4f533 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -489,7 +489,8 @@ "theme": { "dark-label": "Dunkel", "default-label": "Standard", - "light-label": "Hell" + "light-label": "Hell", + "system-label": "" }, "title": "Einstellungen" }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index ce504e80bb7..255415225ee 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -489,7 +489,8 @@ "theme": { "dark-label": "Dark", "default-label": "Default", - "light-label": "Light" + "light-label": "Light", + "system-label": "System" }, "title": "Preferences" }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index 67b51a2fc28..ed9234f94f3 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -489,7 +489,8 @@ "theme": { "dark-label": "Oscuro", "default-label": "Por defecto", - "light-label": "Claro" + "light-label": "Claro", + "system-label": "" }, "title": "Preferencias" }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 579ecc1b245..baf901bae6f 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -489,7 +489,8 @@ "theme": { "dark-label": "Sombre", "default-label": "Par défaut", - "light-label": "Clair" + "light-label": "Clair", + "system-label": "" }, "title": "Préférences" }, diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 57932f6f053..ffbd5154681 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -489,7 +489,8 @@ "theme": { "dark-label": "Đäřĸ", "default-label": "Đęƒäūľŧ", - "light-label": "Ŀįģĥŧ" + "light-label": "Ŀįģĥŧ", + "system-label": "Ŝyşŧęm" }, "title": "Přęƒęřęʼnčęş" }, @@ -576,4 +577,4 @@ "option-tooltip": "Cľęäř şęľęčŧįőʼnş" } } -} +} \ No newline at end of file diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 442a0ebc52d..5b37684294f 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -489,7 +489,8 @@ "theme": { "dark-label": "深色", "default-label": "默认", - "light-label": "浅色" + "light-label": "浅色", + "system-label": "" }, "title": "首选项" }, diff --git a/public/views/index-template.html b/public/views/index-template.html index 8e3459d61dd..79798877e97 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -14,9 +14,10 @@ + [[ if eq .Theme "light" ]] - [[ else ]] + [[ else if eq .Theme "dark" ]] [[ end ]] @@ -251,6 +252,26 @@ } }; + // Set theme to match system only on startup. + // Do not react to changes in system theme after startup. + if (window.grafanaBootData.user.theme === "system") { + document.body.classList.remove("theme-system"); + var darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); + var cssLink = document.createElement("link"); + cssLink.rel = 'stylesheet'; + + if (darkQuery.matches) { + document.body.classList.add("theme-dark"); + cssLink.href = window.grafanaBootData.themePaths.dark; + window.grafanaBootData.user.lightTheme = false; + } else { + document.body.classList.add("theme-light"); + cssLink.href = window.grafanaBootData.themePaths.light; + window.grafanaBootData.user.lightTheme = true; + } + document.head.appendChild(cssLink); + } + window.__grafana_load_failed = function() { var preloader = document.getElementsByClassName("preloader"); if (preloader.length) {