Skip to content

Commit

Permalink
Allow overriding the themeTag temporarily via the query-string
Browse files Browse the repository at this point in the history
Also:
* Fix `getOverrideOrDefault` not correctly returning overridden values

Signed-off-by: Miki <[email protected]>
  • Loading branch information
AMoo-Miki committed Oct 5, 2024
1 parent 200986a commit b803891
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 33 deletions.
16 changes: 16 additions & 0 deletions packages/osd-ui-shared-deps/theme_config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ThemeTag, { version: ThemeVersion; mode: ThemeMode }>;

/**
* Map of themeVersion values to labels
* Note: this is used for ui display
Expand Down
17 changes: 15 additions & 2 deletions packages/osd-ui-shared-deps/theme_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -23,14 +24,26 @@ 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;

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`;
Expand Down
102 changes: 86 additions & 16 deletions src/core/server/rendering/rendering_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, UserProvidedValues } from '../ui_settings';

const DEFAULT_TITLE = 'OpenSearch Dashboards';

Expand Down Expand Up @@ -97,21 +98,20 @@ 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'],
},
uiSettings,
themeTagOverride
);

const brandingAssignment = await this.assignBrandingConfig(
darkMode,
Expand Down Expand Up @@ -429,4 +429,74 @@ 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 any of the extracted details are invalid, the system defaults are used.
*/
private getThemeDetails = (
userSettings: Record<'darkMode' | 'version', UserProvidedValues<any>>,
uiSettings: IUiSettingsClient,
themeTagOverride?: string
): { darkMode: boolean; version: string } => {
const darkMode = (() => {
// 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) return userSettings.darkMode.userValue;

// 4. The default value specified in the YAML file or the schema
return uiSettings.getDefault('theme:darkMode');
})();

const version = (() => {
// 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;

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
uiSettings.getDefault('theme:version')
);
})();

return {
// If the value for `darkMode` couldn't be deduced, system default is used
darkMode: (darkMode as boolean) ?? (uiSettings.getDefault('theme:darkMode') as boolean),
// Checking `themeVersionValueMap` makes sure the version is valid; if not system default is used
version:
themeVersionValueMap[version as string] ||
(uiSettings.getDefault('theme:version') as string),
};
};
}
2 changes: 1 addition & 1 deletion src/core/server/ui_settings/ui_settings_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class UiSettingsClient implements IUiSettingsClient {
}

getOverrideOrDefault(key: string): unknown {
return this.isOverridden(key) ? this.overrides[key].value : this.defaults[key]?.value;
return this.isOverridden(key) ? this.overrides[key] : this.defaults[key]?.value;
}

getDefault(key: string): unknown {
Expand Down
1 change: 0 additions & 1 deletion src/core/server/ui_settings/ui_settings_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export const DEFAULT_THEME_VERSION = 'v8';
*
* The schema below exposes only a limited set of settings to be set in the config file.
*
* ToDo: Remove overrides; these were added to force the lock down the theme version.
* The schema is temporarily relaxed to allow overriding the `darkMode` and setting
* `defaults`. An upcoming change would relax them further to allow setting them.
*/
Expand Down
20 changes: 14 additions & 6 deletions src/legacy/ui/ui_render/bootstrap/template.js.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@ var osdCsp = JSON.parse(document.querySelector('osd-csp').getAttribute('data'));
window.__osdStrictCsp__ = osdCsp.strictCsp;
window.__osdThemeTag__ = "{{themeTag}}";
window.__osdPublicPath__ = {{publicPathMap}};
window.__osdBundles__ = {{osdBundlesLoaderSource}}
window.__osdBundles__ = {{osdBundlesLoaderSource}};


// Handle theme overridden via query-string
var themeTagOverride = new URLSearchParams(window.location.search).get('themeTag');
if (themeTagOverride) {
if (`{{{validThemeTags}}}`.split(',').includes(themeTagOverride)) {
window.__osdThemeTag__ = themeTagOverride;
} else {
console.warn('Ignoring invalid `themeTag` override');
}
}

if (window.__osdStrictCsp__ && window.__osdCspNotEnforced__) {
var legacyBrowserError = document.getElementById('osd_legacy_browser_error');
Expand Down Expand Up @@ -79,11 +90,8 @@ if (window.__osdStrictCsp__ && window.__osdCspNotEnforced__) {
], function () {
__osdBundles__.get('entry/core/public').__osdBootstrap__();

load([
{{#each styleSheetPaths}}
'{{this}}',
{{/each}}
]);
var styleSheetPaths = JSON.parse(`{{themeTagStyleSheetPaths}}`)[window.__osdThemeTag__];
load(styleSheetPaths);
});
}
}
31 changes: 24 additions & 7 deletions src/legacy/ui/ui_render/ui_render_mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,14 @@ export function uiRenderMixin(osdServer, server, config) {
? await uiSettings.get('theme:darkMode')
: uiSettings.getOverrideOrDefault('theme:darkMode');
const themeMode = darkMode ? 'dark' : 'light';
const isThemeModeOverridden = uiSettings.isOverridden('theme:darkMode');

const configuredThemeVersion =
!authEnabled || request.auth.isAuthenticated
? await uiSettings.get('theme:version')
: uiSettings.getOverrideOrDefault('theme:version');
const isThemeVersionOverridden = uiSettings.isOverridden('theme:version');

// Validate themeVersion is in valid format
const themeVersion =
UiSharedDeps.themeVersionValueMap[configuredThemeVersion] ||
Expand All @@ -150,12 +153,25 @@ export function uiRenderMixin(osdServer, server, config) {

const regularBundlePath = `${basePath}/${buildHash}/bundles`;

const styleSheetPaths = [
`${regularBundlePath}/osd-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
`${regularBundlePath}/osd-ui-shared-deps/${UiSharedDeps.themeCssDistFilenames[themeVersion][themeMode]}`,
`${basePath}/node_modules/@osd/ui-framework/dist/${UiSharedDeps.kuiCssDistFilenames[themeVersion][themeMode]}`,
`${basePath}/ui/legacy_${themeMode}_theme.css`,
];
/**
* If the theme's version or darkMode is overridden in the YAML configuration
* file, all the CSS assets offered will have their version or darkMode enforced
* based on the configured override. This is so a themeTagOverride will not be
* able to supersede the configuration overrides.
*/
const themeTagStyleSheetPaths = {};
for (const [themeTag, { version, mode }] of UiSharedDeps.themeTagDetailMap) {
// Override the version or mode offered for themeTags if needed
const effectiveVersion = isThemeVersionOverridden ? themeVersion : version;
const effectiveMode = isThemeModeOverridden ? themeMode : mode;

themeTagStyleSheetPaths[themeTag] = [
`${regularBundlePath}/osd-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`,
`${regularBundlePath}/osd-ui-shared-deps/${UiSharedDeps.themeCssDistFilenames[effectiveVersion][effectiveMode]}`,
`${basePath}/node_modules/@osd/ui-framework/dist/${UiSharedDeps.kuiCssDistFilenames[effectiveVersion][effectiveMode]}`,
`${basePath}/ui/legacy_${mode}_theme.css`,
];
}

const kpUiPlugins = osdServer.newPlatform.__internals.uiPlugins;
const kpPluginPublicPaths = new Map();
Expand Down Expand Up @@ -196,8 +212,9 @@ export function uiRenderMixin(osdServer, server, config) {
const bootstrap = new AppBootstrap({
templateData: {
themeTag,
validThemeTags: UiSharedDeps.themeTags.join(','),
jsDependencyPaths,
styleSheetPaths,
themeTagStyleSheetPaths: JSON.stringify(themeTagStyleSheetPaths),
publicPathMap,
},
});
Expand Down

0 comments on commit b803891

Please sign in to comment.