Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: simplify theme configuration and defaulting #7625

Merged
merged 11 commits into from
Aug 16, 2024
5 changes: 5 additions & 0 deletions changelogs/fragments/7625.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
refactor:
- Simplify theme configuration and defaulting ([#7625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7625))

deprecate:
- Deprecating `CssDistFilename` exports in favor of `themeCssDistFilenames` in `@osd/ui-shared-deps` ([#7625](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7625))
33 changes: 32 additions & 1 deletion docs/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Themes are defined in OUI via https://github.com/opensearch-project/oui/blob/main/src/themes/themes.ts. When Building OUI, there are several theming artifacts generated (beyond the react components) for each mode (light/dark) of each theme:

1. Theme compiled stylesheets (e.g. `@elastic/eui/dist/eui_theme_dark.css`). Consumed as entry files in [/packages/osd-ui-shared-deps/webpack.config.js](/packages/osd-ui-shared-deps/webpack.config.js) and republished by `osd-ui-shared-deps` (e.g. [UiSharedDeps.darkCssDistFilename](/packages/osd-ui-shared-deps/index.js)).
1. Theme compiled stylesheets (e.g. `@elastic/eui/dist/eui_theme_dark.css`). Consumed as entry files in [/packages/osd-ui-shared-deps/webpack.config.js](/packages/osd-ui-shared-deps/webpack.config.js) and republished by `osd-ui-shared-deps` (e.g. [UiSharedDeps.themeCssDistFilenames](/packages/osd-ui-shared-deps/index.js)).
2. Theme compiled and minified stylesheets (e.g. `@elastic/eui/dist/eui_theme_dark.min.css`). These appear unused by OpenSearch Dashboards
3. Theme computed SASS variables as JSON (e.g. `@elastic/eui/dist/eui_theme_dark.json`). Consumed by [/packages/osd-ui-shared-deps/theme.ts](/packages/osd-ui-shared-deps/theme.ts) and made available to other components via the mode and theme aware `euiThemeVars`. In general, these should not be consumed by any other component directly.
4. Theme type definition file for SASS variables as JSON (e.g. `@elastic/eui/dist/eui_theme_dark.json.d.ts`)
Expand Down Expand Up @@ -129,3 +129,34 @@ Component styles are not loaded as stylesheets.
4. Used by `src/core/server/rendering/views/theme.ts` to inject values into `src/core/server/rendering/views/styles.tsx`
5. Used (incorrectly) to style a badge color in `src/plugins/index_pattern_management/public/components/create_button/create_button.tsx`
6. Used by `src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts` to create Monaco theme styles

## Theme Management

### Change default theme

Update `DEFAULT_THEME_VERSION` in `src/core/server/ui_settings/ui_settings_config.ts` to point to the desired theme version.

### Adding a new theme

1. Add a [a new theme to OUI](https://github.com/opensearch-project/oui/blob/main/wiki/theming.md) and publish new OUI version
2. Update OSD to consume new OUI version
3. Make the following changes in OSD:
1. Load your theme by creating sass files in `src/core/public/core_app/styles`
2. Update [webpack config](packages/osd-ui-shared-deps/webpack.config.js) to create css files for your theme
2. Add kui css files:
1. Create kui sass files for your theme in `packages/osd-ui-framework/src/`
2. Update `packages/osd-ui-framework/Gruntfile.js` to build these files
3. Generate the files by running `npx grunt compileCss` from this package root
3. Add fonts to OSD:
1. Make sure your theme fonts are in [/src/core/server/core_app/assets/fonts](/src/core/server/core_app/assets/fonts/readme.md)
2. Update `src/core/server/rendering/views/fonts.tsx` to reference those files
3. Update src/core/server/core_app/assets/fonts/readme.md to reference the fonts
4. Update `packages/osd-ui-shared-deps/theme_config.js`:
1. Add version and label for version to `THEME_VERSION_LABEL_MAP`
2. Update `kuiCssDistFilenames` map for new theme
3. Update `ThemeTag` type in corresponding definition file (`theme_config.d.ts`)
5. Load variables for new theme in `packages/osd-ui-shared-deps/theme.ts'`
6. Update `src/legacy/ui/ui_render/ui_render_mixin.js':
1. Load variables for your theme in `THEME_SOURCES`
2. Define the text font for your theme in `fontText`
3. Define the code font for your theme in `fontCode`
9 changes: 5 additions & 4 deletions packages/osd-optimizer/src/common/theme_tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
* under the License.
*/

import { themeTags as THEME_TAGS } from '@osd/ui-shared-deps';
import type { ThemeTag, ThemeTags } from '@osd/ui-shared-deps';
import { ascending } from './array_helpers';

const tags = (...themeTags: string[]) =>
Expand All @@ -37,10 +39,9 @@ const validTag = (tag: any): tag is ThemeTag => ALL_THEMES.includes(tag);
const isArrayOfStrings = (input: unknown): input is string[] =>
Array.isArray(input) && input.every((v) => typeof v === 'string');

export type ThemeTags = readonly ThemeTag[];
export type ThemeTag = 'v7light' | 'v7dark' | 'v8light' | 'v8dark';
export const DEFAULT_THEMES = tags('v7light', 'v7dark', 'v8light', 'v8dark');
export const ALL_THEMES = tags('v7light', 'v7dark', 'v8light', 'v8dark');
export type { ThemeTag, ThemeTags };
export const DEFAULT_THEMES = tags(...THEME_TAGS);
export const ALL_THEMES = tags(...THEME_TAGS);

export function parseThemeTags(input?: any): ThemeTags {
if (!input) {
Expand Down
10 changes: 9 additions & 1 deletion packages/osd-ui-shared-deps/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,39 @@ export const jsFilename: string;
*/
export const jsDepFilenames: string[];

/**
* Re-export all types from theme_config
*/
export * from './theme_config';

/**
* Filename of the unthemed css file in the distributable directory
*/
export const baseCssDistFilename: string;

/**
* Filename of the dark-theme css file in the distributable directory
* @deprecated
*/
export const darkCssDistFilename: string;

/**
* Filename of the dark-theme css file in the distributable directory
* @deprecated
*/
export const darkV8CssDistFilename: string;

/**
* Filename of the light-theme css file in the distributable directory
* @deprecated
*/
export const lightCssDistFilename: string;

/**
* Filename of the light-theme css file in the distributable directory
* @deprecated
*/
export const lightV8CssDistFilename: string;

/**
* Externals mapping inteded to be used in a webpack config
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/osd-ui-shared-deps/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,18 @@

const Path = require('path');

Object.assign(exports, require('./theme_config'));
exports.distDir = Path.resolve(__dirname, 'target');
exports.jsDepFilenames = ['[email protected]'];
exports.jsFilename = 'osd-ui-shared-deps.js';
exports.baseCssDistFilename = 'osd-ui-shared-deps.css';
/** @deprecated */
exports.lightCssDistFilename = 'osd-ui-shared-deps.v7.light.css';
/** @deprecated */
exports.lightV8CssDistFilename = 'osd-ui-shared-deps.v8.light.css';
/** @deprecated */
exports.darkCssDistFilename = 'osd-ui-shared-deps.v7.dark.css';
/** @deprecated */
exports.darkV8CssDistFilename = 'osd-ui-shared-deps.v8.dark.css';
exports.externals = {
// stateful deps
Expand Down
9 changes: 5 additions & 4 deletions packages/osd-ui-shared-deps/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ export type Theme = typeof LightTheme;

// in the OpenSearch Dashboards app we can rely on this global being defined, but in
// some cases (like jest) the global is undefined
export const tag: string = globals.__osdThemeTag__ || 'v8light';
export const version = tag.startsWith('v7') ? 7 : 8;
virajsanghvi marked this conversation as resolved.
Show resolved Hide resolved
export const darkMode = tag.endsWith('dark');
export const tag: string = globals.__osdThemeTag__;
const themeVersion = tag?.replace(/(light|dark)$/, '') || 'v8';
export const version = parseInt(themeVersion.replace(/[^\d]+/g, ''), 10) || 8;
export const darkMode = tag?.endsWith?.('dark');

export let euiLightVars: Theme;
export let euiDarkVars: Theme;
if (version === 7) {
if (themeVersion === 'v7') {
euiLightVars = require('@elastic/eui/dist/eui_theme_light.json');
euiDarkVars = require('@elastic/eui/dist/eui_theme_dark.json');
} else {
Expand Down
41 changes: 41 additions & 0 deletions packages/osd-ui-shared-deps/theme_config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Types for valid theme tags (themeVersion + themeMode)
* Note: used by @osd/optimizer
*/
export type ThemeTag = 'v7light' | 'v7dark' | 'v8light' | 'v8dark';
export type ThemeTags = readonly ThemeTag[];

/**
* List of valid ThemeTags
* Note: used by @osd/optimizer
*/
export const themeTags: ThemeTags;

/**
* Map of themeVersion values to labels
* Note: this is used for ui display
*/
export const themeVersionLabelMap: Record<string, string>;

/**
* Map of labels and versions to themeVersion values
* Note: this is used to correct incorrectly persisted ui settings
*/
export const themeVersionValueMap: Record<string, string>;

/**
* Theme CSS distributable filenames by themeVersion and themeMode
* Note: used by bootstrap template
*/
export const themeCssDistFilenames: Record<string, Record<string, string>>;

/**
* KUI CSS distributable filenames by themeVersion and themeMode
* Note: used by bootstrap template
*/
export const kuiCssDistFilenames: Record<string, Record<string, string>>;
44 changes: 44 additions & 0 deletions packages/osd-ui-shared-deps/theme_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

/**
* The purpose of this file is to centalize 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.
*/

const THEME_MODES = ['light', 'dark'];
const THEME_VERSION_LABEL_MAP = {
v7: 'v7',
v8: 'Next (preview)',
};
const THEME_VERSION_VALUE_MAP = {
// allow version lookup by label ...
...Object.fromEntries(Object.entries(THEME_VERSION_LABEL_MAP).map((a) => a.reverse())),
// ... or by the version itself
...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}`));

exports.themeVersionLabelMap = THEME_VERSION_LABEL_MAP;

exports.themeVersionValueMap = THEME_VERSION_VALUE_MAP;

exports.themeTags = THEME_TAGS;

exports.themeCssDistFilenames = THEME_VERSIONS.reduce((map, v) => {
map[v] = THEME_MODES.reduce((acc, m) => {
acc[m] = `osd-ui-shared-deps.${v}.${m}.css`;
return acc;
}, {});
return map;
}, {});

exports.kuiCssDistFilenames = {
v7: { dark: 'kui_dark.css', light: 'kui_light.css' },
v8: { dark: 'kui_next_dark.css', light: 'kui_next_light.css' },
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/core/public/ui_settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ export interface IUiSettingsClient {
*/
getAll: () => Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>>;

/**
* Gets the default value for a specific uiSetting. If the parameter is not defined and the key is
* not registered by any plugin then an error is thrown, otherwise reads the default value defined by
* a plugin.
*/
getDefault: <T = any>(key: string) => T;

/**
* Sets the value for a uiSetting. If the setting is not registered by any plugin
* it will be stored as a custom setting. The new value will be synchronously available via
Expand Down
18 changes: 18 additions & 0 deletions src/core/public/ui_settings/ui_settings_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ afterEach(() => {
done$.complete();
});

describe('#getDefault', () => {
it('fetches correct uiSettings defaults', () => {
const { client } = setup();
expect(client.getDefault('dateFormat')).toMatchSnapshot();
expect(client.getDefault('aLongNumeral')).toBe(BigInt(Number.MAX_SAFE_INTEGER) + 11n);
});

it('converts json default values', () => {
const { client } = setup({ defaults: { test: { value: '{"a": 1}', type: 'json' } } });
expect(client.getDefault('test')).toMatchSnapshot();
});

it("throws on unknown properties that don't have a value yet.", () => {
const { client } = setup();
expect(() => client.getDefault('unknownProperty')).toThrowErrorMatchingSnapshot();
});
});

describe('#get', () => {
it('gives access to uiSettings values', () => {
const { client } = setup();
Expand Down
37 changes: 26 additions & 11 deletions src/core/public/ui_settings/ui_settings_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { cloneDeep, defaultsDeep } from 'lodash';
import { Observable, Subject, concat, defer, of } from 'rxjs';
import { filter, map } from 'rxjs/operators';

import { UserProvidedValues, PublicUiSettingsParams } from 'src/core/server/types';
import { UserProvidedValues, PublicUiSettingsParams, UiSettingsType } from 'src/core/server/types';
import { IUiSettingsClient, UiSettingsState } from './types';

import { UiSettingsApi } from './ui_settings_api';
Expand Down Expand Up @@ -78,6 +78,18 @@ export class UiSettingsClient implements IUiSettingsClient {
return cloneDeep(this.cache);
}

getDefault<T = any>(key: string): T {
const declared = this.isDeclared(key);

if (!declared) {
throw new Error(
`Unexpected \`IUiSettingsClient.getDefaultValue("${key}")\` call on unrecognized configuration setting "${key}".
Please check that the setting for "${key}" exists.`
);
}
return this.resolveValue(this.cache[key].value, this.cache[key].type);
}

get<T = any>(key: string, defaultOverride?: T) {
const declared = this.isDeclared(key);

Expand All @@ -99,16 +111,7 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r
const userValue = this.cache[key].userValue;
const defaultValue = defaultOverride !== undefined ? defaultOverride : this.cache[key].value;
const value = userValue == null ? defaultValue : userValue;

if (type === 'json') {
return JSON.parse(value);
}

return type === 'number' && typeof value !== 'bigint'
? isFinite(value) && (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER)
? BigInt(value)
: parseFloat(value)
: value;
return this.resolveValue(value, type);
}

get$<T = any>(key: string, defaultOverride?: T) {
Expand Down Expand Up @@ -180,6 +183,18 @@ You can use \`IUiSettingsClient.get("${key}", defaultValue)\`, which will just r
return this.updateErrors$.asObservable();
}

private resolveValue(value: any, type: UiSettingsType | undefined) {
if (type === 'json') {
return JSON.parse(value);
}

return type === 'number' && typeof value !== 'bigint'
? isFinite(value) && (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER)
? BigInt(value)
: parseFloat(value)
: value;
}

private getBrowserStoredSettings() {
const uiSettingsJSON = window.localStorage.getItem('uiSettings') || '{}';
try {
Expand Down
1 change: 1 addition & 0 deletions src/core/public/ui_settings/ui_settings_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { IUiSettingsClient } from './types';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<IUiSettingsClient> = {
getAll: jest.fn(),
getDefault: jest.fn(),
get: jest.fn(),
get$: jest.fn(),
set: jest.fn(),
Expand Down
Loading
Loading