Preferences: Add theme preference to match system theme (#61986)

* user essentials mob! 🔱

lastFile:pkg/api/preferences.go

* user essentials mob! 🔱

* user essentials mob! 🔱

lastFile:packages/grafana-data/src/types/config.ts

* user essentials mob! 🔱

lastFile:public/app/core/services/echo/utils.test.ts

* user essentials mob! 🔱

* user essentials mob! 🔱

lastFile:public/views/index-template.html

* user essentials mob! 🔱

* 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 <ashley.harrison@grafana.com>
Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
pull/62466/head
Josh Hunt 2 years ago committed by GitHub
parent be7b90bbd1
commit d51e7ec7ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      conf/defaults.ini
  2. 4
      docs/sources/setup-grafana/configure-grafana/_index.md
  3. 5
      packages/grafana-data/src/types/config.ts
  4. 17
      packages/grafana-runtime/src/config.ts
  5. 3
      pkg/api/dtos/models.go
  6. 2
      pkg/api/dtos/prefs.go
  7. 15
      pkg/api/index.go
  8. 5
      pkg/api/preferences.go
  9. 1
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  10. 6
      public/app/core/services/context_srv.ts
  11. 3
      public/app/core/services/echo/utils.test.ts
  12. 3
      public/locales/de-DE/grafana.json
  13. 3
      public/locales/en-US/grafana.json
  14. 3
      public/locales/es-ES/grafana.json
  15. 3
      public/locales/fr-FR/grafana.json
  16. 5
      public/locales/pseudo-LOCALE/grafana.json
  17. 3
      public/locales/zh-Hans/grafana.json
  18. 23
      public/views/index-template.html

@ -423,7 +423,7 @@ verify_email_enabled = false
login_hint = email or username login_hint = email or username
password_hint = password password_hint = password
# Default UI theme ("dark" or "light") # Default UI theme ("dark" or "light" or "system")
default_theme = dark default_theme = dark
# Default UI language (supported IETF language tag, such as en-US) # Default UI language (supported IETF language tag, such as en-US)

@ -772,7 +772,9 @@ Text used as placeholder text on login page for password input.
### default_theme ### 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 ### default_language

@ -117,7 +117,7 @@ export interface CurrentUserDTO {
login: string; login: string;
email: string; email: string;
name: string; name: string;
lightTheme: boolean; theme: string; // dark | light | system
orgCount: number; orgCount: number;
orgId: number; orgId: number;
orgName: string; orgName: string;
@ -129,6 +129,9 @@ export interface CurrentUserDTO {
locale: string; locale: string;
language: string; language: string;
permissions?: Record<string, boolean>; permissions?: Record<string, boolean>;
/** @deprecated Use theme instead */
lightTheme: boolean;
} }
/** Contains essential user and config info /** Contains essential user and config info

@ -149,6 +149,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
constructor(options: GrafanaBootConfig) { constructor(options: GrafanaBootConfig) {
this.bootData = options.bootData; this.bootData = options.bootData;
this.bootData.user.lightTheme = getThemeMode(options) === 'light';
this.isPublicDashboardView = options.bootData.settings.isPublicDashboardView; this.isPublicDashboardView = options.bootData.settings.isPublicDashboardView;
const defaults = { 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) { 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 mode = config.bootData.user.lightTheme ? 'light' : 'dark';
const themeOptions: NewThemeOptions = { const themeOptions: NewThemeOptions = {
colors: { mode }, colors: { mode },
}; };

@ -33,7 +33,8 @@ type CurrentUser struct {
Login string `json:"login"` Login string `json:"login"`
Email string `json:"email"` Email string `json:"email"`
Name string `json:"name"` Name string `json:"name"`
LightTheme bool `json:"lightTheme"` Theme string `json:"theme"`
LightTheme bool `json:"lightTheme"` // deprecated, use theme instead
OrgCount int `json:"orgCount"` OrgCount int `json:"orgCount"`
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"` OrgName string `json:"orgName"`

@ -6,7 +6,7 @@ import (
// swagger:model // swagger:model
type UpdatePrefsCmd struct { type UpdatePrefsCmd struct {
// Enum: light,dark // Enum: light,dark,system
Theme string `json:"theme"` Theme string `json:"theme"`
// The numerical :id of a favorited dashboard // The numerical :id of a favorited dashboard
// Default:0 // Default:0

@ -17,8 +17,9 @@ import (
const ( const (
// Themes // Themes
lightName = "light" lightName = "light"
darkName = "dark" darkName = "dark"
systemName = "system"
) )
func (hs *HTTPServer) editorInAnyFolder(c *contextmodel.ReqContext) bool { func (hs *HTTPServer) editorInAnyFolder(c *contextmodel.ReqContext) bool {
@ -100,6 +101,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
OrgRole: c.OrgRole, OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email), GravatarUrl: dtos.GetGravatarUrl(c.Email),
IsGrafanaAdmin: c.IsGrafanaAdmin, IsGrafanaAdmin: c.IsGrafanaAdmin,
Theme: prefs.Theme,
LightTheme: prefs.Theme == lightName, LightTheme: prefs.Theme == lightName,
Timezone: prefs.Timezone, Timezone: prefs.Timezone,
WeekStart: weekStart, WeekStart: weekStart,
@ -150,12 +152,9 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
} }
themeURLParam := c.Query("theme") themeURLParam := c.Query("theme")
if themeURLParam == lightName { if themeURLParam == lightName || themeURLParam == darkName || themeURLParam == systemName {
data.User.LightTheme = true data.User.Theme = themeURLParam
data.Theme = lightName data.Theme = themeURLParam
} else if themeURLParam == darkName {
data.User.LightTheme = false
data.Theme = darkName
} }
hs.HooksService.RunIndexDataHooks(&data, c) hs.HooksService.RunIndexDataHooks(&data, c)

@ -17,6 +17,7 @@ const (
defaultTheme string = "" defaultTheme string = ""
darkTheme string = "dark" darkTheme string = "dark"
lightTheme string = "light" lightTheme string = "light"
systemTheme string = "system"
) )
// POST /api/preferences/set-home-dash // 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 { 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) 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 { 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) return response.Error(400, "Invalid theme", nil)
} }

@ -71,6 +71,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
{ value: '', label: t('shared-preferences.theme.default-label', 'Default') }, { value: '', label: t('shared-preferences.theme.default-label', 'Default') },
{ value: 'dark', label: t('shared-preferences.theme.dark-label', 'Dark') }, { value: 'dark', label: t('shared-preferences.theme.dark-label', 'Dark') },
{ value: 'light', label: t('shared-preferences.theme.light-label', 'Light') }, { value: 'light', label: t('shared-preferences.theme.light-label', 'Light') },
{ value: 'system', label: t('shared-preferences.theme.system-label', 'System') },
]; ];
} }

@ -7,14 +7,14 @@ import { CurrentUserInternal } from 'app/types/config';
import config from '../../core/config'; import config from '../../core/config';
export class User implements CurrentUserInternal { export class User implements Omit<CurrentUserInternal, 'lightTheme'> {
isSignedIn: boolean; isSignedIn: boolean;
id: number; id: number;
login: string; login: string;
email: string; email: string;
name: string; name: string;
externalUserId: string; externalUserId: string;
lightTheme: boolean; theme: string;
orgCount: number; orgCount: number;
orgId: number; orgId: number;
orgName: string; orgName: string;
@ -43,7 +43,7 @@ export class User implements CurrentUserInternal {
this.timezone = ''; this.timezone = '';
this.fiscalYearStartMonth = 0; this.fiscalYearStartMonth = 0;
this.helpFlags1 = 0; this.helpFlags1 = 0;
this.lightTheme = false; this.theme = 'dark';
this.hasEditPermissionInFolders = false; this.hasEditPermissionInFolders = false;
this.email = ''; this.email = '';
this.name = ''; this.name = '';

@ -8,7 +8,8 @@ const baseUser: CurrentUserDTO = {
login: 'myUsername', login: 'myUsername',
email: 'email@example.com', email: 'email@example.com',
name: 'My Name', name: 'My Name',
lightTheme: false, theme: 'dark',
lightTheme: false, // deprecated
orgCount: 1, orgCount: 1,
orgId: 1, orgId: 1,
orgName: 'Main Org.', orgName: 'Main Org.',

@ -489,7 +489,8 @@
"theme": { "theme": {
"dark-label": "Dunkel", "dark-label": "Dunkel",
"default-label": "Standard", "default-label": "Standard",
"light-label": "Hell" "light-label": "Hell",
"system-label": ""
}, },
"title": "Einstellungen" "title": "Einstellungen"
}, },

@ -489,7 +489,8 @@
"theme": { "theme": {
"dark-label": "Dark", "dark-label": "Dark",
"default-label": "Default", "default-label": "Default",
"light-label": "Light" "light-label": "Light",
"system-label": "System"
}, },
"title": "Preferences" "title": "Preferences"
}, },

@ -489,7 +489,8 @@
"theme": { "theme": {
"dark-label": "Oscuro", "dark-label": "Oscuro",
"default-label": "Por defecto", "default-label": "Por defecto",
"light-label": "Claro" "light-label": "Claro",
"system-label": ""
}, },
"title": "Preferencias" "title": "Preferencias"
}, },

@ -489,7 +489,8 @@
"theme": { "theme": {
"dark-label": "Sombre", "dark-label": "Sombre",
"default-label": "Par défaut", "default-label": "Par défaut",
"light-label": "Clair" "light-label": "Clair",
"system-label": ""
}, },
"title": "Préférences" "title": "Préférences"
}, },

@ -489,7 +489,8 @@
"theme": { "theme": {
"dark-label": "Đäřĸ", "dark-label": "Đäřĸ",
"default-label": "Đęƒäūľŧ", "default-label": "Đęƒäūľŧ",
"light-label": "Ŀįģĥŧ" "light-label": "Ŀįģĥŧ",
"system-label": "Ŝyşŧęm"
}, },
"title": "Přęƒęřęʼnčęş" "title": "Přęƒęřęʼnčęş"
}, },
@ -576,4 +577,4 @@
"option-tooltip": "Cľęäř şęľęčŧįőʼnş" "option-tooltip": "Cľęäř şęľęčŧįőʼnş"
} }
} }
} }

@ -489,7 +489,8 @@
"theme": { "theme": {
"dark-label": "深色", "dark-label": "深色",
"default-label": "默认", "default-label": "默认",
"light-label": "浅色" "light-label": "浅色",
"system-label": ""
}, },
"title": "首选项" "title": "首选项"
}, },

@ -14,9 +14,10 @@
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" /> <link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
<link rel="mask-icon" href="[[.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" /> <link rel="mask-icon" href="[[.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
<!-- If theme is "system", we inject the stylesheets with javascript further down the page -->
[[ if eq .Theme "light" ]] [[ if eq .Theme "light" ]]
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.light %>" /> <link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.light %>" />
[[ else ]] [[ else if eq .Theme "dark" ]]
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.dark %>" /> <link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.dark %>" />
[[ end ]] [[ 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() { window.__grafana_load_failed = function() {
var preloader = document.getElementsByClassName("preloader"); var preloader = document.getElementsByClassName("preloader");
if (preloader.length) { if (preloader.length) {

Loading…
Cancel
Save