diff --git a/packages/osd-ui-shared-deps/theme_config.d.ts b/packages/osd-ui-shared-deps/theme_config.d.ts index ff88a4965aca..080611cddc2e 100644 --- a/packages/osd-ui-shared-deps/theme_config.d.ts +++ b/packages/osd-ui-shared-deps/theme_config.d.ts @@ -3,6 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ +/** + * Types for valid theme versions + */ +type ThemeVersion = 'v7' | 'v8' | 'v9'; + +/** + * Types for valid theme color-scheme modes + */ +type ThemeMode = 'light' | 'dark'; + /** * Types for valid theme tags (themeVersion + themeMode) * Note: used by @osd/optimizer @@ -16,6 +26,12 @@ export type ThemeTags = readonly ThemeTag[]; */ export const themeTags: ThemeTags; +/** + * Map of themeTag values to their version and mode + * Note: this is used for ui display + */ +export const themeTagDetailMap: Map; + /** * Map of themeVersion values to labels * Note: this is used for ui display diff --git a/packages/osd-ui-shared-deps/theme_config.js b/packages/osd-ui-shared-deps/theme_config.js index 0ccd422e321d..ad782d98b2d7 100644 --- a/packages/osd-ui-shared-deps/theme_config.js +++ b/packages/osd-ui-shared-deps/theme_config.js @@ -4,10 +4,11 @@ */ /** - * The purpose of this file is to centalize theme configuration so it can be used across server, + * The purpose of this file is to centralize theme configuration so it can be used across server, * client, and dev tooling. DO NOT add dependencies that wouldn't operate in all of these contexts. * * Default theme is specified in the uiSettings schema. + * A version (key) and color-scheme mode cannot contain a backtick character. */ const THEME_MODES = ['light', 'dark']; @@ -23,7 +24,17 @@ const THEME_VERSION_VALUE_MAP = { ...Object.fromEntries(Object.keys(THEME_VERSION_LABEL_MAP).map((v) => [v, v])), }; const THEME_VERSIONS = Object.keys(THEME_VERSION_LABEL_MAP); -const THEME_TAGS = THEME_VERSIONS.flatMap((v) => THEME_MODES.map((m) => `${v}${m}`)); +const THEME_TAGS = []; + +const themeTagDetailMap = new Map(); +THEME_VERSIONS.forEach((version) => { + THEME_MODES.forEach((mode) => { + const key = `${version}${mode}`; + + themeTagDetailMap.set(key, { version, mode }); + THEME_TAGS.push(key); + }); +}); exports.themeVersionLabelMap = THEME_VERSION_LABEL_MAP; @@ -31,6 +42,8 @@ exports.themeVersionValueMap = THEME_VERSION_VALUE_MAP; exports.themeTags = THEME_TAGS; +exports.themeTagDetailMap = themeTagDetailMap; + exports.themeCssDistFilenames = THEME_VERSIONS.reduce((map, v) => { map[v] = THEME_MODES.reduce((acc, m) => { acc[m] = `osd-ui-shared-deps.${v}.${m}.css`; diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 8e50e331200d..210768cb72a7 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -153,6 +153,7 @@ function createRawRequestMock(customization: DeepPartial = {}) { isAuthenticated: true, }, headers: {}, + query: {}, path, route: { settings: {} }, url, diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index ad92d759a832..00d2fc0cfd5c 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -144,8 +144,8 @@ Object { "legacyMetadata": Object { "uiSettings": Object { "defaults": Object { - "registered": Object { - "name": "title", + "theme:darkMode": Object { + "value": true, }, }, "user": Object {}, @@ -306,11 +306,7 @@ Object { }, "legacyMetadata": Object { "uiSettings": Object { - "defaults": Object { - "registered": Object { - "name": "title", - }, - }, + "defaults": Object {}, "user": Object {}, }, }, diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index 5fa7d010989e..ac37ea595787 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -106,9 +106,433 @@ describe('RenderingService', () => { expect(data).toMatchSnapshot(INJECTED_METADATA); }); + it('renders "core" page driven by configured defaults', async () => { + // Defaults: Y, User: N, Overrides: N, themeTag: N + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue({}); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v7'); + }); + + it('renders "core" page driven by user settings', async () => { + // Defaults: Y, User: Y, Overrides: N, themeTag: N + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('light'); + expect($elStyle.attr('data-theme')).toBe('v8'); + }); + + it('renders "core" page driven by configured overrides', async () => { + // Defaults: Y, User: N, Overrides: Y, themeTag: N + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + const overridesConfig: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => overridesConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.isOverridden.mockImplementation((name) => name in overridesConfig); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('light'); + expect($elStyle.attr('data-theme')).toBe('v8'); + }); + + it('renders "core" page driven by configured theme-tag', async () => { + // Defaults: Y, User: N, Overrides: N, themeTag: Y + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue({}); + + const content = await render( + createOpenSearchDashboardsRequest({ query: { themeTag: 'v9light' } }), + uiSettings, + {} + ); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('light'); + expect($elStyle.attr('data-theme')).toBe('v9'); + }); + + it('renders "core" page driven by theme-tag despite user settings', async () => { + // Defaults: Y, User: Y, Overrides: N, themeTag: Y + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + + const content = await render( + createOpenSearchDashboardsRequest({ query: { themeTag: 'v9dark' } }), + uiSettings, + {} + ); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v9'); + }); + + it('renders "core" page driven by configured overrides despite user settings', async () => { + // Defaults: Y, User: Y, Overrides: Y + const defaultsConfig: Record = { + 'theme:darkMode': false, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + const overridesConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v9', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => overridesConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + uiSettings.isOverridden.mockImplementation((name) => name in overridesConfig); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v9'); + }); + + it('renders "core" page driven by configured overrides despite theme-tag', async () => { + // Defaults: Y, User: Y, Overrides: Y + const defaultsConfig: Record = { + 'theme:darkMode': false, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + const overridesConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v9', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => overridesConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + uiSettings.isOverridden.mockImplementation((name) => name in overridesConfig); + + const content = await render( + createOpenSearchDashboardsRequest({ query: { themeTag: 'v7light' } }), + uiSettings, + {} + ); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v9'); + }); + + it('renders "core" page using defaults when user setting is invalid', async () => { + // Defaults: Y, User: INVALID, Overrides: N + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': undefined, + 'theme:version': 'invalid', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v7'); + }); + + it('renders "core" page using defaults when configured override in invalid', async () => { + // Defaults: Y, User: N, Overrides: INVALID + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + const overridesConfig: Record = { + 'theme:darkMode': undefined, + 'theme:version': 'invalid', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => overridesConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.isOverridden.mockImplementation((name) => name in overridesConfig); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v7'); + }); + + it('renders "core" page using defaults when configured override in invalid despite user settings', async () => { + // Defaults: Y, User: Y, Overrides: INVALID + const defaultsConfig: Record = { + 'theme:darkMode': false, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + const overridesConfig: Record = { + 'theme:darkMode': undefined, + 'theme:version': 'invalid', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => overridesConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + uiSettings.isOverridden.mockImplementation((name) => name in overridesConfig); + + const content = await render(createOpenSearchDashboardsRequest(), uiSettings, {}); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('light'); + expect($elStyle.attr('data-theme')).toBe('v7'); + }); + + it('renders "core" page driven by configured defaults when theme-tag is invalid', async () => { + // Defaults: Y, User: N, Overrides: N, themeTag: N + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue({}); + + const content = await render( + createOpenSearchDashboardsRequest({ query: { themeTag: 'invalid' } }), + uiSettings, + {} + ); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v7'); + }); + + it('renders "core" page driven by user settings when theme-tag is invalid', async () => { + // Defaults: Y, User: Y, Overrides: N, themeTag: N + const defaultsConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => defaultsConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + + const content = await render( + createOpenSearchDashboardsRequest({ query: { themeTag: 'invalid' } }), + uiSettings, + {} + ); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('light'); + expect($elStyle.attr('data-theme')).toBe('v8'); + }); + + it('renders "core" page driven by configured overrides despite an invalid theme-tag', async () => { + // Defaults: Y, User: Y, Overrides: Y + const defaultsConfig: Record = { + 'theme:darkMode': false, + 'theme:version': 'v7', + }; + const userSettings: Record = { + 'theme:darkMode': false, + 'theme:version': 'v8', + }; + const overridesConfig: Record = { + 'theme:darkMode': true, + 'theme:version': 'v9', + }; + uiSettings.getOverrideOrDefault.mockImplementation((name) => overridesConfig[name]); + uiSettings.getRegistered.mockReturnValue( + Object.keys(defaultsConfig).reduce( + (acc, key) => ({ ...acc, [key]: { value: defaultsConfig[key] } }), + {} + ) + ); + uiSettings.getUserProvided.mockResolvedValue( + Object.keys(userSettings).reduce( + (acc, key) => ({ ...acc, [key]: { userValue: userSettings[key] } }), + {} + ) + ); + uiSettings.isOverridden.mockImplementation((name) => name in overridesConfig); + + const content = await render( + createOpenSearchDashboardsRequest({ query: { themeTag: 'invalid' } }), + uiSettings, + {} + ); + const dom = load(content); + const $elStyle = dom('style[data-theme][data-color-scheme]'); + + expect($elStyle.attr('data-color-scheme')).toBe('dark'); + expect($elStyle.attr('data-theme')).toBe('v9'); + }); + it('renders "core" page driven by defaults', async () => { uiSettings.getUserProvided.mockResolvedValue({ 'theme:darkMode': { userValue: false } }); uiSettings.getOverrideOrDefault.mockImplementation((name) => name === 'theme:darkMode'); + uiSettings.getRegistered.mockReturnValue({ 'theme:darkMode': { value: true } }); const content = await render(createOpenSearchDashboardsRequest(), uiSettings, { includeUserSettings: false, }); @@ -134,6 +558,7 @@ describe('RenderingService', () => { uiSettings.getOverrideOrDefault.mockImplementation((name) => name === 'theme:darkMode' ? undefined : false ); + uiSettings.getRegistered.mockReturnValue({}); const content = await render(createOpenSearchDashboardsRequest(), uiSettings, { includeUserSettings: false, }); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 36f55bb22097..5c94a92479f4 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -33,7 +33,7 @@ import { renderToStaticMarkup } from 'react-dom/server'; import { first, take } from 'rxjs/operators'; import { i18n } from '@osd/i18n'; import { Agent as HttpsAgent } from 'https'; -import { themeVersionValueMap } from '@osd/ui-shared-deps'; +import { themeVersionValueMap, themeTagDetailMap, ThemeTag } from '@osd/ui-shared-deps'; import Axios from 'axios'; // @ts-expect-error untyped internal module used to prevent axios from using xhr adapter in tests @@ -53,6 +53,7 @@ import { OpenSearchDashboardsConfigType } from '../opensearch_dashboards_config' import { HttpConfigType } from '../http/http_config'; import { SslConfig } from '../http/ssl_config'; import { LoggerFactory } from '../logging'; +import { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from '../ui_settings'; const DEFAULT_TITLE = 'OpenSearch Dashboards'; @@ -97,21 +98,24 @@ export class RenderingService { defaults: uiSettings.getRegistered(), user: includeUserSettings ? await uiSettings.getUserProvided() : {}, }; - // Cannot use `uiSettings.get()` since a user might not be authenticated - const darkMode = - (settings.user?.['theme:darkMode']?.userValue ?? - uiSettings.getOverrideOrDefault('theme:darkMode')) || - false; - - // At the very least, the schema should define a default theme; the '' will be unreachable - const configuredThemeVersion = - (settings.user?.['theme:version']?.userValue ?? - uiSettings.getOverrideOrDefault('theme:version')) || - ''; - // Validate themeVersion is in valid format - const themeVersion = - themeVersionValueMap[configuredThemeVersion] || - (uiSettings.getDefault('theme:version') as string); + + const themeTagOverride = (request.query as any).themeTag; + /* At the very least, the schema should define a default theme and darkMode; + * the false and '' below will be unreachable. + */ + const { darkMode = false, version: themeVersion = '' } = this.getThemeDetails( + // Cannot use `uiSettings.get()` since a user might not be authenticated + { + darkMode: settings.user?.['theme:darkMode'], + version: settings.user?.['theme:version'], + }, + { + darkMode: settings.defaults?.['theme:darkMode'], + version: settings.defaults?.['theme:version'], + }, + uiSettings, + themeTagOverride + ); const brandingAssignment = await this.assignBrandingConfig( darkMode, @@ -429,4 +433,83 @@ export class RenderingService { } return true; }; + + /** + * Determines the color-scheme mode and version of the theme to be applied. + * + * The theme values are selected in the following order of precedence: + * 1. A configured override from the YAML config file. + * 2. A requested override from the `themeTag` parameter in the URL. + * 3. A value configured by the user. + * 4. The default value specified in the YAML file or the schema. + * + * If `themeTag` is invalid, it is ignored. + * If any other extracted detail is invalid, the system default is used. + */ + private getThemeDetails = ( + userSettings: Record<'darkMode' | 'version', UserProvidedValues | undefined>, + defaults: Record<'darkMode' | 'version', PublicUiSettingsParams | undefined>, + uiSettings: IUiSettingsClient, + themeTagOverride?: string + ): { darkMode: boolean; version: string } => { + const darkMode = (() => { + /* eslint-disable dot-notation */ + // 1. A configured override from the YAML config file + if (uiSettings.isOverridden('theme:darkMode')) { + // The override value is stored in `userValue` + return uiSettings.getOverrideOrDefault('theme:darkMode') as string; + } + + // Check validity of `themeTagOverride` and get its details + const themeTagDetail = themeTagOverride + ? themeTagDetailMap.get(themeTagOverride as ThemeTag) + : undefined; + + // 2. A requested override from the `themeTag` parameter in the URL + if (themeTagDetail?.mode !== undefined) return themeTagDetail.mode === 'dark'; + + // 3. A value configured by the user + if (userSettings.darkMode?.userValue !== undefined) return userSettings.darkMode.userValue; + + // 4. The default value specified in the YAML file or the schema + return defaults['darkMode']?.value; + })(); + + const version = (() => { + /* eslint-disable dot-notation */ + // 1. A configured override from the YAML config file + if (uiSettings.isOverridden('theme:version')) { + // The override value is stored in `userValue` + return uiSettings.getOverrideOrDefault('theme:version') as string; + } + + // Check validity of `themeTagOverride` and get its details + const themeTagDetail = themeTagOverride + ? themeTagDetailMap.get(themeTagOverride as ThemeTag) + : undefined; + + // The version is a `string` and the best test is `||` + return ( + // 2. A requested override from the `themeTag` parameter in the URL + themeTagDetail?.version || + // 3. A value configured by the user + userSettings.version?.userValue || + // 4. The default value specified in the YAML file or the schema + defaults['version']?.value + ); + })(); + + return { + /* eslint-disable dot-notation */ + // If the value for `darkMode` couldn't be deduced, system default is used. + // The `false` is unreachable since schema will always have a default; set to accommodate tests. + darkMode: (darkMode as boolean) ?? defaults['darkMode']?.value ?? false, + // Checking `themeVersionValueMap` makes sure the version is valid; if not system default is used + version: + themeVersionValueMap[version as string] || + (defaults['version']?.value as string) || + // The `''` is unreachable since schema will always have a default; set to accommodate tests. + '', + }; + }; } diff --git a/src/core/server/rendering/views/styles.tsx b/src/core/server/rendering/views/styles.tsx index c3edbfe01bfd..8cd20f9c5253 100644 --- a/src/core/server/rendering/views/styles.tsx +++ b/src/core/server/rendering/views/styles.tsx @@ -48,6 +48,8 @@ export const Styles: FunctionComponent = ({ theme, darkMode }) => { return (