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>
This commit is contained in:
Josh Hunt 2023-01-30 09:51:51 +00:00 committed by GitHub
parent be7b90bbd1
commit d51e7ec7ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 79 additions and 27 deletions

View File

@ -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)

View File

@ -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

View File

@ -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<string, boolean>;
/** @deprecated Use theme instead */
lightTheme: boolean;
}
/** Contains essential user and config info

View File

@ -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 },
};

View File

@ -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"`

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -71,6 +71,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
{ 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') },
];
}

View File

@ -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<CurrentUserInternal, 'lightTheme'> {
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 = '';

View File

@ -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.',

View File

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

View File

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

View File

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

View File

@ -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"
},

View File

@ -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ş"
}
}
}
}

View File

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

View File

@ -14,9 +14,10 @@
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
<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" ]]
<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 %>" />
[[ 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) {