mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
be7b90bbd1
commit
d51e7ec7ef
@ -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…
Reference in New Issue
Block a user