Skip to content

Commit

Permalink
refactor: simplify theme configuration and defaulting (#7625) (#7746)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: Viraj Sanghvi <[email protected]>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
(cherry picked from commit bcdbbef)
  • Loading branch information
virajsanghvi authored Aug 19, 2024
1 parent 23ff261 commit 1e5addb
Show file tree
Hide file tree
Showing 26 changed files with 426 additions and 65 deletions.
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))
162 changes: 162 additions & 0 deletions docs/theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Theme System

## Basic concepts

### Theme definitions in OUI

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.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`)

Note that all of these artifacts should ideally only be imported or used directly in one place (by `osd-ui-shared-deps`).

In addition to these artifacts, OpenSearch Dashboards also makes heavy use of the theme SASS variables and mixins as defined in the source files (e.g. `@elastic/eui/src/theme_dark.scss`).

### Theme definitions in OpenSearch Dashboards

1. Theme tags are defined in [/packages/osd-optimizer/src/common/theme_tags.ts](/packages/osd-optimizer/src/common/theme_tags.ts) corresponding to each mode (light/dark) of each OUI theme.
2. These tags must correspond to entrypoint SCSS files in [/src/core/public/core_app/styles/](/src/core/public/core_app/styles/_globals_v8dark.scss), because they are imported by all SCSS files as part of the `sass-loader` in [/packages/osd-optimizer/src/worker/webpack.config.ts](/packages/osd-optimizer/src/worker/webpack.config.ts) and [/packages/osd-optimizer/src/worker/theme_loader.ts](/packages/osd-optimizer/src/worker/theme_loader.ts). Note that the optimizer webpack will compile a separate stylesheet for each unique mode and theme combination.
3. OUI SCSS source files are also imported by `osd-ui-framework`, which generates the legacy KUI stylesheets (e.g. [/packages/osd-ui-framework/src/kui_next_dark.scss](/packages/osd-ui-framework/src/kui_next_dark.scss)). KUI is a UI library that predates EUI/OUI, and should be deprecated and fully removed via [#1060](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1060). Because it's a legacy package it has its own build process that doesn't use webpack; it just [compiles the SCSS files with grunt](/packages/osd-ui-framework/Gruntfile.js). But similarly to 2., a separate stylesheet is generated for each mode and theme combination.

### Thmemed assets in OpenSearch Dasboards

In general, most themed assests can be found in [/src/core/server/core_app/assets](src/core/server/core_app/assets/fonts/readme.md) (it also includes non-themed assets such as `favicons`, which could easily be themed if desired in the future).

Most of the graphics/images are only dark/light mode-specific, not theme-specific:

1. `default_branding` marks
2. `logos`

This directory also includes legacy CSS files ([/src/core/server/core_app/assets/legacy_dark_theme.css](/src/core/server/core_app/assets/legacy_dark_theme.css) and [/src/core/server/core_app/assets/legacy_light_theme.css](/src/core/server/core_app/assets/legacy_light_theme.css)), which predate even KUI, and are still used by some plugins (notably `discover`). See [#4385](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/4385) for an experiment in removing these. Unlike KUI, they don't rely on OUI themes at all.

Finally, font assets are a bit of a special case. Theme-specific fonts are defined by OUI, but it doesn't include the font definitions directly. Instead, the font assets are in [/src/core/server/core_app/assets/fonts](/src/core/server/core_app/assets/fonts/readme.md). The corresponding `@font-face` style definitions are generated at runtime via [/src/core/server/rendering/views/fonts.tsx](/src/core/server/rendering/views/fonts.tsx).

## Theme settings

## Theme loading

```mermaid
sequenceDiagram
autonumber
critical Setup
core/server->>core/server/rendering: setup rendering service
core/server/rendering->>core/server: provide render() method
core/server->>core/server: setup legacy service
core/server->>legacy: create legacy server
legacy->>legacy: start ui mixin to<br>handle special routes
core/server->>core/server/core_app: setup core app
core/server/core_app->>core/server/core_app: register default routes
core/server/core_app->>core/server/core_app: register static asset dir routes
end
Browser->>core/server: OSD page request (e.g. /app/home#/ )
core/server->>core/server/core_app: request to default route<br>(via `http` service)
core/server/core_app->>core/server: call renderCoreApp()
core/server->>core/server/rendering: call render()
critical Initial page bootstrap
core/server/rendering->>core/server/rendering: get theme settings from config
core/server/rendering->>core/server/rendering: assign branding values \<br>(including dark mode)
core/server/rendering->>Browser: return static loading page template
Note over core/server/rendering,Browser: includes inlined font-face styles and static loading page styles
critical <head> (render blocking)
Browser->>Browser: define injection points
Browser->>Browser: load static loading page styles
Browser->>Browser: load font-face styles
Browser->>legacy: load startup.js special route
legacy->>legacy: build startup.js from template
Note over legacy: inject theme settings and font sources
legacy->>Browser: startup.js
critical startup.js
Browser->>Browser: get theme preferences from local storage
Browser->>Browser: set global theme tag
Browser->>Browser: inject theme-specific loading page styles
Browser->>Browser: inject theme-specific font css vars
end
end
Browser->>Browser: render loading/error page<br>(with loaders hidden)
Browser->>legacy: load bootstrap.js special route
legacy->>legacy: build bootstrap.js from template
legacy->>Browser: bootstrap.js
critical bootstrap.js
Browser->>Browser: toggle visibility of errors/loaders
Browser->>Browser: get theme preferences from local storage
Browser->>core/server/core_app: load js bundles
core/server/core_app->>Browser: (React application)
Browser->>core/server/core_app: load theme-specific stylesheets<br>(base, OUI, KUI, legacy)
core/server/core_app->>Browser: themed css
end
end
```

### Loading

`src/legacy/ui/ui_render/ui_render_mixin.js` via `src/legacy/ui/ui_render/bootstrap/template.js.hbs` and `src/legacy/ui/ui_render/bootstrap/app_bootstrap.js`. Aliased in `src/legacy/ui/ui_mixin.js`, called by `src/legacy/server/osd_server.js`. Called by `src/core/server/legacy/legacy_service.ts` via `src/core/server/server.ts`

### Injected style tags

1. `src/core/server/rendering/views/styles.tsx` - depends on dark/light mode and injects style tag in head
2. `src/core/server/rendering/views/fonts.tsx` - depends on theme version and injects font style tag in head
3. Monaco editor styles
4. Ace styles
5. Ace TM overrides
6. Ace error styles
6. Component styles

### Styleshsheets loaded

Each of the following are loaded in the browser by the [bootstrap script](/src/legacy/ui/ui_render/bootstrap/template.js.hbs) in this order. Currently, these are never unloaded.

1. Monaco editor styles (e.g. [/packages/osd-ui-shared-deps/target/osd-ui-shared-deps.css](/packages/osd-ui-shared-deps/target/osd-ui-shared-deps.css)), packaged by [/packages/osd-ui-shared-deps/webpack.config.js](/packages/osd-ui-shared-deps/webpack.config.js). In theory, this file could include styles from other shared dependencies, but currently `osd-monaco` is the only package that exports styles. Note that these are the default, un-themed styles; theming of monaco editors is handled by [/src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts](/src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts).
2. Theme and mode-specific OUI styles (e.g. [](), compiled by `packages/osd-ui-shared-deps/webpack.config.js`).
3. Theme and mode-specific KUI styles (e.g. `packages/osd-ui-framework/src/kui_next_dark.scss`, compiled by `packages/osd-ui-framework/Gruntfile.js`). Separate stylesheets for each theme version/dark mode combo (colors).
4. Mode-specific legacy styles (e.g. [/src/core/server/core_app/assets/legacy_dark_theme.css](/src/core/server/core_app/assets/legacy_dark_theme.css))

Component styles are not loaded as stylesheets.

## Current theme usage

### JSON/JS Vars

1. Defined by `packages/osd-ui-shared-deps/theme.ts`
1. Used by `src/plugins/charts/public/static/color_maps/color_maps.ts` to set vis colors
2. Used by `src/plugins/discover/public/application/components/chart/histogram/histogram.tsx` to define Discover histogram Elastic Chart styling
3. Used by `src/plugins/maps_legacy/public/map/opensearch_dashboards_map.js` and `src/plugins/region_map/public/choropleth_layer.js` for minor map UI styling (line color, empty shade)
4. Used by `src/plugins/vis_type_vega/public/data_model/vega_parser.ts` for Vega/Vega-Lite theming
2. Used by `src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js` for tooltip spacing
3. Used by `src/plugins/expressions/public/react_expression_renderer.tsx` to define padding options.
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;
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
Loading

0 comments on commit 1e5addb

Please sign in to comment.