diff --git a/packages/core/ui-settings/core-ui-settings-common/index.ts b/packages/core/ui-settings/core-ui-settings-common/index.ts index c06251643fbf7..d290b9065c546 100644 --- a/packages/core/ui-settings/core-ui-settings-common/index.ts +++ b/packages/core/ui-settings/core-ui-settings-common/index.ts @@ -17,6 +17,18 @@ export type { GetUiSettingsContext, } from './src/ui_settings'; export { type DarkModeValue, parseDarkModeValue } from './src/dark_mode'; -export { parseThemeNameValue } from './src/theme_name'; +export { + DEFAULT_THEME_TAGS, + SUPPORTED_THEME_TAGS, + DEFAULT_THEME_NAME, + SUPPORTED_THEME_NAMES, + FALLBACK_THEME_TAG, + parseThemeTags, + hasNonDefaultThemeTags, + parseThemeNameValue, + type ThemeName, + type ThemeTag, + type ThemeTags, +} from './src/theme'; export { TIMEZONE_OPTIONS } from './src/timezones'; diff --git a/packages/core/ui-settings/core-ui-settings-common/src/theme.ts b/packages/core/ui-settings/core-ui-settings-common/src/theme.ts new file mode 100644 index 0000000000000..ca89958deac16 --- /dev/null +++ b/packages/core/ui-settings/core-ui-settings-common/src/theme.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const DEFAULT_THEME_NAME = 'amsterdam'; +export const SUPPORTED_THEME_NAMES = ['amsterdam', 'borealis']; + +export type ThemeName = typeof SUPPORTED_THEME_NAMES[number]; + +/** + * Theme tags of the Amsterdam theme + */ +export const ThemeAmsterdamTags = ['v8light', 'v8dark'] as const; + +/** + * Theme tags of the experimental Borealis theme + */ +export const ThemeBorealisTags = ['borealislight', 'borealisdark'] as const; + +/** + * An array of all theme tags supported by Kibana. Note that this list doesn't + * reflect what theme tags are available in a Kibana build. + */ +export const SUPPORTED_THEME_TAGS = [...ThemeAmsterdamTags, ...ThemeBorealisTags] as const; + +export type ThemeTag = (typeof SUPPORTED_THEME_TAGS)[number]; +export type ThemeTags = readonly ThemeTag[]; + +/** + * An array of theme tags available in Kibana by default when not customized + * using KBN_OPTIMIZER_THEMES environment variable. + */ +export const DEFAULT_THEME_TAGS: ThemeTags = ThemeAmsterdamTags; + +export const FALLBACK_THEME_TAG: ThemeTag = 'v8light'; + +const isValidTag = (tag: unknown) => + SUPPORTED_THEME_TAGS.includes(tag as (typeof SUPPORTED_THEME_TAGS)[number]); + +export function parseThemeTags(input?: unknown): ThemeTags { + if (!input) { + return DEFAULT_THEME_TAGS; + } + + if (input === '*') { + // TODO: Replace with SUPPORTED_THEME_TAGS when Borealis is in public beta + return DEFAULT_THEME_TAGS; + } + + let rawTags: string[]; + if (typeof input === 'string') { + rawTags = input.split(',').map((tag) => tag.trim()); + } else if (Array.isArray(input)) { + rawTags = input; + } else { + throw new Error('Invalid theme tags, must be an array of strings'); + } + + if (!rawTags.length) { + throw new Error( + `Invalid theme tags, you must specify at least one of [${SUPPORTED_THEME_TAGS.join(', ')}]` + ); + } + + const invalidTags = rawTags.filter((t) => !isValidTag(t)); + if (invalidTags.length) { + throw new Error( + `Invalid theme tags [${invalidTags.join(', ')}], options: [${SUPPORTED_THEME_TAGS.join( + ', ' + )}]` + ); + } + + return rawTags as ThemeTags; +} + +export const hasNonDefaultThemeTags = (tags: ThemeTags) => + tags.length !== DEFAULT_THEME_TAGS.length || + tags.some((tag) => !DEFAULT_THEME_TAGS.includes(tag as (typeof DEFAULT_THEME_TAGS)[number])); + +export const parseThemeNameValue = (value: unknown): ThemeName => { + if (typeof value !== 'string') { + return DEFAULT_THEME_NAME; + } + + const themeName = value.toLowerCase(); + if (SUPPORTED_THEME_NAMES.includes(themeName.toLowerCase() as ThemeName)) { + return themeName as ThemeName; + } + + return DEFAULT_THEME_NAME; +}; diff --git a/packages/core/ui-settings/core-ui-settings-common/src/theme_name.ts b/packages/core/ui-settings/core-ui-settings-common/src/theme_name.ts deleted file mode 100644 index 2e6043d098d66..0000000000000 --- a/packages/core/ui-settings/core-ui-settings-common/src/theme_name.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export const parseThemeNameValue = (value: unknown): string => { - if (typeof value !== 'string') { - return 'amsterdam'; - } - - return value; -}; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts index b2c7e6029cf93..d45d89f4e4af5 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts @@ -10,15 +10,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; -import type { UiSettingsParams } from '@kbn/core-ui-settings-common'; - -function parseThemeTags() { - if (!process.env.KBN_OPTIMIZER_THEMES || process.env.KBN_OPTIMIZER_THEMES === '*') { - return ['v8light', 'v8dark']; - } - - return process.env.KBN_OPTIMIZER_THEMES.split(',').map((t) => t.trim()); -} +import { type UiSettingsParams, parseThemeTags, SUPPORTED_THEME_NAMES } from '@kbn/core-ui-settings-common'; function getThemeInfo(options: GetThemeSettingsOptions) { if (options?.isDist ?? true) { @@ -27,7 +19,7 @@ function getThemeInfo(options: GetThemeSettingsOptions) { }; } - const themeTags = parseThemeTags(); + const themeTags = parseThemeTags(process.env.KBN_OPTIMIZER_THEMES); return { defaultDarkMode: themeTags[0].endsWith('dark'), }; @@ -98,11 +90,14 @@ export const getThemeSettings = ( defaultMessage: 'Theme', }), type: 'select', - options: ['amsterdam'], + options: SUPPORTED_THEME_NAMES, optionLabels: { amsterdam: i18n.translate('core.ui_settings.params.themeName.options.amsterdam', { defaultMessage: 'Amsterdam', }), + borealis: i18n.translate('core.ui_settings.params.themeName.options.borealis', { + defaultMessage: 'Borealis', + }), }, value: 'amsterdam', readonly: Object.hasOwn(options, 'isThemeSwitcherEnabled') @@ -111,6 +106,7 @@ export const getThemeSettings = ( requiresPageReload: true, schema: schema.oneOf([ schema.literal('amsterdam'), + schema.literal('borealis'), // Allow experimental themes schema.string(), ]), diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 112e677c9d713..543991a92065d 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -18,7 +18,6 @@ export * from './rxjs_helpers'; export * from './array_helpers'; export * from './event_stream_helpers'; export * from './parse_path'; -export * from './theme_tags'; export * from './obj_helpers'; export * from './hashes'; export * from './dll_manifest'; diff --git a/packages/kbn-optimizer/src/common/theme_tags.test.ts b/packages/kbn-optimizer/src/common/theme_tags.test.ts deleted file mode 100644 index edf58797587f6..0000000000000 --- a/packages/kbn-optimizer/src/common/theme_tags.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { parseThemeTags } from './theme_tags'; - -it('returns default tags when passed undefined', () => { - expect(parseThemeTags()).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('returns all tags when passed *', () => { - expect(parseThemeTags('*')).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('returns specific tag when passed a single value', () => { - expect(parseThemeTags('v8light')).toMatchInlineSnapshot(` - Array [ - "v8light", - ] - `); -}); - -it('returns specific tags when passed a comma separated list', () => { - expect(parseThemeTags('v8light,v8dark')).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('returns specific tags when passed an array', () => { - expect(parseThemeTags(['v8light', 'v8dark'])).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('throws when an invalid tag is in the array', () => { - expect(() => parseThemeTags(['v8light', 'v7light'])).toThrowErrorMatchingInlineSnapshot( - `"Invalid theme tags [v7light], options: [v8dark, v8light]"` - ); -}); - -it('throws when an invalid tags in comma separated list', () => { - expect(() => parseThemeTags('v8light ,v7light')).toThrowErrorMatchingInlineSnapshot( - `"Invalid theme tags [v7light], options: [v8dark, v8light]"` - ); -}); - -it('returns tags in alphabetical order', () => { - const tags = parseThemeTags(['v8dark', 'v8light']); - expect(tags).toEqual(tags.slice().sort((a, b) => a.localeCompare(b))); -}); - -it('returns an immutable array', () => { - expect(() => { - const tags = parseThemeTags('v8light'); - // @ts-expect-error - tags.push('foo'); - }).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`); -}); diff --git a/packages/kbn-optimizer/src/common/theme_tags.ts b/packages/kbn-optimizer/src/common/theme_tags.ts deleted file mode 100644 index fc126d55a4330..0000000000000 --- a/packages/kbn-optimizer/src/common/theme_tags.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ascending } from './array_helpers'; - -const tags = (...themeTags: string[]) => - Object.freeze(themeTags.sort(ascending((tag) => tag)) as ThemeTag[]); - -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 = 'v8light' | 'v8dark'; -export const DEFAULT_THEMES = tags('v8light', 'v8dark'); -export const ALL_THEMES = tags('v8light', 'v8dark'); - -export function parseThemeTags(input?: any): ThemeTags { - if (!input) { - return DEFAULT_THEMES; - } - - if (input === '*') { - return ALL_THEMES; - } - - if (typeof input === 'string') { - input = input.split(',').map((tag) => tag.trim()); - } - - if (!isArrayOfStrings(input)) { - throw new Error(`Invalid theme tags, must be an array of strings`); - } - - if (!input.length) { - throw new Error( - `Invalid theme tags, you must specify at least one of [${ALL_THEMES.join(', ')}]` - ); - } - - const invalidTags = input.filter((t) => !validTag(t)); - if (invalidTags.length) { - throw new Error( - `Invalid theme tags [${invalidTags.join(', ')}], options: [${ALL_THEMES.join(', ')}]` - ); - } - - return tags(...input); -} diff --git a/packages/kbn-optimizer/src/common/worker_config.ts b/packages/kbn-optimizer/src/common/worker_config.ts index 8881d2354740b..89a074293d998 100644 --- a/packages/kbn-optimizer/src/common/worker_config.ts +++ b/packages/kbn-optimizer/src/common/worker_config.ts @@ -10,7 +10,7 @@ import Path from 'path'; import { UnknownVals } from './ts_helpers'; -import { ThemeTags, parseThemeTags } from './theme_tags'; +import { ThemeTags, parseThemeTags } from '@kbn/core-ui-settings-common'; export interface WorkerConfig { readonly repoRoot: string; diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts index 3173ef2a05980..2bb810f45d240 100644 --- a/packages/kbn-optimizer/src/log_optimizer_state.ts +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -10,11 +10,12 @@ import { inspect } from 'util'; import { ToolingLog } from '@kbn/tooling-log'; +import { hasNonDefaultThemeTags } from '@kbn/core-ui-settings-common'; import { tap } from 'rxjs'; import { OptimizerConfig } from './optimizer'; import { OptimizerUpdate$ } from './run_optimizer'; -import { CompilerMsg, pipeClosure, ALL_THEMES } from './common'; +import { CompilerMsg, pipeClosure } from './common'; export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { return pipeClosure((update$: OptimizerUpdate$) => { @@ -80,9 +81,9 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { ); } - if (config.themeTags.length !== ALL_THEMES.length) { + if (hasNonDefaultThemeTags(config.themeTags)) { log.warning( - `only building [${config.themeTags}] themes, customize with the KBN_OPTIMIZER_THEMES environment variable` + `running with non-default [${config.themeTags}] set of themes, customize with the KBN_OPTIMIZER_THEMES environment variable` ); } } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index a3329dcc3d57f..5fd2318953a8c 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -8,10 +8,10 @@ */ jest.mock('@kbn/repo-packages'); +jest.mock('@kbn/core-ui-settings-common'); jest.mock('./assign_bundles_to_workers'); jest.mock('./kibana_platform_plugins'); jest.mock('./get_plugin_bundles'); -jest.mock('../common/theme_tags'); jest.mock('./filter_by_id'); jest.mock('./focus_bundles'); jest.mock('../limits'); @@ -29,7 +29,7 @@ import { REPO_ROOT } from '@kbn/repo-info'; import { createAbsolutePathSerializer } from '@kbn/jest-serializers'; import { OptimizerConfig, ParsedOptions } from './optimizer_config'; -import { parseThemeTags } from '../common'; +import { parseThemeTags } from '@kbn/core-ui-settings-common'; expect.addSnapshotSerializer(createAbsolutePathSerializer()); diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b09650c0708da..1b04a6fbd25a3 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -10,16 +10,9 @@ import Path from 'path'; import Os from 'os'; import { getPackages, getPluginPackagesFilter, type PluginSelector } from '@kbn/repo-packages'; +import { ThemeTag, ThemeTags, parseThemeTags } from '@kbn/core-ui-settings-common'; -import { - Bundle, - WorkerConfig, - CacheableWorkerConfig, - ThemeTag, - ThemeTags, - parseThemeTags, - omit, -} from '../common'; +import { Bundle, WorkerConfig, CacheableWorkerConfig, omit } from '../common'; import { toKibanaPlatformPlugin, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; diff --git a/packages/kbn-optimizer/src/worker/theme_loader.ts b/packages/kbn-optimizer/src/worker/theme_loader.ts index 92a728f17f5cb..f53560692b9ba 100644 --- a/packages/kbn-optimizer/src/worker/theme_loader.ts +++ b/packages/kbn-optimizer/src/worker/theme_loader.ts @@ -9,12 +9,7 @@ import { stringifyRequest, getOptions } from 'loader-utils'; import webpack from 'webpack'; -import { parseThemeTags, ALL_THEMES, ThemeTag } from '../common'; - -const getVersion = (tag: ThemeTag) => 8; -const getIsDark = (tag: ThemeTag) => tag.includes('dark'); -const compare = (a: ThemeTag, b: ThemeTag) => - (getVersion(a) === getVersion(b) ? 1 : 0) + (getIsDark(a) === getIsDark(b) ? 1 : 0); +import { FALLBACK_THEME_TAG, parseThemeTags } from '@kbn/core-ui-settings-common'; // eslint-disable-next-line import/no-default-export export default function (this: webpack.loader.LoaderContext) { @@ -24,26 +19,15 @@ export default function (this: webpack.loader.LoaderContext) { const bundleId = options.bundleId as string; const themeTags = parseThemeTags(options.themeTags); - const cases = ALL_THEMES.map((tag) => { - if (themeTags.includes(tag)) { - return ` - case '${tag}': - return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});`; - } - - const fallback = themeTags - .slice() - .sort((a, b) => compare(b, tag) - compare(a, tag)) - .shift()!; - - const message = `SASS files in [${bundleId}] were not built for theme [${tag}]. Styles were compiled using the [${fallback}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [${tag}] in a comma separated list of themes you want to compile. You can also set it to "*" to build all themes.`; - return ` - case '${tag}': - console.error(new Error(${JSON.stringify(message)})); - return require(${stringifyRequest(this, `${this.resourcePath}?${fallback}`)})`; - }).join('\n'); - return ` -switch (window.__kbnThemeTag__) {${cases} +switch (window.__kbnThemeTag__) { +${themeTags.map( + (tag) => ` + case '${tag}': + return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});` +).join('\n')} + default: + console.error(new Error("SASS files in [${bundleId}] were not built for theme [" + window.__kbnThemeTag__ + "]. Styles were compiled using the [${FALLBACK_THEME_TAG}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [" + window.__kbnThemeTag__ + "] in a comma-separated list of themes you want to compile. You can also set it to '*' to build all themes.")); + return require(${stringifyRequest(this, `${this.resourcePath}?${FALLBACK_THEME_TAG}`)}); }`; } diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index f8cb993537be7..d6e79f66a561a 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -18,6 +18,7 @@ "kbn_references": [ "@kbn/config-schema", "@kbn/dev-utils", + "@kbn/core-ui-settings-common", "@kbn/optimizer-webpack-helpers", "@kbn/std", "@kbn/ui-shared-deps-npm",