mirror of https://github.com/grafana/grafana.git
Themes: Adds support for extraThemes (behind feature toggle) (#67981)
* Foundations to support more themes * Fixes * add another test theme * Refactorings * more refactoring * Update * Fixing tests * Fix * Update
This commit is contained in:
parent
d883404f50
commit
f8cf67347f
|
@ -111,6 +111,7 @@ Alpha features might be changed or removed without prior notice.
|
|||
| `pluginsAPIManifestKey` | Use grafana.com API to retrieve the public manifest key |
|
||||
| `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order, CSV upload and advanced mode |
|
||||
| `opensearchDetectVersion` | Enable version detection in OpenSearch |
|
||||
| `extraThemes` | Enables extra themes |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { createTheme } from './createTheme';
|
||||
export { getThemeById, getBuiltInThemes, type ThemeRegistryItem } from './registry';
|
||||
export type { NewThemeOptions } from './createTheme';
|
||||
export type { ThemeRichColor, GrafanaTheme2 } from './types';
|
||||
export type { ThemeColors } from './createColors';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { Registry, RegistryItem } from '../utils/Registry';
|
||||
|
||||
import { createTheme } from './createTheme';
|
||||
import { GrafanaTheme2 } from './types';
|
||||
|
||||
export interface ThemeRegistryItem extends RegistryItem {
|
||||
isExtra?: boolean;
|
||||
build: () => GrafanaTheme2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Only for internal use, never use this from a plugin
|
||||
**/
|
||||
export function getThemeById(id: string): GrafanaTheme2 {
|
||||
const theme = themeRegistry.getIfExists(id) ?? themeRegistry.get('dark');
|
||||
return theme.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* For internal use only
|
||||
*/
|
||||
export function getBuiltInThemes(includeExtras?: boolean) {
|
||||
return themeRegistry.list().filter((item) => {
|
||||
return includeExtras ? true : !item.isExtra;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* There is also a backend list at services/perferences/themes.go
|
||||
*/
|
||||
const themeRegistry = new Registry<ThemeRegistryItem>(() => {
|
||||
return [
|
||||
{ id: 'system', name: 'System preference', build: getSystemPreferenceTheme },
|
||||
{ id: 'dark', name: 'Dark', build: () => createTheme({ colors: { mode: 'dark' } }) },
|
||||
{ id: 'light', name: 'Light', build: () => createTheme({ colors: { mode: 'light' } }) },
|
||||
{ id: 'blue-night', name: 'Blue night', build: createBlueNight, isExtra: true },
|
||||
{ id: 'midnight', name: 'Midnight', build: createMidnight, isExtra: true },
|
||||
];
|
||||
});
|
||||
|
||||
function getSystemPreferenceTheme() {
|
||||
const mediaResult = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const id = mediaResult.matches ? 'dark' : 'light';
|
||||
return getThemeById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just a temporary placeholder for a possible new theme
|
||||
*/
|
||||
function createMidnight(): GrafanaTheme2 {
|
||||
const whiteBase = '204, 204, 220';
|
||||
|
||||
return createTheme({
|
||||
name: 'Midnight',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
background: {
|
||||
canvas: '#000000',
|
||||
primary: '#000000',
|
||||
secondary: '#181818',
|
||||
},
|
||||
border: {
|
||||
weak: `rgba(${whiteBase}, 0.17)`,
|
||||
medium: `rgba(${whiteBase}, 0.25)`,
|
||||
strong: `rgba(${whiteBase}, 0.35)`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Just a temporary placeholder for a possible new theme
|
||||
*/
|
||||
function createBlueNight(): GrafanaTheme2 {
|
||||
return createTheme({
|
||||
name: 'Blue night',
|
||||
colors: {
|
||||
mode: 'dark',
|
||||
background: {
|
||||
canvas: '#15161d',
|
||||
primary: '#15161d',
|
||||
secondary: '#1d1f2e',
|
||||
},
|
||||
border: {
|
||||
weak: `#2e304f`,
|
||||
medium: `#2e304f`,
|
||||
strong: `#2e304f`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -97,4 +97,5 @@ export interface FeatureToggles {
|
|||
opensearchDetectVersion?: boolean;
|
||||
faroDatasourceSelector?: boolean;
|
||||
enableDatagridEditing?: boolean;
|
||||
extraThemes?: boolean;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
AuthSettings,
|
||||
BootData,
|
||||
BuildInfo,
|
||||
createTheme,
|
||||
DataSourceInstanceSettings,
|
||||
FeatureToggles,
|
||||
GrafanaConfig,
|
||||
|
@ -16,7 +15,7 @@ import {
|
|||
PanelPluginMeta,
|
||||
systemDateFormats,
|
||||
SystemDateFormatSettings,
|
||||
NewThemeOptions,
|
||||
getThemeById,
|
||||
} from '@grafana/data';
|
||||
|
||||
export interface AzureSettings {
|
||||
|
@ -158,7 +157,6 @@ 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 = {
|
||||
|
@ -195,39 +193,15 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
|||
}
|
||||
|
||||
// Creating theme after applying feature toggle overrides in case we need to toggle anything
|
||||
this.theme2 = createTheme(getThemeCustomizations(this));
|
||||
|
||||
this.theme2 = getThemeById(this.bootData.user.theme);
|
||||
this.bootData.user.lightTheme = this.theme2.isLight;
|
||||
this.theme = this.theme2.v1;
|
||||
|
||||
// Special feature toggle that impact theme/component looks
|
||||
this.theme2.flags.topnav = this.featureToggles.topnav;
|
||||
}
|
||||
}
|
||||
|
||||
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 },
|
||||
};
|
||||
|
||||
return themeOptions;
|
||||
}
|
||||
|
||||
function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) {
|
||||
if (window.location.href.indexOf('__feature') === -1) {
|
||||
return;
|
||||
|
|
|
@ -18,7 +18,7 @@ type IndexViewData struct {
|
|||
NavTree *navtree.NavTreeRoot
|
||||
BuildVersion string
|
||||
BuildCommit string
|
||||
Theme string
|
||||
ThemeType string
|
||||
NewGrafanaVersionExists bool
|
||||
NewGrafanaVersion string
|
||||
AppName string
|
||||
|
|
|
@ -16,13 +16,6 @@ import (
|
|||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
// Themes
|
||||
lightName = "light"
|
||||
darkName = "dark"
|
||||
systemName = "system"
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) editorInAnyFolder(c *contextmodel.ReqContext) bool {
|
||||
hasEditPermissionInFoldersQuery := folder.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
|
||||
hasEditPermissionInFoldersQueryResult, err := hs.DashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery)
|
||||
|
@ -91,6 +84,8 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
|||
weekStart = *prefs.WeekStart
|
||||
}
|
||||
|
||||
theme := hs.getThemeForIndexData(prefs.Theme, c.Query("theme"))
|
||||
|
||||
data := dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserID,
|
||||
|
@ -105,8 +100,8 @@ 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,
|
||||
Theme: theme.ID,
|
||||
LightTheme: theme.Type == "light",
|
||||
Timezone: prefs.Timezone,
|
||||
WeekStart: weekStart,
|
||||
Locale: locale,
|
||||
|
@ -119,7 +114,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
|||
},
|
||||
},
|
||||
Settings: settings,
|
||||
Theme: prefs.Theme,
|
||||
ThemeType: theme.Type,
|
||||
AppUrl: appURL,
|
||||
AppSubUrl: appSubURL,
|
||||
GoogleAnalyticsId: settings.GoogleAnalyticsId,
|
||||
|
@ -164,12 +159,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
|||
data.User.Name = data.User.Login
|
||||
}
|
||||
|
||||
themeURLParam := c.Query("theme")
|
||||
if themeURLParam == lightName || themeURLParam == darkName || themeURLParam == systemName {
|
||||
data.User.Theme = themeURLParam
|
||||
data.Theme = themeURLParam
|
||||
}
|
||||
|
||||
hs.HooksService.RunIndexDataHooks(&data, c)
|
||||
|
||||
data.NavTree.ApplyAdminIA()
|
||||
|
@ -201,3 +190,18 @@ func (hs *HTTPServer) NotFoundHandler(c *contextmodel.ReqContext) {
|
|||
|
||||
c.HTML(404, "index", data)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam string) *pref.ThemeDTO {
|
||||
if themeURLParam != "" && pref.IsValidThemeID(themeURLParam) {
|
||||
return pref.GetThemeByID(themeURLParam)
|
||||
}
|
||||
|
||||
if pref.IsValidThemeID(themePrefId) {
|
||||
theme := pref.GetThemeByID(themePrefId)
|
||||
if !theme.IsExtra || hs.Features.IsEnabled(featuremgmt.FlagExtraThemes) {
|
||||
return theme
|
||||
}
|
||||
}
|
||||
|
||||
return pref.GetThemeByID(hs.Cfg.DefaultTheme)
|
||||
}
|
||||
|
|
|
@ -13,13 +13,6 @@ import (
|
|||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTheme string = ""
|
||||
darkTheme string = "dark"
|
||||
lightTheme string = "light"
|
||||
systemTheme string = "system"
|
||||
)
|
||||
|
||||
// POST /api/preferences/set-home-dash
|
||||
func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
cmd := pref.SavePreferenceCommand{}
|
||||
|
@ -135,8 +128,8 @@ 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 && dtoCmd.Theme != systemTheme {
|
||||
return response.Error(400, "Invalid theme", nil)
|
||||
if !pref.IsValidThemeID(dtoCmd.Theme) {
|
||||
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
|
||||
}
|
||||
|
||||
dashboardID := dtoCmd.HomeDashboardID
|
||||
|
@ -193,7 +186,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 && *dtoCmd.Theme != systemTheme {
|
||||
if dtoCmd.Theme != nil && !pref.IsValidThemeID(*dtoCmd.Theme) {
|
||||
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -530,5 +530,12 @@ var (
|
|||
State: FeatureStateBeta,
|
||||
Owner: grafanaBiSquad,
|
||||
},
|
||||
{
|
||||
Name: "extraThemes",
|
||||
Description: "Enables extra themes",
|
||||
FrontendOnly: true,
|
||||
State: FeatureStateAlpha,
|
||||
Owner: grafanaUserEssentialsSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -78,3 +78,4 @@ advancedDataSourcePicker,alpha,@grafana/dashboards-squad,false,false,false,true
|
|||
opensearchDetectVersion,alpha,@grafana/aws-plugins,false,false,false,true
|
||||
faroDatasourceSelector,beta,@grafana/app-o11y,false,false,false,true
|
||||
enableDatagridEditing,beta,@grafana/grafana-bi-squad,false,false,false,true
|
||||
extraThemes,alpha,@grafana/user-essentials,false,false,false,true
|
||||
|
|
|
|
@ -322,4 +322,8 @@ const (
|
|||
// FlagEnableDatagridEditing
|
||||
// Enables the edit functionality in the datagrid panel
|
||||
FlagEnableDatagridEditing = "enableDatagridEditing"
|
||||
|
||||
// FlagExtraThemes
|
||||
// Enables extra themes
|
||||
FlagExtraThemes = "extraThemes"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package pref
|
||||
|
||||
type ThemeDTO struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
IsExtra bool `json:"isExtra"`
|
||||
}
|
||||
|
||||
var themes = []ThemeDTO{
|
||||
{ID: "light", Type: "light"},
|
||||
{ID: "dark", Type: "dark"},
|
||||
{ID: "system", Type: "dark"},
|
||||
{ID: "midnight", Type: "dark", IsExtra: true},
|
||||
{ID: "blue-night", Type: "dark", IsExtra: true},
|
||||
}
|
||||
|
||||
func GetThemeByID(id string) *ThemeDTO {
|
||||
for _, theme := range themes {
|
||||
if theme.ID == id {
|
||||
return &theme
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsValidThemeID(id string) bool {
|
||||
for _, theme := range themes {
|
||||
if theme.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { assertInstanceOf } from 'test/helpers/asserts';
|
||||
import { getSelectParent, selectOptionInTest } from 'test/helpers/selectOptionInTest';
|
||||
|
||||
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
|
||||
|
@ -129,8 +128,8 @@ describe('SharedPreferences', () => {
|
|||
});
|
||||
|
||||
it('renders the theme preference', () => {
|
||||
const lightThemeRadio = assertInstanceOf(screen.getByLabelText('Light'), HTMLInputElement);
|
||||
expect(lightThemeRadio.checked).toBeTruthy();
|
||||
const themeSelect = getSelectParent(screen.getByLabelText('Interface theme'));
|
||||
expect(themeSelect).toHaveTextContent('Light');
|
||||
});
|
||||
|
||||
it('renders the home dashboard preference', async () => {
|
||||
|
@ -156,14 +155,13 @@ describe('SharedPreferences', () => {
|
|||
});
|
||||
|
||||
it("saves the user's new preferences", async () => {
|
||||
const darkThemeRadio = assertInstanceOf(screen.getByLabelText('Dark'), HTMLInputElement);
|
||||
await userEvent.click(darkThemeRadio);
|
||||
|
||||
await selectOptionInTest(screen.getByLabelText('Interface theme'), 'Dark');
|
||||
await selectOptionInTest(screen.getByLabelText('Timezone'), 'Australia/Sydney');
|
||||
await selectOptionInTest(screen.getByLabelText('Week start'), 'Saturday');
|
||||
await selectOptionInTest(screen.getByLabelText(/language/i), 'Français');
|
||||
|
||||
await userEvent.click(screen.getByText('Save'));
|
||||
|
||||
expect(mockPrefsUpdate).toHaveBeenCalledWith({
|
||||
timezone: 'Australia/Sydney',
|
||||
weekStart: 'saturday',
|
||||
|
@ -177,9 +175,7 @@ describe('SharedPreferences', () => {
|
|||
});
|
||||
|
||||
it("saves the user's default preferences", async () => {
|
||||
const defThemeRadio = assertInstanceOf(screen.getByLabelText('Default'), HTMLInputElement);
|
||||
await userEvent.click(defThemeRadio);
|
||||
|
||||
await selectOptionInTest(screen.getByLabelText('Interface theme'), 'Default');
|
||||
await selectOptionInTest(screen.getByLabelText('Home Dashboard'), 'Default');
|
||||
await selectOptionInTest(screen.getByLabelText('Timezone'), 'Default');
|
||||
await selectOptionInTest(screen.getByLabelText('Week start'), 'Default');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { css } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { FeatureState, SelectableValue } from '@grafana/data';
|
||||
import { FeatureState, SelectableValue, getBuiltInThemes, ThemeRegistryItem } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Preferences as UserPreferencesDTO } from '@grafana/schema/src/raw/preferences/x/preferences_types.gen';
|
||||
|
@ -11,7 +11,6 @@ import {
|
|||
FieldSet,
|
||||
Form,
|
||||
Label,
|
||||
RadioButtonGroup,
|
||||
Select,
|
||||
stylesFactory,
|
||||
TimeZonePicker,
|
||||
|
@ -22,6 +21,7 @@ import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
|
|||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { LANGUAGES } from 'app/core/internationalization/constants';
|
||||
import { PreferencesService } from 'app/core/services/PreferencesService';
|
||||
import { changeTheme } from 'app/core/services/theme';
|
||||
|
||||
export interface Props {
|
||||
resourceUri: string;
|
||||
|
@ -67,12 +67,13 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||
queryHistory: { homeTab: '' },
|
||||
};
|
||||
|
||||
this.themeOptions = [
|
||||
{ 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') },
|
||||
];
|
||||
this.themeOptions = getBuiltInThemes(config.featureToggles.extraThemes).map((theme) => ({
|
||||
value: theme.id,
|
||||
label: getTranslatedThemeName(theme),
|
||||
}));
|
||||
|
||||
// Add default option
|
||||
this.themeOptions.unshift({ value: '', label: t('shared-preferences.theme.default-label', 'Default') });
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
|
@ -98,8 +99,12 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
onThemeChanged = (value: string) => {
|
||||
this.setState({ theme: value });
|
||||
onThemeChanged = (value: SelectableValue<string>) => {
|
||||
this.setState({ theme: value.value });
|
||||
|
||||
if (value.value) {
|
||||
changeTheme(value.value, true);
|
||||
}
|
||||
};
|
||||
|
||||
onTimeZoneChanged = (timezone?: string) => {
|
||||
|
@ -131,21 +136,19 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
|||
const { disabled } = this.props;
|
||||
const styles = getStyles();
|
||||
const languages = getLanguageOptions();
|
||||
let currentThemeOption = this.themeOptions[0].value;
|
||||
if (theme?.length) {
|
||||
currentThemeOption = this.themeOptions.find((item) => item.value === theme)?.value;
|
||||
}
|
||||
const currentThemeOption = this.themeOptions.find((x) => x.value === theme) ?? this.themeOptions[0];
|
||||
|
||||
return (
|
||||
<Form onSubmit={this.onSubmitForm}>
|
||||
{() => {
|
||||
return (
|
||||
<FieldSet label={<Trans i18nKey="shared-preferences.title">Preferences</Trans>} disabled={disabled}>
|
||||
<Field label={t('shared-preferences.fields.theme-label', 'UI Theme')}>
|
||||
<RadioButtonGroup
|
||||
<Field label={t('shared-preferences.fields.theme-label', 'Interface theme')}>
|
||||
<Select
|
||||
options={this.themeOptions}
|
||||
value={currentThemeOption}
|
||||
onChange={this.onThemeChanged}
|
||||
inputId="shared-preferences-theme-select"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
|
@ -240,3 +243,16 @@ const getStyles = stylesFactory(() => {
|
|||
`,
|
||||
};
|
||||
});
|
||||
|
||||
function getTranslatedThemeName(theme: ThemeRegistryItem) {
|
||||
switch (theme.id) {
|
||||
case 'dark':
|
||||
return t('shared.preferences.theme.dark-label', 'Dark');
|
||||
case 'light':
|
||||
return t('shared.preferences.theme.light-label', 'Light');
|
||||
case 'system':
|
||||
return t('shared.preferences.theme.system-label', 'System preference');
|
||||
default:
|
||||
return theme.name;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createTheme } from '@grafana/data';
|
||||
import { getThemeById } from '@grafana/data/src/themes/registry';
|
||||
import { ThemeChangedEvent } from '@grafana/runtime';
|
||||
|
||||
import appEvents from '../app_events';
|
||||
|
@ -7,42 +7,42 @@ import { contextSrv } from '../core';
|
|||
|
||||
import { PreferencesService } from './PreferencesService';
|
||||
|
||||
export async function changeTheme(mode: 'dark' | 'light', runtimeOnly?: boolean) {
|
||||
const newTheme = createTheme({
|
||||
colors: {
|
||||
mode: mode,
|
||||
},
|
||||
});
|
||||
export async function changeTheme(themeId: string, runtimeOnly?: boolean) {
|
||||
const oldTheme = config.theme2;
|
||||
|
||||
const newTheme = getThemeById(themeId);
|
||||
|
||||
// Special feature toggle that impact theme/component looks
|
||||
newTheme.flags.topnav = config.featureToggles.topnav;
|
||||
|
||||
appEvents.publish(new ThemeChangedEvent(newTheme));
|
||||
config.theme2.isDark = newTheme.isDark;
|
||||
|
||||
// Add css file for new theme
|
||||
if (oldTheme.colors.mode !== newTheme.colors.mode) {
|
||||
const newCssLink = document.createElement('link');
|
||||
newCssLink.rel = 'stylesheet';
|
||||
newCssLink.href = config.bootData.themePaths[newTheme.colors.mode];
|
||||
newCssLink.onload = () => {
|
||||
// Remove old css file
|
||||
const bodyLinks = document.getElementsByTagName('link');
|
||||
for (let i = 0; i < bodyLinks.length; i++) {
|
||||
const link = bodyLinks[i];
|
||||
|
||||
if (link.href && link.href.includes(`build/grafana.${oldTheme.colors.mode}`)) {
|
||||
// Remove existing link once the new css has loaded to avoid flickering
|
||||
// If we add new css at the same time we remove current one the page will be rendered without css
|
||||
// As the new css file is loading
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.head.insertBefore(newCssLink, document.head.firstChild);
|
||||
}
|
||||
|
||||
if (runtimeOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add css file for new theme
|
||||
const newCssLink = document.createElement('link');
|
||||
newCssLink.rel = 'stylesheet';
|
||||
newCssLink.href = config.bootData.themePaths[newTheme.colors.mode];
|
||||
newCssLink.onload = () => {
|
||||
// Remove old css file
|
||||
const bodyLinks = document.getElementsByTagName('link');
|
||||
for (let i = 0; i < bodyLinks.length; i++) {
|
||||
const link = bodyLinks[i];
|
||||
|
||||
if (link.href && link.href.includes(`build/grafana.${!newTheme.isDark ? 'dark' : 'light'}`)) {
|
||||
// Remove existing link once the new css has loaded to avoid flickering
|
||||
// If we add new css at the same time we remove current one the page will be rendered without css
|
||||
// As the new css file is loading
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.body.appendChild(newCssLink);
|
||||
|
||||
if (!contextSrv.isSignedIn) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { ThemeChangedEvent } from '@grafana/runtime';
|
||||
import { ThemeChangedEvent, config } from '@grafana/runtime';
|
||||
import { ThemeContext } from '@grafana/ui';
|
||||
|
||||
import { appEvents } from '../core';
|
||||
|
@ -11,6 +11,7 @@ export const ThemeProvider = ({ children, value }: { children: React.ReactNode;
|
|||
|
||||
useEffect(() => {
|
||||
const sub = appEvents.subscribe(ThemeChangedEvent, (event) => {
|
||||
config.theme2 = event.payload;
|
||||
setTheme(event.payload);
|
||||
});
|
||||
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
<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 .ThemeType "light" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.light %>" />
|
||||
[[ else if eq .Theme "dark" ]]
|
||||
[[ else if eq .ThemeType "dark" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/<%= htmlWebpackPlugin.files.cssChunks.dark %>" />
|
||||
[[ end ]]
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
|||
<meta name="msapplication-config" content="public/img/browserconfig.xml" />
|
||||
</head>
|
||||
|
||||
<body class="theme-[[ .Theme ]] [[.AppNameBodyClass]]">
|
||||
<body class="theme-[[ .ThemeType ]] [[.AppNameBodyClass]]">
|
||||
<style>
|
||||
.preloader {
|
||||
height: 100%;
|
||||
|
|
Loading…
Reference in New Issue