diff --git a/packages/core/base/core-base-common/BUILD.bazel b/packages/core/base/core-base-common/BUILD.bazel new file mode 100644 index 0000000000000..30c3b1ae616f4 --- /dev/null +++ b/packages/core/base/core-base-common/BUILD.bazel @@ -0,0 +1,35 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + "@npm//react", + "@npm//tslib", +] + +js_library( + name = "core-base-common", + package_name = "@kbn/core-base-common", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts index 804134cabd4b9..68fa84022ce34 100644 --- a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts @@ -57,6 +57,7 @@ const createSetupContractMock = () => { setupContract.getPlugins.mockReturnValue([]); setupContract.getTheme.mockReturnValue({ darkMode: false, + name: 'amsterdam', version: 'v8', stylesheetPaths: { default: ['light-1.css'], diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts index 1ee75dbfc0d5d..e988420720900 100644 --- a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts @@ -41,6 +41,7 @@ export interface InjectedMetadataExternalUrlPolicy { /** @internal */ export interface InjectedMetadataTheme { darkMode: DarkModeValue; + name: string; version: ThemeVersion; stylesheetPaths: { default: string[]; diff --git a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap index c858b6a8470d2..8b1dc7ef53e15 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap +++ b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap @@ -68,6 +68,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -149,6 +150,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -234,6 +236,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -315,6 +318,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -396,6 +400,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -481,6 +486,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -562,6 +568,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -643,6 +650,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -724,6 +732,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -814,6 +823,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -895,6 +905,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -985,6 +996,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1071,6 +1083,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1152,6 +1165,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1242,6 +1256,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1328,6 +1343,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1414,6 +1430,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1502,6 +1519,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts index 597e4159e4cc7..25d7e241325f3 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts @@ -34,6 +34,18 @@ const createPackageInfo = (parts: Partial = {}): PackageInfo => ({ ...parts, }); +const getClientGetMockImplementation = + ({ darkMode, name }: { darkMode?: boolean; name?: string } = {}) => + (key: string) => { + switch (key) { + case 'theme:darkMode': + return Promise.resolve(darkMode ?? false); + case 'theme:name': + return Promise.resolve(name ?? 'amsterdam'); + } + return Promise.resolve(); + }; + const createUiPlugins = (): UiPlugins => ({ public: new Map(), internal: new Map(), @@ -59,6 +71,7 @@ describe('bootstrapRenderer', () => { getPluginsBundlePathsMock.mockReturnValue(new Map()); renderTemplateMock.mockReturnValue('__rendered__'); getJsDependencyPathsMock.mockReturnValue([]); + uiSettingsClient.get.mockImplementation(getClientGetMockImplementation()); renderer = bootstrapRendererFactory({ auth, @@ -91,13 +104,17 @@ describe('bootstrapRenderer', () => { uiSettingsClient, }); - expect(uiSettingsClient.get).toHaveBeenCalledTimes(1); + expect(uiSettingsClient.get).toHaveBeenCalledTimes(2); expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode'); + expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:name'); }); it('calls getThemeTag with the values from the UiSettingsClient (true/dark) when the UserSettingsService is not provided', async () => { - uiSettingsClient.get.mockResolvedValue(true); - + uiSettingsClient.get.mockImplementation( + getClientGetMockImplementation({ + darkMode: true, + }) + ); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -107,13 +124,13 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); it('calls getThemeTag with the values from the UiSettingsClient (false/light) when the UserSettingsService is not provided', async () => { - uiSettingsClient.get.mockResolvedValue(false); + uiSettingsClient.get.mockImplementation(getClientGetMockImplementation({})); const request = httpServerMock.createKibanaRequest(); @@ -124,7 +141,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); @@ -150,7 +167,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); @@ -166,7 +183,6 @@ describe('bootstrapRenderer', () => { userSettingsService, }); - uiSettingsClient.get.mockResolvedValue(true); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -176,7 +192,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); @@ -192,7 +208,6 @@ describe('bootstrapRenderer', () => { userSettingsService, }); - uiSettingsClient.get.mockResolvedValue(false); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -202,7 +217,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); @@ -218,7 +233,11 @@ describe('bootstrapRenderer', () => { userSettingsService, }); - uiSettingsClient.get.mockResolvedValue(true); + uiSettingsClient.get.mockImplementation( + getClientGetMockImplementation({ + darkMode: true, + }) + ); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -228,7 +247,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); @@ -250,12 +269,17 @@ describe('bootstrapRenderer', () => { uiSettingsClient, }); - expect(uiSettingsClient.get).toHaveBeenCalledTimes(1); + expect(uiSettingsClient.get).toHaveBeenCalledTimes(2); expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode'); + expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:name'); }); it('calls getThemeTag with the correct parameters', async () => { - uiSettingsClient.get.mockResolvedValue(true); + uiSettingsClient.get.mockImplementation( + getClientGetMockImplementation({ + darkMode: true, + }) + ); const request = httpServerMock.createKibanaRequest(); @@ -266,7 +290,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); @@ -283,7 +307,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'system', darkMode: false, }); }); @@ -318,7 +342,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts index 8aa0d2a6c0387..5b8c267532d0b 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts @@ -9,7 +9,6 @@ import { createHash } from 'crypto'; import { PackageInfo } from '@kbn/config'; -import { ThemeVersion } from '@kbn/ui-shared-deps-npm'; import type { KibanaRequest, HttpAuth } from '@kbn/core-http-server'; import { type DarkModeValue, parseDarkModeValue } from '@kbn/core-ui-settings-common'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; @@ -59,7 +58,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ return async function bootstrapRenderer({ uiSettingsClient, request, isAnonymousPage = false }) { let darkMode: DarkModeValue = false; - const themeVersion: ThemeVersion = 'v8'; + let themeName: string = 'amsterdam'; try { const authenticated = isAuthenticated(request); @@ -72,6 +71,8 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ } else { darkMode = parseDarkModeValue(await uiSettingsClient.get('theme:darkMode')); } + + themeName = await uiSettingsClient.get('theme:name'); } } catch (e) { // just use the default values in case of connectivity issues with ES @@ -83,7 +84,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ } const themeTag = getThemeTag({ - themeVersion, + name: !themeName || themeName === 'amsterdam' ? 'v8' : themeName, darkMode, }); const bundlesHref = getBundlesHref(baseHref); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts index 0f9839e8cda89..216e87269818b 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts @@ -10,18 +10,18 @@ import { getThemeTag } from './get_theme_tag'; describe('getThemeTag', () => { - it('returns the correct value for version:v8 and darkMode:false', () => { + it('returns the correct value for name:v8 and darkMode:false', () => { expect( getThemeTag({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }) ).toEqual('v8light'); }); - it('returns the correct value for version:v8 and darkMode:true', () => { + it('returns the correct value for name:v8 and darkMode:true', () => { expect( getThemeTag({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }) ).toEqual('v8dark'); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts index 97f5c17ef240b..f89bd41404633 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts @@ -7,18 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; - /** * Computes the themeTag that will be used on the client-side as `__kbnThemeTag__` * @see `packages/kbn-ui-shared-deps-src/theme.ts` */ -export const getThemeTag = ({ - themeVersion, - darkMode, -}: { - themeVersion: ThemeVersion; - darkMode: boolean; -}) => { - return `${themeVersion}${darkMode ? 'dark' : 'light'}`; +export const getThemeTag = ({ name, darkMode }: { name: string; darkMode: boolean }) => { + return `${name}${darkMode ? 'dark' : 'light'}`; }; diff --git a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx index ace0399f242af..a92c3dac485b5 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx +++ b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx @@ -22,6 +22,7 @@ import type { CustomBranding } from '@kbn/core-custom-branding-common'; import { type DarkModeValue, parseDarkModeValue, + parseThemeNameValue, type UiSettingsParams, type UserProvidedValues, } from '@kbn/core-ui-settings-common'; @@ -211,6 +212,8 @@ export class RenderingService { darkMode = getSettingValue('theme:darkMode', settings, parseDarkModeValue); } + const themeName = getSettingValue('theme:name', settings, parseThemeNameValue); + const themeStylesheetPaths = (mode: boolean) => getThemeStylesheetPaths({ darkMode: mode, @@ -274,6 +277,7 @@ export class RenderingService { }, theme: { darkMode, + name: themeName, version: themeVersion, stylesheetPaths: { default: themeStylesheetPaths(false), diff --git a/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx b/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx index 3f4aebe797172..a3e4516b07510 100644 --- a/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx +++ b/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx @@ -50,7 +50,7 @@ describe('CoreThemeProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: CoreTheme = { darkMode: true }; + const coreTheme: CoreTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( @@ -64,7 +64,7 @@ describe('CoreThemeProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -77,7 +77,7 @@ describe('CoreThemeProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts b/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts index 45c90d90d522a..575d98fe40c8d 100644 --- a/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts +++ b/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts @@ -45,6 +45,7 @@ describe('ThemeService', () => { beforeEach(() => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: false, stylesheetPaths: { dark: ['dark-1.css'], @@ -58,6 +59,7 @@ describe('ThemeService', () => { const theme = await firstValueFrom(theme$); expect(theme).toEqual({ darkMode: false, + name: 'amsterdam', }); }); @@ -88,6 +90,7 @@ describe('ThemeService', () => { beforeEach(() => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: true, stylesheetPaths: { dark: ['dark-1.css'], @@ -101,6 +104,7 @@ describe('ThemeService', () => { const theme = await firstValueFrom(theme$); expect(theme).toEqual({ darkMode: true, + name: 'amsterdam', }); }); @@ -131,6 +135,7 @@ describe('ThemeService', () => { beforeEach(() => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: 'system', stylesheetPaths: { dark: ['dark-1.css'], @@ -150,6 +155,7 @@ describe('ThemeService', () => { expect(theme).toEqual({ darkMode: false, + name: 'amsterdam', }); expect(window.__kbnThemeTag__).toEqual('v8light'); @@ -177,6 +183,7 @@ describe('ThemeService', () => { expect(theme).toEqual({ darkMode: false, + name: 'amsterdam', }); expect(window.__kbnThemeTag__).toEqual('v8light'); @@ -196,6 +203,7 @@ describe('ThemeService', () => { expect(theme).toEqual({ darkMode: true, + name: 'amsterdam', }); expect(window.__kbnThemeTag__).toEqual('v8dark'); @@ -244,6 +252,7 @@ describe('ThemeService', () => { it('exposes a `theme$` observable with the values provided by the injected metadata', async () => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: true, stylesheetPaths: { dark: [], @@ -255,6 +264,7 @@ describe('ThemeService', () => { const theme = await firstValueFrom(theme$); expect(theme).toEqual({ darkMode: true, + name: 'amsterdam', }); }); }); diff --git a/packages/core/theme/core-theme-browser-internal/src/theme_service.ts b/packages/core/theme/core-theme-browser-internal/src/theme_service.ts index 7bc51c9a0c34a..e79a19550bb8d 100644 --- a/packages/core/theme/core-theme-browser-internal/src/theme_service.ts +++ b/packages/core/theme/core-theme-browser-internal/src/theme_service.ts @@ -28,16 +28,21 @@ export class ThemeService { public setup({ injectedMetadata }: ThemeServiceSetupDeps): ThemeServiceSetup { const themeMetadata = injectedMetadata.getTheme(); + this.themeMetadata = themeMetadata; - let theme: CoreTheme; + let darkMode: boolean; if (themeMetadata.darkMode === 'system' && browsersSupportsSystemTheme()) { - theme = { darkMode: systemThemeIsDark() }; + darkMode = systemThemeIsDark(); } else { - const darkMode = themeMetadata.darkMode === 'system' ? false : themeMetadata.darkMode; - theme = { darkMode }; + darkMode = themeMetadata.darkMode === 'system' ? false : themeMetadata.darkMode; } + const theme: CoreTheme = { + darkMode, + name: themeMetadata.name, + }; + this.applyTheme(theme); this.contract = { @@ -73,11 +78,13 @@ export class ThemeService { }); _setDarkMode(darkMode); - updateKbnThemeTag(darkMode); + updateKbnThemeTag(theme); } } -const updateKbnThemeTag = (darkMode: boolean) => { +const updateKbnThemeTag = (theme: CoreTheme) => { + const name = theme.name === 'amsterdam' ? 'v8' : theme.name; + const globals: any = typeof window === 'undefined' ? {} : window; - globals.__kbnThemeTag__ = darkMode ? 'v8dark' : 'v8light'; + globals.__kbnThemeTag__ = `${name}${theme.darkMode ? 'dark' : 'light'}`; }; diff --git a/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts b/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts index beee2320d7cca..e3d2b66645794 100644 --- a/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts +++ b/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts @@ -14,6 +14,7 @@ import type { ThemeService } from '@kbn/core-theme-browser-internal'; const mockTheme: CoreTheme = { darkMode: false, + name: 'amsterdam', }; const createThemeMock = (): CoreTheme => { diff --git a/packages/core/theme/core-theme-browser/src/types.ts b/packages/core/theme/core-theme-browser/src/types.ts index 161758ec362f3..365cde9f814ac 100644 --- a/packages/core/theme/core-theme-browser/src/types.ts +++ b/packages/core/theme/core-theme-browser/src/types.ts @@ -17,6 +17,10 @@ import { Observable } from 'rxjs'; export interface CoreTheme { /** is dark mode enabled or not */ readonly darkMode: boolean; + /** + * Name of the active theme + */ + readonly name: string; } /** 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 b7adb288008df..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,5 +17,18 @@ export type { GetUiSettingsContext, } from './src/ui_settings'; export { type DarkModeValue, parseDarkModeValue } from './src/dark_mode'; +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..7bf9cbfe9486a --- /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-server-internal/src/settings/index.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts index f74977af04b8b..093b4eef9a6de 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts @@ -18,6 +18,7 @@ import { getAnnouncementsSettings } from './announcements'; interface GetCoreSettingsOptions { isDist?: boolean; + isThemeSwitcherEnabled?: boolean; } export const getCoreSettings = ( 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 5701694f97abc..36324f951952e 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,11 @@ 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 +23,7 @@ function getThemeInfo(options: GetThemeSettingsOptions) { }; } - const themeTags = parseThemeTags(); + const themeTags = parseThemeTags(process.env.KBN_OPTIMIZER_THEMES); return { defaultDarkMode: themeTags[0].endsWith('dark'), }; @@ -35,6 +31,7 @@ function getThemeInfo(options: GetThemeSettingsOptions) { interface GetThemeSettingsOptions { isDist?: boolean; + isThemeSwitcherEnabled?: boolean; } export const getThemeSettings = ( @@ -89,5 +86,34 @@ export const getThemeSettings = ( readonly: true, schema: schema.literal('v8'), }, + /** + * Theme name is the (upcoming) replacement for theme versions. + */ + 'theme:name': { + name: i18n.translate('core.ui_settings.params.themeName', { + defaultMessage: 'Theme', + }), + type: 'select', + 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') + ? !options.isThemeSwitcherEnabled + : true, + requiresPageReload: true, + schema: schema.oneOf([ + schema.literal('amsterdam'), + schema.literal('borealis'), + // Allow experimental themes + schema.string(), + ]), + }, }; }; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts index 6563ffff78949..04b7ff6b0f558 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts @@ -19,6 +19,11 @@ const deprecations: ConfigDeprecationProvider = ({ unused, renameFromRoot }) => const configSchema = schema.object({ overrides: schema.object({}, { unknowns: 'allow' }), publicApiEnabled: offeringBasedSchema({ serverless: schema.boolean({ defaultValue: false }) }), + experimental: schema.maybe( + schema.object({ + themeSwitcherEnabled: schema.maybe(schema.boolean({ defaultValue: false })), + }) + ), }); export type UiSettingsConfigType = TypeOf; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts index 958391b5fc725..70c880c85594f 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts @@ -68,10 +68,15 @@ export class UiSettingsService public async preboot(): Promise { this.log.debug('Prebooting ui settings service'); - const { overrides } = await firstValueFrom(this.config$); + const { overrides, experimental } = await firstValueFrom(this.config$); this.overrides = overrides; - this.register(getCoreSettings({ isDist: this.isDist })); + this.register( + getCoreSettings({ + isDist: this.isDist, + isThemeSwitcherEnabled: experimental?.themeSwitcherEnabled, + }) + ); return { createDefaultsClient: () => diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index bc0f7206a2835..9a7c95917878a 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -45,6 +45,7 @@ export const SHORT_DOTS_ENABLE_ID = 'shortDots:enable'; export const SORT_OPTIONS_ID = 'sort:options'; export const STATE_STORE_IN_SESSION_STORAGE_ID = 'state:storeInSessionStorage'; export const THEME_DARK_MODE_ID = 'theme:darkMode'; +export const THEME_NAME_ID = 'theme:name'; export const TIMEPICKER_QUICK_RANGES_ID = 'timepicker:quickRanges'; export const TIMEPICKER_REFRESH_INTERVAL_DEFAULTS_ID = 'timepicker:refreshIntervalDefaults'; export const TIMEPICKER_TIME_DEFAULTS_ID = 'timepicker:timeDefaults'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8e1cd3e8fb7a1..58424700d9bf6 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -2,7 +2,7 @@ pageLoadAssetSize: actions: 20000 advancedSettings: 27596 aiAssistantManagementSelection: 19146 - aiops: 16526 + aiops: 16600 alerting: 106936 apm: 64385 banners: 17946 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..6a61c3a99af07 100644 --- a/packages/kbn-optimizer/src/common/worker_config.ts +++ b/packages/kbn-optimizer/src/common/worker_config.ts @@ -9,8 +9,8 @@ import Path from 'path'; +import { ThemeTags, parseThemeTags } from '@kbn/core-ui-settings-common'; import { UnknownVals } from './ts_helpers'; -import { ThemeTags, parseThemeTags } from './theme_tags'; 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_cache_key.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts index 6d520836ac4b2..f08849005e971 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts @@ -90,8 +90,8 @@ describe('getOptimizerCacheKey()', () => { "optimizerCacheKey": "♻", "repoRoot": , "themeTags": Array [ - "v8dark", "v8light", + "v8dark", ], }, } 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..3bce12d94e974 100644 --- a/packages/kbn-optimizer/src/worker/theme_loader.ts +++ b/packages/kbn-optimizer/src/worker/theme_loader.ts @@ -9,12 +9,11 @@ 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, + hasNonDefaultThemeTags, +} from '@kbn/core-ui-settings-common'; // eslint-disable-next-line import/no-default-export export default function (this: webpack.loader.LoaderContext) { @@ -23,27 +22,34 @@ export default function (this: webpack.loader.LoaderContext) { const options = getOptions(this); 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'); + const isFallbackNeeded = hasNonDefaultThemeTags(themeTags); + + /** + * The following piece of code generates a `switch` statement that gets injected into the output + * bundle for all `.scss` file imports. The generated `switch` contains: + * - a `case` clause for each of the bundled theme tags, + * - an optional `default` clause for the theme fallback logic, included when some of the default + * Kibana theme tags are omitted and the fallback logic might be needed. The fallback logic + * should never have to run and is added as an extra precaution layer. + */ + + let defaultClause = ''; + if (isFallbackNeeded) { + defaultClause = ` + 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}`)});`; + } return ` -switch (window.__kbnThemeTag__) {${cases} +switch (window.__kbnThemeTag__) { +${themeTags + .map( + (tag) => ` + case '${tag}': + return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});` + ) + .join('\n')} + ${defaultClause} }`; } 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", diff --git a/packages/kbn-storybook/src/lib/decorators.tsx b/packages/kbn-storybook/src/lib/decorators.tsx index d6b3c6abf2f40..270da371172eb 100644 --- a/packages/kbn-storybook/src/lib/decorators.tsx +++ b/packages/kbn-storybook/src/lib/decorators.tsx @@ -21,7 +21,7 @@ import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root'; import { i18n } from '@kbn/i18n'; -const theme$ = new BehaviorSubject({ darkMode: false }); +const theme$ = new BehaviorSubject({ darkMode: false, name: 'amsterdam' }); const i18nStart: I18nStart = { Context: ({ children }) => {children}, @@ -43,7 +43,7 @@ const KibanaContextDecorator: DecoratorFn = (storyFn, { globals }) => { const colorMode = globals.euiTheme === 'v8.dark' ? 'dark' : 'light'; useEffect(() => { - theme$.next({ darkMode: colorMode === 'dark' }); + theme$.next({ darkMode: colorMode === 'dark', name: 'amsterdam' }); }, [colorMode]); return ( diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel index d2e67ccd14ac6..b0d7bb65843d9 100644 --- a/packages/kbn-ui-shared-deps-src/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel @@ -39,6 +39,7 @@ webpack_cli( "//packages/shared-ux/error_boundary", "//packages/kbn-rison", "//packages/shared-ux/code_editor/impl:code_editor", + "//packages/react/kibana_context/theme", ], output_dir = True, args = [ diff --git a/packages/kbn-ui-shared-deps-src/src/definitions.js b/packages/kbn-ui-shared-deps-src/src/definitions.js index f56baf1731ac9..2c391dd18d1bc 100644 --- a/packages/kbn-ui-shared-deps-src/src/definitions.js +++ b/packages/kbn-ui-shared-deps-src/src/definitions.js @@ -105,6 +105,7 @@ const externals = { '@kbn/esql-ast': '__kbnSharedDeps__.KbnEsqlAst', '@kbn/ebt-tools': '__kbnSharedDeps__.KbnEbtTools', '@elastic/apm-rum-core': '__kbnSharedDeps__.ElasticApmRumCore', + '@kbn/react-kibana-context-theme': '__kbnSharedDeps__.KbnReactKibanaContextTheme', }; module.exports = { distDir, jsFilename, cssDistFilename, externals }; diff --git a/packages/kbn-ui-shared-deps-src/src/entry.js b/packages/kbn-ui-shared-deps-src/src/entry.js index f87c2e7d75ead..25432fcfe399e 100644 --- a/packages/kbn-ui-shared-deps-src/src/entry.js +++ b/packages/kbn-ui-shared-deps-src/src/entry.js @@ -78,3 +78,4 @@ export const KbnCodeEditor = require('@kbn/code-editor'); export const KbnEsqlAst = require('@kbn/esql-ast'); export const KbnEbtTools = require('@kbn/ebt-tools'); export const ElasticApmRumCore = require('@elastic/apm-rum-core'); +export const KbnReactKibanaContextTheme = require('@kbn/react-kibana-context-theme'); diff --git a/packages/kbn-ui-theme/src/theme.ts b/packages/kbn-ui-theme/src/theme.ts index 0d8f7fb789c2a..3718c7dfb9d64 100644 --- a/packages/kbn-ui-theme/src/theme.ts +++ b/packages/kbn-ui-theme/src/theme.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +// TODO(tkajtoch): Add support for multiple themes + /* eslint-disable-next-line @kbn/eslint/module_migration */ import { default as v8Light } from '@elastic/eui/dist/eui_theme_light.json'; /* eslint-disable-next-line @kbn/eslint/module_migration */ diff --git a/packages/kbn-unified-data-table/__mocks__/services.ts b/packages/kbn-unified-data-table/__mocks__/services.ts index c4bafef861afb..8a3f9568ba5e9 100644 --- a/packages/kbn-unified-data-table/__mocks__/services.ts +++ b/packages/kbn-unified-data-table/__mocks__/services.ts @@ -43,7 +43,7 @@ export function createServicesMock() { ...uiSettingsMock, }; - const theme = themeServiceMock.createSetupContract({ darkMode: false }); + const theme = themeServiceMock.createSetupContract({ darkMode: false, name: 'amsterdam' }); corePluginMock.theme = theme; const dataPlugin = dataPluginMock.createStartContract(); diff --git a/packages/react/kibana_context/common/BUILD.bazel b/packages/react/kibana_context/common/BUILD.bazel new file mode 100644 index 0000000000000..43f20a833d07f --- /dev/null +++ b/packages/react/kibana_context/common/BUILD.bazel @@ -0,0 +1,36 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + "@npm//react", + "@npm//tslib", + "@npm//@elastic/eui", +] + +js_library( + name = "common", + package_name = "@kbn/react-kibana-context-common", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/react/kibana_context/common/color_mode.test.ts b/packages/react/kibana_context/common/color_mode.test.ts index 2062f628b54ae..8fcd75f49d45a 100644 --- a/packages/react/kibana_context/common/color_mode.test.ts +++ b/packages/react/kibana_context/common/color_mode.test.ts @@ -11,10 +11,10 @@ import { getColorMode } from './color_mode'; describe('getColorMode', () => { it('returns the correct `colorMode` when `darkMode` is enabled', () => { - expect(getColorMode({ darkMode: true })).toEqual('DARK'); + expect(getColorMode({ name: 'amsterdam', darkMode: true })).toEqual('DARK'); }); it('returns the correct `colorMode` when `darkMode` is disabled', () => { - expect(getColorMode({ darkMode: false })).toEqual('LIGHT'); + expect(getColorMode({ name: 'amsterdam', darkMode: false })).toEqual('LIGHT'); }); }); diff --git a/packages/react/kibana_context/common/index.ts b/packages/react/kibana_context/common/index.ts index 77edac36e7912..541e804b788c7 100644 --- a/packages/react/kibana_context/common/index.ts +++ b/packages/react/kibana_context/common/index.ts @@ -8,6 +8,7 @@ */ export { getColorMode } from './color_mode'; +export { getThemeConfigByName, DEFAULT_THEME_CONFIG, type ThemeConfig } from './theme'; export type { KibanaTheme, ThemeServiceStart } from './types'; import type { KibanaTheme } from './types'; @@ -18,4 +19,5 @@ import type { KibanaTheme } from './types'; */ export const defaultTheme: KibanaTheme = { darkMode: false, + name: 'amsterdam', }; diff --git a/packages/react/kibana_context/common/kibana.jsonc b/packages/react/kibana_context/common/kibana.jsonc index 90fde56f05df6..b52bc6a40d0cc 100644 --- a/packages/react/kibana_context/common/kibana.jsonc +++ b/packages/react/kibana_context/common/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-browser", + "type": "shared-common", "id": "@kbn/react-kibana-context-common", "owner": "@elastic/appex-sharedux" } diff --git a/packages/react/kibana_context/common/theme.ts b/packages/react/kibana_context/common/theme.ts new file mode 100644 index 0000000000000..45c89172cf187 --- /dev/null +++ b/packages/react/kibana_context/common/theme.ts @@ -0,0 +1,26 @@ +/* + * 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 { EuiThemeSystem, EuiThemeAmsterdam } from '@elastic/eui'; + +export interface ThemeConfig { + euiTheme: EuiThemeSystem; +} + +const THEMES: Record = { + amsterdam: { + euiTheme: EuiThemeAmsterdam, + }, +}; + +export const getThemeConfigByName = (name: string): ThemeConfig | null => { + return THEMES[name as keyof typeof THEMES] || null; +}; + +export const DEFAULT_THEME_CONFIG = THEMES.amsterdam; diff --git a/packages/react/kibana_context/common/types.ts b/packages/react/kibana_context/common/types.ts index 2118c797d8be6..05e7ab7a0c7d5 100644 --- a/packages/react/kibana_context/common/types.ts +++ b/packages/react/kibana_context/common/types.ts @@ -20,6 +20,10 @@ import { Observable } from 'rxjs'; export interface KibanaTheme { /** is dark mode enabled or not */ readonly darkMode: boolean; + /** + * Name of the active theme + */ + readonly name: string; } // To avoid a circular dependency with the deprecation of `CoreThemeProvider`, diff --git a/packages/react/kibana_context/root/BUILD.bazel b/packages/react/kibana_context/root/BUILD.bazel new file mode 100644 index 0000000000000..1c47c25bc20a9 --- /dev/null +++ b/packages/react/kibana_context/root/BUILD.bazel @@ -0,0 +1,37 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + "@npm//react", + "@npm//tslib", + "@npm//@elastic/eui", + "//packages/core/base/core-base-common", +] + +js_library( + name = "root", + package_name = "@kbn/react-kibana-context-root", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/react/kibana_context/root/eui_provider.test.tsx b/packages/react/kibana_context/root/eui_provider.test.tsx index 069954b09fc60..d7486be2d4798 100644 --- a/packages/react/kibana_context/root/eui_provider.test.tsx +++ b/packages/react/kibana_context/root/eui_provider.test.tsx @@ -54,7 +54,7 @@ describe('KibanaEuiProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: KibanaTheme = { darkMode: true }; + const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( @@ -70,7 +70,7 @@ describe('KibanaEuiProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -83,7 +83,7 @@ describe('KibanaEuiProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/react/kibana_context/root/eui_provider.tsx b/packages/react/kibana_context/root/eui_provider.tsx index 74b2f6d8605ab..1e4e45c9f36f1 100644 --- a/packages/react/kibana_context/root/eui_provider.tsx +++ b/packages/react/kibana_context/root/eui_provider.tsx @@ -13,7 +13,12 @@ import createCache from '@emotion/cache'; import { EuiProvider, EuiProviderProps, euiStylisPrefixer } from '@elastic/eui'; import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common'; -import { getColorMode, defaultTheme } from '@kbn/react-kibana-context-common'; +import { + getColorMode, + defaultTheme, + getThemeConfigByName, + DEFAULT_THEME_CONFIG, +} from '@kbn/react-kibana-context-common'; import { ThemeServiceStart } from '@kbn/react-kibana-context-common'; /** @@ -64,8 +69,13 @@ export const KibanaEuiProvider: FC> = modify, children, }) => { - const theme = useObservable(theme$, defaultTheme); - const themeColorMode = useMemo(() => getColorMode(theme), [theme]); + const kibanaTheme = useObservable(theme$, defaultTheme); + const themeColorMode = useMemo(() => getColorMode(kibanaTheme), [kibanaTheme]); + + const theme = useMemo(() => { + const config = getThemeConfigByName(kibanaTheme.name) || DEFAULT_THEME_CONFIG; + return config.euiTheme; + }, [kibanaTheme.name]); // In some cases-- like in Storybook or testing-- we want to explicitly override the // colorMode provided by the `theme`. @@ -76,7 +86,9 @@ export const KibanaEuiProvider: FC> = const globalStyles = globalStylesProp === false ? false : undefined; return ( - + {children} ); diff --git a/packages/react/kibana_context/root/kibana.jsonc b/packages/react/kibana_context/root/kibana.jsonc index 80a029c6ed487..740d92da927c9 100644 --- a/packages/react/kibana_context/root/kibana.jsonc +++ b/packages/react/kibana_context/root/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-browser", + "type": "shared-common", "id": "@kbn/react-kibana-context-root", "owner": "@elastic/appex-sharedux" } diff --git a/packages/react/kibana_context/root/root_provider.test.tsx b/packages/react/kibana_context/root/root_provider.test.tsx index df4bdf14c5c22..919adb09581d5 100644 --- a/packages/react/kibana_context/root/root_provider.test.tsx +++ b/packages/react/kibana_context/root/root_provider.test.tsx @@ -58,7 +58,7 @@ describe('KibanaRootContextProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: KibanaTheme = { darkMode: true }; + const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/react/kibana_context/theme/BUILD.bazel b/packages/react/kibana_context/theme/BUILD.bazel new file mode 100644 index 0000000000000..504477ad7a0ed --- /dev/null +++ b/packages/react/kibana_context/theme/BUILD.bazel @@ -0,0 +1,38 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +BUNDLER_DEPS = [ + "@npm//react", + "@npm//tslib", + "@npm//react-use", + "//packages/react/kibana_context/common", + "//packages/react/kibana_context/root", +] + +js_library( + name = "theme", + package_name = "@kbn/react-kibana-context-theme", + srcs = ["package.json"] + SRCS, + deps = BUNDLER_DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/react/kibana_context/theme/kibana.jsonc b/packages/react/kibana_context/theme/kibana.jsonc index b3f8ba25cc5d3..56ae8b57a6682 100644 --- a/packages/react/kibana_context/theme/kibana.jsonc +++ b/packages/react/kibana_context/theme/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-browser", + "type": "shared-common", "id": "@kbn/react-kibana-context-theme", "owner": "@elastic/appex-sharedux" } diff --git a/packages/react/kibana_context/theme/theme_provider.test.tsx b/packages/react/kibana_context/theme/theme_provider.test.tsx index a20af098d857d..9889da9a689a3 100644 --- a/packages/react/kibana_context/theme/theme_provider.test.tsx +++ b/packages/react/kibana_context/theme/theme_provider.test.tsx @@ -52,7 +52,7 @@ describe('KibanaThemeProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -66,7 +66,10 @@ describe('KibanaThemeProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ + darkMode: true, + name: 'amsterdam', + }); const wrapper = mountWithIntl( @@ -79,7 +82,7 @@ describe('KibanaThemeProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/react/kibana_mount/to_mount_point.test.tsx b/packages/react/kibana_mount/to_mount_point.test.tsx index 2232551877750..50a49263e2532 100644 --- a/packages/react/kibana_mount/to_mount_point.test.tsx +++ b/packages/react/kibana_mount/to_mount_point.test.tsx @@ -41,7 +41,7 @@ describe('toMountPoint', () => { }; it('exposes the euiTheme when `theme$` is provided', async () => { - const theme = { theme$: of({ darkMode: true }) }; + const theme = { theme$: of({ darkMode: true, name: 'amsterdam' }) }; const mount = toMountPoint(, { theme, i18n, analytics }); const targetEl = document.createElement('div'); @@ -53,7 +53,7 @@ describe('toMountPoint', () => { }); it('propagates changes of the theme$ observable', async () => { - const theme$ = new BehaviorSubject({ darkMode: true }); + const theme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const mount = toMountPoint(, { theme: { theme$ }, i18n, analytics }); @@ -65,7 +65,7 @@ describe('toMountPoint', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - theme$.next({ darkMode: false }); + theme$.next({ darkMode: false, name: 'amsterdam' }); }); await flushPromises(); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts index d91f31e243783..01c40ecc53d2d 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts @@ -11,6 +11,6 @@ import { themeServiceMock } from '@kbn/core/public/mocks'; import { ThemeService } from '@kbn/charts-plugin/public/services'; const theme = new ThemeService(); -theme.init(themeServiceMock.createSetupContract({ darkMode: false })); +theme.init(themeServiceMock.createSetupContract({ darkMode: false, name: 'amsterdam' })); export { theme }; diff --git a/src/plugins/charts/public/services/theme/theme.test.tsx b/src/plugins/charts/public/services/theme/theme.test.tsx index ef8594e200dca..ef77405edf27e 100644 --- a/src/plugins/charts/public/services/theme/theme.test.tsx +++ b/src/plugins/charts/public/services/theme/theme.test.tsx @@ -19,7 +19,7 @@ import { ThemeService } from './theme'; import { coreMock } from '@kbn/core/public/mocks'; const createTheme$Mock = (mode: boolean) => { - return from([{ darkMode: mode }]); + return from([{ darkMode: mode, name: 'amsterdam' }]); }; const { theme: setUpMockTheme } = coreMock.createSetup(); @@ -37,6 +37,7 @@ describe('ThemeService', () => { expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toStrictEqual({ darkMode: false, + name: 'amsterdam', }); }); @@ -47,6 +48,7 @@ describe('ThemeService', () => { expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toStrictEqual({ darkMode: true, + name: 'amsterdam', }); }); }); diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index f00d105444630..b00b5a95b3958 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -143,7 +143,7 @@ export function createDiscoverServicesMock(): DiscoverServices { }; const { profilesManagerMock } = createContextAwarenessMocks(); - const theme = themeServiceMock.createSetupContract({ darkMode: false }); + const theme = themeServiceMock.createSetupContract({ darkMode: false, name: 'amsterdam' }); corePluginMock.theme = theme; corePluginMock.chrome.getActiveSolutionNavId$.mockReturnValue(new BehaviorSubject(null)); diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 0de8d47398e51..1732ff51bf87d 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -58,7 +58,7 @@ const setupComponentWithPluginStateMock = async ( }; const setupGuidePanelComponent = async (api: GuidedOnboardingApi) => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); let testBed: TestBed; const GuidePanelComponent = () => ( (undefined); const [guideConfig, setGuideConfig] = useState(undefined); const [isLoading, setIsLoading] = useState(false); - const { darkMode: isDarkTheme } = useObservable(theme$, { darkMode: false }); + const { darkMode: isDarkTheme } = useObservable(theme$, { darkMode: false, name: 'amsterdam' }); const styles = getGuidePanelStyles({ euiThemeContext, isDarkTheme }); diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index ef7820391273b..4d21adeecd377 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -73,7 +73,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => theme, } = services; const addBasePath = http.basePath.prepend; - const currentTheme = useObservable(theme.theme$, { darkMode: false }); + const currentTheme = useObservable(theme.theme$, { darkMode: false, name: 'amsterdam' }); // Home does not have a locator implemented, so hard-code it here. const addDataHref = addBasePath('/app/integrations/browse'); diff --git a/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx b/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx index 4c3e1d91b91b5..95b56ac659603 100644 --- a/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx +++ b/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx @@ -37,7 +37,7 @@ describe('useDarkMode', () => { const mock = (): [KibanaServices, BehaviorSubject] => { const core = coreMock.createStart(); - const subject = new BehaviorSubject({ darkMode: false }); + const subject = new BehaviorSubject({ darkMode: false, name: 'amsterdam' }); core.theme.theme$ = subject.asObservable(); return [core, subject]; @@ -73,7 +73,7 @@ describe('useDarkMode', () => { expect(div!.textContent).toBe('false'); act(() => { - subject.next({ darkMode: true }); + subject.next({ darkMode: true, name: 'amsterdam' }); }); div = container!.querySelector('div'); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index e3374219b6553..c4202cf6f92e2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -272,6 +272,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'theme:name': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, 'state:storeInSessionStorage': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index b49647c3d4791..e4b10f038a75a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -108,6 +108,7 @@ export interface UsageStats { 'timepicker:quickRanges': string; 'theme:version': string; 'theme:darkMode': boolean; + 'theme:name': string; 'state:storeInSessionStorage': boolean; 'savedObjects:perPage': number; 'search:queryLanguage': string; diff --git a/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx index 27aefe1d43488..93511354b19ad 100644 --- a/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx +++ b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx @@ -52,7 +52,7 @@ describe('KibanaThemeProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: CoreTheme = { darkMode: true }; + const coreTheme: CoreTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( @@ -66,7 +66,7 @@ describe('KibanaThemeProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -79,7 +79,7 @@ describe('KibanaThemeProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6a43177492548..78f8b4f2f7b38 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10272,6 +10272,12 @@ "description": "Non-default value of setting." } }, + "theme:name": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, "state:storeInSessionStorage": { "type": "boolean", "_meta": { diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx index 67ed1c8aa6845..373dbc838835f 100644 --- a/x-pack/plugins/fleet/.storybook/context/index.tsx +++ b/x-pack/plugins/fleet/.storybook/context/index.tsx @@ -99,7 +99,7 @@ export const StorybookContext: React.FC<{ settings: getSettings(), theme: { theme$: EMPTY, - getTheme: () => ({ darkMode: false }), + getTheme: () => ({ darkMode: false, name: 'amsterdam' }), }, security: {} as unknown as SecurityServiceStart, userProfile: {} as unknown as UserProfileServiceStart, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 6c4a94d77a871..b32d7456bd2b5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -488,7 +488,10 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ } }, [suggestionForDraggedField, dispatchLens]); - const IS_DARK_THEME: boolean = useObservable(core.theme.theme$, { darkMode: false }).darkMode; + const IS_DARK_THEME: boolean = useObservable(core.theme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; const renderDragDropPrompt = () => { if (chartSizeSpec) { diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx index ec672d20f55da..0633c13b8097a 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx @@ -80,7 +80,10 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const dataGridRef = useRef(null); const isInteractive = props.interactive; - const isDarkMode = useObservable(props.theme.theme$, { darkMode: false }).darkMode; + const isDarkMode = useObservable(props.theme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; const [columnConfig, setColumnConfig] = useState({ columns: props.args.columns, diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 55dea2be2e370..efe5a39458eed 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -465,7 +465,10 @@ export const getDatatableVisualization = ({ }; }, DimensionEditorComponent(props) { - const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + const isDarkMode = useObservable(kibanaTheme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; return ( diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 661921caaa1ef..8d44fec4e96e4 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -494,7 +494,10 @@ export const getPieVisualization = ({ }; }, DimensionEditorComponent(props) { - const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + const isDarkMode = useObservable(kibanaTheme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; return ; }, DimensionEditorDataExtraComponent(props) { diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx index 8d962ef076093..b457926fe373d 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx @@ -294,7 +294,10 @@ export const getTagcloudVisualization = ({ }, DimensionEditorComponent(props) { - const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; if (props.groupId === TAG_GROUP_ID) { return ( l.layerId === props.layerId)!; const dimensionEditor = isReferenceLayer(layer) ? ( diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts index e618e02b1b51e..93260405cce59 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts @@ -10,7 +10,7 @@ import useObservable from 'react-use/lib/useObservable'; import { of } from 'rxjs'; import { useKibanaContextForPlugin } from './use_kibana'; -const themeDefault: CoreTheme = { darkMode: false }; +const themeDefault: CoreTheme = { darkMode: false, name: 'amsterdam' }; export const useIsDarkMode = () => { const { services } = useKibanaContextForPlugin(); diff --git a/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx index dd0f97038740a..482602c87fd06 100644 --- a/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx @@ -53,8 +53,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx index dd0f97038740a..482602c87fd06 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx @@ -53,8 +53,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx index 7ed147138cce3..536cebb21a511 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx @@ -52,8 +52,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx index ea94e2edb6f8b..86e67681ab7af 100644 --- a/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx @@ -55,8 +55,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx index 593c2eafa920e..09740a5be71af 100644 --- a/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx @@ -36,6 +36,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { const mockTheme: CoreTheme = { darkMode: false, + name: 'amsterdam', }; const createTheme$Mock = () => { diff --git a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx index 8b6e951f9c97c..92a9f0b03a35a 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx @@ -27,6 +27,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { const mockTheme: CoreTheme = { darkMode: false, + name: 'amsterdam', }; const createTheme$Mock = () => { diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx index 01d6fc95afcc1..e78ed371468fc 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx @@ -318,7 +318,7 @@ describe('useUserProfileForm', () => { const data: UserProfileData = {}; const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false }); - coreStart.theme.getTheme.mockReturnValue({ darkMode: true }); + coreStart.theme.getTheme.mockReturnValue({ darkMode: true, name: 'amsterdam' }); coreStart.settings.client.isOverridden.mockReturnValue(true); const testWrapper = mount( @@ -354,7 +354,7 @@ describe('useUserProfileForm', () => { const data: UserProfileData = {}; const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false }); - coreStart.theme.getTheme.mockReturnValue({ darkMode: false }); + coreStart.theme.getTheme.mockReturnValue({ darkMode: false, name: 'amsterdam' }); coreStart.settings.client.isOverridden.mockReturnValue(true); const testWrapper = mount( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx index 5d9d126f1940d..12b1162082bfc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx @@ -43,7 +43,7 @@ describe('Modal Inspect', () => { }; const renderModalInspectQuery = () => { - const theme = { theme$: of({ darkMode: false }) }; + const theme = { theme$: of({ darkMode: false, name: 'amsterdam' }) }; return render(, { wrapper: ({ children }) => ( {children}