From 5821783068f0d95279e679aa20975b9ebcd33448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 22 Mar 2021 08:48:53 +0100 Subject: [PATCH] Themes: Switch theme without reload using global shortcut (#32180) * Themes: Switch theme without reload using global shortcut * Review updates --- .../SharedPreferences/SharedPreferences.tsx | 13 ++-- .../app/core/services/PreferencesService.ts | 14 ++++ public/app/core/services/keybindingSrv.ts | 4 ++ public/app/core/services/toggleTheme.ts | 43 +++++++++++ public/app/features/profile/ProfileCtrl.ts | 71 ------------------- public/app/types/index.ts | 1 + public/app/types/preferences.ts | 7 ++ public/views/index-template.html | 6 +- 8 files changed, 79 insertions(+), 80 deletions(-) create mode 100644 public/app/core/services/PreferencesService.ts create mode 100644 public/app/core/services/toggleTheme.ts delete mode 100644 public/app/features/profile/ProfileCtrl.ts create mode 100644 public/app/types/preferences.ts diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index d210c6244e3..908ac1cdc54 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -19,6 +19,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; import { backendSrv } from 'app/core/services/backend_srv'; +import { PreferencesService } from 'app/core/services/PreferencesService'; export interface Props { resourceUri: string; @@ -38,11 +39,12 @@ const themes: SelectableValue[] = [ ]; export class SharedPreferences extends PureComponent { - backendSrv = backendSrv; + service: PreferencesService; constructor(props: Props) { super(props); + this.service = new PreferencesService(props.resourceUri); this.state = { homeDashboardId: 0, theme: '', @@ -52,7 +54,7 @@ export class SharedPreferences extends PureComponent { } async componentDidMount() { - const prefs = await backendSrv.get(`/api/${this.props.resourceUri}/preferences`); + const prefs = await this.service.load(); const dashboards = await backendSrv.search({ starred: true }); const defaultDashboardHit: DashboardSearchHit = { id: 0, @@ -88,12 +90,7 @@ export class SharedPreferences extends PureComponent { onSubmitForm = async () => { const { homeDashboardId, theme, timezone } = this.state; - - await backendSrv.put(`/api/${this.props.resourceUri}/preferences`, { - homeDashboardId, - theme, - timezone, - }); + this.service.update({ homeDashboardId, theme, timezone }); window.location.reload(); }; diff --git a/public/app/core/services/PreferencesService.ts b/public/app/core/services/PreferencesService.ts new file mode 100644 index 00000000000..174e3dd7d5b --- /dev/null +++ b/public/app/core/services/PreferencesService.ts @@ -0,0 +1,14 @@ +import { UserPreferencesDTO } from 'app/types'; +import { backendSrv } from './backend_srv'; + +export class PreferencesService { + constructor(private resourceUri: string) {} + + update(preferences: UserPreferencesDTO): Promise { + return backendSrv.put(`/api/${this.resourceUri}/preferences`, preferences); + } + + load(): Promise { + return backendSrv.get(`/api/${this.resourceUri}/preferences`); + } +} diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 8ef4acb18f6..3767c115d35 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -20,6 +20,7 @@ import { import { contextSrv } from '../core'; import { getDatasourceSrv } from '../../features/plugins/datasource_srv'; import { getTimeSrv } from '../../features/dashboard/services/TimeSrv'; +import { toggleTheme } from './toggleTheme'; export class KeybindingSrv { modalOpen = false; @@ -43,6 +44,9 @@ export class KeybindingSrv { this.bind('esc', this.exit); this.bindGlobal('esc', this.globalEsc); } + + this.bind('t t', () => toggleTheme(false)); + this.bind('t r', () => toggleTheme(true)); } private globalEsc() { diff --git a/public/app/core/services/toggleTheme.ts b/public/app/core/services/toggleTheme.ts new file mode 100644 index 00000000000..307dfea48db --- /dev/null +++ b/public/app/core/services/toggleTheme.ts @@ -0,0 +1,43 @@ +import { getTheme } from '@grafana/ui'; +import { ThemeChangedEvent } from 'app/types/events'; +import appEvents from '../app_events'; +import { config } from '../config'; +import { PreferencesService } from './PreferencesService'; + +export async function toggleTheme(runtimeOnly: boolean) { + const currentTheme = config.theme; + const newTheme = getTheme(currentTheme.isDark ? 'light' : 'dark'); + appEvents.publish(new ThemeChangedEvent(newTheme)); + + if (runtimeOnly) { + return; + } + + // Add css file for new theme + const newCssLink = document.createElement('link'); + newCssLink.rel = 'stylesheet'; + newCssLink.href = config.bootData.themePaths[newTheme.type]; + document.body.appendChild(newCssLink); + + // 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.indexOf(`build/grafana.${currentTheme.type}`) > 0) { + // Remove existing link after a 500ms to allow new css to load 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 + setTimeout(() => link.remove(), 500); + } + } + + // Persist new theme + const service = new PreferencesService('user'); + const currentPref = await service.load(); + + await service.update({ + ...currentPref, + theme: newTheme.type, + }); +} diff --git a/public/app/features/profile/ProfileCtrl.ts b/public/app/features/profile/ProfileCtrl.ts deleted file mode 100644 index 44d6b57a6cf..00000000000 --- a/public/app/features/profile/ProfileCtrl.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { coreModule, NavModelSrv } from 'app/core/core'; -import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; -import { UserSession } from 'app/types'; -import { getBackendSrv } from '@grafana/runtime'; -import { promiseToDigest } from 'app/core/utils/promiseToDigest'; -import { IScope } from 'angular'; - -export class ProfileCtrl { - sessions: object[] = []; - navModel: any; - - /** @ngInject */ - constructor(private $scope: IScope, navModelSrv: NavModelSrv) { - this.getUserSessions(); - this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0); - } - - getUserSessions() { - promiseToDigest(this.$scope)( - getBackendSrv() - .get('/api/user/auth-tokens') - .then((sessions: UserSession[]) => { - sessions.reverse(); - - const found = sessions.findIndex((session: UserSession) => { - return session.isActive; - }); - - if (found) { - const now = sessions[found]; - sessions.splice(found, found); - sessions.unshift(now); - } - - this.sessions = sessions.map((session: UserSession) => { - return { - id: session.id, - isActive: session.isActive, - seenAt: dateTimeFormatTimeAgo(session.seenAt), - createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), - clientIp: session.clientIp, - browser: session.browser, - browserVersion: session.browserVersion, - os: session.os, - osVersion: session.osVersion, - device: session.device, - }; - }); - }) - ); - } - - revokeUserSession(tokenId: number) { - promiseToDigest(this.$scope)( - getBackendSrv() - .post('/api/user/revoke-auth-token', { - authTokenId: tokenId, - }) - .then(() => { - this.sessions = this.sessions.filter((session: UserSession) => { - if (session.id === tokenId) { - return false; - } - return true; - }); - }) - ); - } -} - -coreModule.controller('ProfileCtrl', ProfileCtrl); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 5a0d5438021..a6c51a6c388 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -16,6 +16,7 @@ export * from './ldap'; export * from './appEvent'; export * from './angular'; export * from './query'; +export * from './preferences'; import * as CoreEvents from './events'; export { CoreEvents }; diff --git a/public/app/types/preferences.ts b/public/app/types/preferences.ts new file mode 100644 index 00000000000..f2403644fb5 --- /dev/null +++ b/public/app/types/preferences.ts @@ -0,0 +1,7 @@ +import { TimeZone } from '@grafana/data'; + +export interface UserPreferencesDTO { + timezone: TimeZone; + homeDashboardId: number; + theme: string; +} diff --git a/public/views/index-template.html b/public/views/index-template.html index d91cdcfd512..ebcdc18e5cc 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -257,7 +257,11 @@ window.grafanaBootData = { user: [[.User]], settings: [[.Settings]], - navTree: [[.NavTree]] + navTree: [[.NavTree]], + themePaths: { + light: '[[.ContentDeliveryURL]]public/build/grafana.light.<%= webpack.hash %>.css', + dark: '[[.ContentDeliveryURL]]public/build/grafana.dark.<%= webpack.hash %>.css' + } }; // In case the js files fails to load the code below will show an info message.