Enable the new Borealis theme (#210468)

Resolves https://github.com/elastic/eui-private/issues/169

## Summary

This PR makes Borealis the default theme in Serverless (traditional
kibana flavor already uses Borealis as the default) and adds a
`coreRendering.defaultThemeName` LD feature flag to allow a graceful
switch when this code gets deployed next week.

To switch back to Amsterdam when developing locally, set
`feature_flags.overrides.coreRendering.defaultThemeName: amsterdam` in
`kibana.dev.yml`

Please note that `DEFAULT_THEME_TAGS` still includes both Amsterdam and
Borealis. We've decided to keep Amsterdam bundled in case of any
unexpected errors. We'll make Amsterdam opt-in and reduce the bundle
size within the next two weeks (target date Feb 21st).

For the sake of a straightforward review of this PR, I will remove the
previously defined `theme:name` UI setting and `themeSwitcherEnabled`
logic in a follow-up PR.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Tomasz Kajtoch 2025-02-14 09:39:01 +01:00 committed by GitHub
parent 58a2d7d292
commit df6df00979
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 146 additions and 72 deletions

View file

@ -271,9 +271,6 @@ xpack.dataUsage.enabled: true
# This feature is disabled in Serverless until fully tested within a Serverless environment
xpack.dataUsage.enableExperimental: ['dataUsageDisabled']
# Ensure Serverless is using the Amsterdam theme
uiSettings.experimental.defaultTheme: "amsterdam"
# This feature is disabled in Serverless until Inference Endpoint become enabled within a Serverless environment
xpack.stack_connectors.enableExperimental: ['inferenceConnectorOff']

View file

@ -68,7 +68,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -150,7 +150,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -236,7 +236,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -318,7 +318,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -400,7 +400,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -486,7 +486,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -568,7 +568,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -650,7 +650,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -732,7 +732,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -823,7 +823,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -905,7 +905,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -996,7 +996,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -1083,7 +1083,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -1165,7 +1165,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -1256,7 +1256,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -1343,7 +1343,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -1430,7 +1430,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",
@ -1519,7 +1519,7 @@ Object {
"serverBasePath": "/mock-server-basepath",
"theme": Object {
"darkMode": "theme:darkMode",
"name": "theme:name",
"name": "borealis",
"stylesheetPaths": Object {
"dark": Array [
"/style-1.css",

View file

@ -14,6 +14,7 @@ import {
getJsDependencyPathsMock,
} from './bootstrap_renderer.test.mocks';
import { BehaviorSubject } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import { AuthStatus } from '@kbn/core-http-server';
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
@ -21,6 +22,7 @@ import { httpServiceMock, httpServerMock } from '@kbn/core-http-server-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks';
import { bootstrapRendererFactory, BootstrapRenderer } from './bootstrap_renderer';
import { userSettingsServiceMock } from '@kbn/core-user-settings-server-mocks';
import { DEFAULT_THEME_NAME, ThemeName } from '@kbn/core-ui-settings-common';
const createPackageInfo = (parts: Partial<PackageInfo> = {}): PackageInfo => ({
branch: 'master',
@ -37,12 +39,10 @@ const createPackageInfo = (parts: Partial<PackageInfo> = {}): PackageInfo => ({
const getClientGetMockImplementation =
({ darkMode, name }: { darkMode?: boolean | string; name?: string } = {}) =>
(key: string) => {
switch (key) {
case 'theme:darkMode':
return Promise.resolve(darkMode ?? false);
case 'theme:name':
return Promise.resolve(name ?? 'borealis');
if (key === 'theme:darkMode') {
return Promise.resolve(darkMode ?? false);
}
return Promise.resolve();
};
@ -59,6 +59,7 @@ describe('bootstrapRenderer', () => {
let uiPlugins: UiPlugins;
let packageInfo: PackageInfo;
let userSettingsService: ReturnType<typeof userSettingsServiceMock.createSetupContract>;
const themeName$ = new BehaviorSubject<ThemeName>(DEFAULT_THEME_NAME);
beforeEach(() => {
auth = httpServiceMock.createAuth();
@ -78,6 +79,7 @@ describe('bootstrapRenderer', () => {
packageInfo,
uiPlugins,
baseHref: `/base-path/${packageInfo.buildShaShort}`, // the base href as provided by static assets module
themeName$,
});
});
@ -104,9 +106,8 @@ describe('bootstrapRenderer', () => {
uiSettingsClient,
});
expect(uiSettingsClient.get).toHaveBeenCalledTimes(2);
expect(uiSettingsClient.get).toHaveBeenCalledTimes(1);
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 () => {
@ -155,6 +156,7 @@ describe('bootstrapRenderer', () => {
uiPlugins,
baseHref: '/base-path',
userSettingsService,
themeName$,
});
uiSettingsClient.get.mockResolvedValue(false);
@ -181,6 +183,7 @@ describe('bootstrapRenderer', () => {
uiPlugins,
baseHref: '/base-path',
userSettingsService,
themeName$,
});
const request = httpServerMock.createKibanaRequest();
@ -206,6 +209,7 @@ describe('bootstrapRenderer', () => {
uiPlugins,
baseHref: '/base-path',
userSettingsService,
themeName$,
});
const request = httpServerMock.createKibanaRequest();
@ -231,6 +235,7 @@ describe('bootstrapRenderer', () => {
uiPlugins,
baseHref: '/base-path',
userSettingsService,
themeName$,
});
uiSettingsClient.get.mockImplementation(
@ -269,9 +274,8 @@ describe('bootstrapRenderer', () => {
uiSettingsClient,
});
expect(uiSettingsClient.get).toHaveBeenCalledTimes(2);
expect(uiSettingsClient.get).toHaveBeenCalledTimes(1);
expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode');
expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:name');
});
it('calls getThemeTag with the correct parameters', async () => {
@ -350,31 +354,40 @@ describe('bootstrapRenderer', () => {
darkMode: false,
});
});
});
it('calls getThemeTag with `v8` theme name when buildFlavor is `serverless`', async () => {
renderer = bootstrapRendererFactory({
auth,
packageInfo: {
...packageInfo,
buildFlavor: 'serverless',
},
uiPlugins,
baseHref: `/base-path/${packageInfo.buildShaShort}`, // the base href as provided by static assets module
});
it('calls getThemeTag with the correct theme name', async () => {
uiSettingsClient.get.mockImplementation(
getClientGetMockImplementation({
darkMode: true,
})
);
const request = httpServerMock.createKibanaRequest();
const request = httpServerMock.createKibanaRequest();
await renderer({
request,
uiSettingsClient,
});
expect(getThemeTagMock).toHaveBeenCalledTimes(1);
expect(getThemeTagMock).toHaveBeenCalledWith({
name: 'borealis',
darkMode: false,
});
await renderer({
request,
uiSettingsClient,
});
expect(getThemeTagMock).toHaveBeenCalledTimes(1);
expect(getThemeTagMock).toHaveBeenCalledWith(
expect.objectContaining({
name: themeName$.getValue(),
})
);
themeName$.next('amsterdam');
await renderer({
request,
uiSettingsClient,
});
expect(getThemeTagMock).toHaveBeenLastCalledWith(
expect.objectContaining({
// 'amsterdam' is mapped to 'v8' for backwards compatibility
name: 'v8',
})
);
});
[false, true].forEach((isAnonymousPage) => {

View file

@ -8,14 +8,13 @@
*/
import { createHash } from 'crypto';
import { BehaviorSubject } from 'rxjs';
import { PackageInfo } from '@kbn/config';
import type { KibanaRequest, HttpAuth } from '@kbn/core-http-server';
import {
type DarkModeValue,
type ThemeName,
DEFAULT_THEME_NAME,
parseDarkModeValue,
parseThemeNameValue,
} from '@kbn/core-ui-settings-common';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
@ -36,6 +35,7 @@ interface FactoryOptions {
uiPlugins: UiPlugins;
auth: HttpAuth;
userSettingsService?: InternalUserSettingsServiceSetup;
themeName$: BehaviorSubject<ThemeName>;
}
interface RenderedOptions {
@ -55,6 +55,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({
uiPlugins,
auth,
userSettingsService,
themeName$,
}) => {
const isAuthenticated = (request: KibanaRequest) => {
const { status: authStatus } = auth.get(request);
@ -64,11 +65,9 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({
return async function bootstrapRenderer({ uiSettingsClient, request, isAnonymousPage = false }) {
let darkMode: DarkModeValue = false;
let themeName: ThemeName = DEFAULT_THEME_NAME;
const themeName = themeName$.getValue();
try {
themeName = parseThemeNameValue(await uiSettingsClient.get('theme:name'));
const authenticated = isAuthenticated(request);
if (authenticated) {

View file

@ -27,10 +27,13 @@ import {
mockRenderingServiceParams,
mockRenderingPrebootDeps,
mockRenderingSetupDeps,
mockRenderingStartDeps,
} from './test_helpers/params';
import { InternalRenderingServicePreboot, InternalRenderingServiceSetup } from './types';
import { RenderingService } from './rendering_service';
import { RenderingService, DEFAULT_THEME_NAME_FEATURE_FLAG } from './rendering_service';
import { AuthStatus } from '@kbn/core-http-server';
import { DEFAULT_THEME_NAME, ThemeName } from '@kbn/core-ui-settings-common';
import { BehaviorSubject } from 'rxjs';
const BUILD_DATE = '2023-05-15T23:12:09+0000';
const INJECTED_METADATA = {
@ -584,4 +587,40 @@ describe('RenderingService', () => {
return [(await service.setup(mockRenderingSetupDeps)).render, mockRenderingSetupDeps];
});
});
describe('start()', () => {
it('subscribes to the featureFlags.setStringValue$ observable and updates theme name accordingly', async () => {
// setup and render added to assert the current theme name
const { render } = await service.setup(mockRenderingSetupDeps);
const themeName$ = new BehaviorSubject<ThemeName>(DEFAULT_THEME_NAME);
const getStringValue$ = jest
.fn()
.mockImplementation((_, fallback) => themeName$.asObservable());
service.start({
...mockRenderingStartDeps,
featureFlags: {
...mockRenderingStartDeps.featureFlags,
getStringValue$,
},
});
expect(getStringValue$).toHaveBeenCalledTimes(1);
expect(getStringValue$).toHaveBeenCalledWith(
DEFAULT_THEME_NAME_FEATURE_FLAG,
DEFAULT_THEME_NAME
);
const uiSettings = {
client: uiSettingsServiceMock.createClient(),
globalClient: uiSettingsServiceMock.createClient(),
};
let renderResult = await render(createKibanaRequest(), uiSettings);
expect(renderResult).toContain(',&quot;name&quot;:&quot;borealis&quot;');
themeName$.next('amsterdam');
renderResult = await render(createKibanaRequest(), uiSettings);
expect(renderResult).toContain(',&quot;name&quot;:&quot;amsterdam&quot;');
});
});
});

View file

@ -9,8 +9,7 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { firstValueFrom, of } from 'rxjs';
import { catchError, take, timeout } from 'rxjs';
import { BehaviorSubject, firstValueFrom, of, map, catchError, take, timeout } from 'rxjs';
import { i18n as i18nLib } from '@kbn/i18n';
import type { ThemeVersion } from '@kbn/ui-shared-deps-npm';
@ -26,6 +25,7 @@ import {
parseThemeNameValue,
type UiSettingsParams,
type UserProvidedValues,
DEFAULT_THEME_NAME,
} from '@kbn/core-ui-settings-common';
import { Template } from './views';
import {
@ -35,6 +35,7 @@ import {
InternalRenderingServicePreboot,
InternalRenderingServiceSetup,
RenderingMetadata,
RenderingStartDeps,
} from './types';
import { registerBootstrapRoute, bootstrapRendererFactory } from './bootstrap';
import {
@ -60,8 +61,14 @@ type RenderOptions =
const themeVersion: ThemeVersion = 'v8';
// TODO: Remove the temporary feature flag and supporting code when Borealis is live in Serverless
// https://github.com/elastic/eui-private/issues/192
export const DEFAULT_THEME_NAME_FEATURE_FLAG = 'coreRendering.defaultThemeName';
/** @internal */
export class RenderingService {
private readonly themeName$ = new BehaviorSubject<ThemeName>(DEFAULT_THEME_NAME);
constructor(private readonly coreContext: CoreContext) {}
public async preboot({
@ -77,6 +84,7 @@ export class RenderingService {
baseHref: http.staticAssets.getHrefBase(),
packageInfo: this.coreContext.env.packageInfo,
auth: http.auth,
themeName$: this.themeName$,
}),
});
});
@ -103,6 +111,7 @@ export class RenderingService {
baseHref: http.staticAssets.getHrefBase(),
packageInfo: this.coreContext.env.packageInfo,
auth: http.auth,
themeName$: this.themeName$,
userSettingsService: userSettings,
}),
});
@ -121,6 +130,14 @@ export class RenderingService {
};
}
public start({ featureFlags }: RenderingStartDeps) {
featureFlags
.getStringValue$<ThemeName>(DEFAULT_THEME_NAME_FEATURE_FLAG, DEFAULT_THEME_NAME)
// Parse the input feature flag value to ensure it's of type ThemeName
.pipe(map((value) => parseThemeNameValue(value)))
.subscribe(this.themeName$);
}
private async render(
renderOptions: RenderOptions,
request: KibanaRequest,
@ -213,8 +230,6 @@ export class RenderingService {
darkMode = getSettingValue<DarkModeValue>('theme:darkMode', settings, parseDarkModeValue);
}
const themeName = getSettingValue<ThemeName>('theme:name', settings, parseThemeNameValue);
const themeStylesheetPaths = (mode: boolean) =>
getThemeStylesheetPaths({
darkMode: mode,
@ -279,7 +294,7 @@ export class RenderingService {
},
theme: {
darkMode,
name: themeName,
name: this.themeName$.getValue(),
version: themeVersion,
stylesheetPaths: {
default: themeStylesheetPaths(false),

View file

@ -48,3 +48,6 @@ export const mockRenderingSetupDeps = {
userSettings,
i18n: i18nServiceMock.createSetupContract(),
};
export const mockRenderingStartDeps = {
featureFlags: coreFeatureFlagsMock.createStart(),
};

View file

@ -22,10 +22,12 @@ export const setupMock: jest.Mocked<InternalRenderingServiceSetup> = {
};
export const mockPreboot = jest.fn().mockResolvedValue(prebootMock);
export const mockSetup = jest.fn().mockResolvedValue(setupMock);
export const mockStart = jest.fn();
export const mockStop = jest.fn();
export const mockRenderingService: jest.Mocked<IRenderingService> = {
preboot: mockPreboot,
setup: mockSetup,
start: mockStart,
stop: mockStop,
};
export const RenderingService = jest.fn<IRenderingService, [typeof mockRenderingServiceParams]>(

View file

@ -25,6 +25,7 @@ import type { InternalUserSettingsServiceSetup } from '@kbn/core-user-settings-s
import type { I18nServiceSetup } from '@kbn/core-i18n-server';
import type { InternalI18nServicePreboot } from '@kbn/core-i18n-server-internal';
import type { InternalFeatureFlagsSetup } from '@kbn/core-feature-flags-server-internal';
import type { FeatureFlagsStart } from '@kbn/core-feature-flags-server';
/** @internal */
export interface RenderingMetadata {
@ -60,6 +61,11 @@ export interface RenderingSetupDeps {
i18n: I18nServiceSetup;
}
/** @internal */
export interface RenderingStartDeps {
featureFlags: FeatureFlagsStart;
}
/** @internal */
export interface IRenderOptions {
/**

View file

@ -47,6 +47,7 @@
"@kbn/apm-config-loader",
"@kbn/core-feature-flags-server-internal",
"@kbn/core-feature-flags-server-mocks",
"@kbn/core-feature-flags-server",
],
"exclude": [
"target/**/*",

View file

@ -34,6 +34,7 @@ function createRenderingService() {
const mock: RenderingServiceMock = {
preboot: jest.fn(),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};

View file

@ -454,6 +454,10 @@ export class Server {
this.httpRateLimiter.start();
this.status.start();
this.rendering.start({
featureFlags: featureFlagsStart,
});
this.coreStart = {
analytics: analyticsStart,
capabilities: capabilitiesStart,

View file

@ -47,12 +47,6 @@ export function parseThemeTags(input?: unknown): ThemeTags {
return DEFAULT_THEME_TAGS;
}
// TODO: remove when Borealis is in public beta
// This is left here for backwards compatibility during Borealis testing.
if (input === 'experimental') {
return DEFAULT_THEME_TAGS;
}
let rawTags: string[];
if (typeof input === 'string') {
rawTags = input.split(',').map((tag) => tag.trim());

View file

@ -58,7 +58,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const logLevelBadge = await firstCell.findByTestSubject('*logLevelBadgeCell-');
expect(await logLevelBadge.getVisibleText()).to.be('debug');
expect(await logLevelBadge.getComputedStyle('background-color')).to.be(
'rgba(190, 207, 227, 1)'
'rgba(232, 241, 255, 1)'
);
});
@ -199,7 +199,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
logLevelBadge = await firstCell.findByTestSubject('*logLevelBadgeCell-');
expect(await logLevelBadge.getVisibleText()).to.be('debug');
expect(await logLevelBadge.getComputedStyle('background-color')).to.be(
'rgba(190, 207, 227, 1)'
'rgba(232, 241, 255, 1)'
);
});
@ -216,7 +216,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
logLevelBadge = await firstCell.findByTestSubject('*logLevelBadgeCell-');
expect(await logLevelBadge.getVisibleText()).to.be('debug');
expect(await logLevelBadge.getComputedStyle('background-color')).to.be(
'rgba(190, 207, 227, 1)'
'rgba(232, 241, 255, 1)'
);
});
});