mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Browser-side theme service: add synchronous accessor for current theme (#173535)
## Summary Related to https://github.com/elastic/kibana/issues/173529. Very likely, some of the work for https://github.com/elastic/kibana/issues/173529 will require a synchronous API to retrieve the current theme. Opening this PR now to avoid blocking the issue.
This commit is contained in:
parent
3ea5865a0a
commit
40b354958f
17 changed files with 104 additions and 61 deletions
|
@ -19,6 +19,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Rx.Observable,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -147,6 +148,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -254,6 +256,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
|
|
@ -100,6 +100,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -361,6 +362,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -677,6 +679,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -914,6 +917,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1156,6 +1160,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1393,6 +1398,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1438,6 +1444,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1581,6 +1588,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1688,6 +1696,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1800,6 +1809,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
@ -1907,6 +1917,7 @@ Array [
|
|||
}
|
||||
theme={
|
||||
Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { take } from 'rxjs/operators';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks';
|
||||
import { ThemeService } from './theme_service';
|
||||
|
||||
|
@ -23,7 +23,16 @@ describe('ThemeService', () => {
|
|||
it('exposes a `theme$` observable with the values provided by the injected metadata', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
const { theme$ } = themeService.setup({ injectedMetadata });
|
||||
const theme = await theme$.pipe(take(1)).toPromise();
|
||||
const theme = await firstValueFrom(theme$);
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('#getTheme() returns the current theme', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
const setup = themeService.setup({ injectedMetadata });
|
||||
const theme = setup.getTheme();
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
|
@ -41,7 +50,17 @@ describe('ThemeService', () => {
|
|||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
themeService.setup({ injectedMetadata });
|
||||
const { theme$ } = themeService.start();
|
||||
const theme = await theme$.pipe(take(1)).toPromise();
|
||||
const theme = await firstValueFrom(theme$);
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('#getTheme() returns the current theme', async () => {
|
||||
injectedMetadata.getTheme.mockReturnValue({ version: 'v8', darkMode: true });
|
||||
themeService.setup({ injectedMetadata });
|
||||
const start = themeService.start();
|
||||
const theme = start.getTheme();
|
||||
expect(theme).toEqual({
|
||||
darkMode: true,
|
||||
});
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Subject, Observable, of } from 'rxjs';
|
||||
import { shareReplay, takeUntil } from 'rxjs/operators';
|
||||
import { Subject, of } from 'rxjs';
|
||||
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
|
||||
import type { CoreTheme, ThemeServiceSetup, ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
|
||||
|
@ -18,26 +17,27 @@ export interface ThemeServiceSetupDeps {
|
|||
|
||||
/** @internal */
|
||||
export class ThemeService {
|
||||
private theme$?: Observable<CoreTheme>;
|
||||
private contract?: ThemeServiceSetup;
|
||||
private stop$ = new Subject<void>();
|
||||
|
||||
public setup({ injectedMetadata }: ThemeServiceSetupDeps): ThemeServiceSetup {
|
||||
const theme = injectedMetadata.getTheme();
|
||||
this.theme$ = of({ darkMode: theme.darkMode });
|
||||
const themeMeta = injectedMetadata.getTheme();
|
||||
const theme: CoreTheme = { darkMode: themeMeta.darkMode };
|
||||
|
||||
return {
|
||||
theme$: this.theme$.pipe(takeUntil(this.stop$), shareReplay(1)),
|
||||
this.contract = {
|
||||
theme$: of(theme),
|
||||
getTheme: () => theme,
|
||||
};
|
||||
|
||||
return this.contract;
|
||||
}
|
||||
|
||||
public start(): ThemeServiceStart {
|
||||
if (!this.theme$) {
|
||||
if (!this.contract) {
|
||||
throw new Error('setup must be called before start');
|
||||
}
|
||||
|
||||
return {
|
||||
theme$: this.theme$.pipe(takeUntil(this.stop$), shareReplay(1)),
|
||||
};
|
||||
return this.contract;
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { of } from 'rxjs';
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import type { ThemeServiceSetup, ThemeServiceStart, CoreTheme } from '@kbn/core-theme-browser';
|
||||
import type { ThemeServiceSetup, CoreTheme } from '@kbn/core-theme-browser';
|
||||
import type { ThemeService } from '@kbn/core-theme-browser-internal';
|
||||
|
||||
const mockTheme: CoreTheme = {
|
||||
|
@ -19,22 +19,16 @@ const createThemeMock = (): CoreTheme => {
|
|||
return { ...mockTheme };
|
||||
};
|
||||
|
||||
const createTheme$Mock = () => {
|
||||
return of(createThemeMock());
|
||||
const createTheme$Mock = (theme: CoreTheme = createThemeMock()) => {
|
||||
return of(theme);
|
||||
};
|
||||
|
||||
const createThemeSetupMock = () => {
|
||||
const setupMock: jest.Mocked<ThemeServiceSetup> = {
|
||||
theme$: createTheme$Mock(),
|
||||
const createThemeContractMock = (theme: CoreTheme = createThemeMock()) => {
|
||||
const themeMock: jest.Mocked<ThemeServiceSetup> = {
|
||||
theme$: createTheme$Mock(theme),
|
||||
getTheme: jest.fn().mockReturnValue(theme),
|
||||
};
|
||||
return setupMock;
|
||||
};
|
||||
|
||||
const createThemeStartMock = () => {
|
||||
const startMock: jest.Mocked<ThemeServiceStart> = {
|
||||
theme$: createTheme$Mock(),
|
||||
};
|
||||
return startMock;
|
||||
return themeMock;
|
||||
};
|
||||
|
||||
type ThemeServiceContract = PublicMethodsOf<ThemeService>;
|
||||
|
@ -46,16 +40,16 @@ const createServiceMock = () => {
|
|||
stop: jest.fn(),
|
||||
};
|
||||
|
||||
mocked.setup.mockReturnValue(createThemeSetupMock());
|
||||
mocked.start.mockReturnValue(createThemeStartMock());
|
||||
mocked.setup.mockReturnValue(createThemeContractMock());
|
||||
mocked.start.mockReturnValue(createThemeContractMock());
|
||||
|
||||
return mocked;
|
||||
};
|
||||
|
||||
export const themeServiceMock = {
|
||||
create: createServiceMock,
|
||||
createSetupContract: createThemeSetupMock,
|
||||
createStartContract: createThemeStartMock,
|
||||
createSetupContract: createThemeContractMock,
|
||||
createStartContract: createThemeContractMock,
|
||||
createTheme: createThemeMock,
|
||||
createTheme$: createTheme$Mock,
|
||||
};
|
||||
|
|
|
@ -22,12 +22,21 @@ export interface CoreTheme {
|
|||
* @public
|
||||
*/
|
||||
export interface ThemeServiceSetup {
|
||||
/**
|
||||
* An observable of the currently active theme.
|
||||
* Note that the observable has a replay effect, so it will emit once during subscriptions.
|
||||
*/
|
||||
theme$: Observable<CoreTheme>;
|
||||
|
||||
/**
|
||||
* Returns the theme currently in use.
|
||||
* Note that when possible, using the `theme$` observable instead is strongly encouraged, as
|
||||
* it will allow to react to dynamic theme switch (even if those are not implemented at the moment)
|
||||
*/
|
||||
getTheme(): CoreTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ThemeServiceStart {
|
||||
theme$: Observable<CoreTheme>;
|
||||
}
|
||||
export type ThemeServiceStart = ThemeServiceSetup;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { of } from 'rxjs';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
||||
import { chromeServiceMock, coreMock } from '@kbn/core/public/mocks';
|
||||
import { chromeServiceMock, coreMock, themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
|
||||
import { IUiSettingsClient, ToastsStart } from '@kbn/core/public';
|
||||
|
@ -41,10 +41,7 @@ export function createServicesMock() {
|
|||
...uiSettingsMock,
|
||||
};
|
||||
|
||||
const theme = {
|
||||
theme$: of({ darkMode: false }),
|
||||
};
|
||||
|
||||
const theme = themeServiceMock.createSetupContract({ darkMode: false });
|
||||
corePluginMock.theme = theme;
|
||||
|
||||
const dataPlugin = dataPluginMock.createStartContract();
|
||||
|
|
|
@ -5,12 +5,11 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import { from } from 'rxjs';
|
||||
|
||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { ThemeService } from '@kbn/charts-plugin/public/services';
|
||||
|
||||
const theme = new ThemeService();
|
||||
theme.init({
|
||||
theme$: from([{ darkMode: false }]),
|
||||
});
|
||||
theme.init(themeServiceMock.createSetupContract({ darkMode: false }));
|
||||
|
||||
export { theme };
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { coreMock, themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ControlsCoreService } from './types';
|
||||
|
||||
export type CoreServiceFactory = PluginServiceFactory<ControlsCoreService>;
|
||||
|
@ -16,9 +15,7 @@ export type CoreServiceFactory = PluginServiceFactory<ControlsCoreService>;
|
|||
export const coreServiceFactory: CoreServiceFactory = () => {
|
||||
const corePluginMock = coreMock.createStart();
|
||||
return {
|
||||
theme: {
|
||||
theme$: new Observable((subscriber) => subscriber.next({ darkMode: false })),
|
||||
},
|
||||
theme: themeServiceMock.createSetupContract(),
|
||||
i18n: corePluginMock.i18n,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -11,7 +11,12 @@ import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
|||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
|
||||
import { savedSearchPluginMock } from '@kbn/saved-search-plugin/public/mocks';
|
||||
import { chromeServiceMock, coreMock, docLinksServiceMock } from '@kbn/core/public/mocks';
|
||||
import {
|
||||
chromeServiceMock,
|
||||
coreMock,
|
||||
docLinksServiceMock,
|
||||
themeServiceMock,
|
||||
} from '@kbn/core/public/mocks';
|
||||
import {
|
||||
CONTEXT_STEP_SETTING,
|
||||
DEFAULT_COLUMNS_SETTING,
|
||||
|
@ -133,9 +138,7 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
...uiSettingsMock,
|
||||
};
|
||||
|
||||
const theme = {
|
||||
theme$: of({ darkMode: false }),
|
||||
};
|
||||
const theme = themeServiceMock.createSetupContract({ darkMode: false });
|
||||
|
||||
corePluginMock.theme = theme;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import { toMountPoint as _toMountPoint } from '@kbn/react-kibana-mount';
|
|||
// and will be removed when the deprecated usages are removed.
|
||||
const themeStart: ThemeServiceStart = {
|
||||
theme$: new Observable((subscriber) => subscriber.next(defaultTheme)),
|
||||
getTheme: () => defaultTheme,
|
||||
};
|
||||
|
||||
// The `i18n` start contract should always be included to ensure
|
||||
|
|
|
@ -35,6 +35,7 @@ exports[`SavedObjectEdition should render normally 1`] = `
|
|||
},
|
||||
},
|
||||
"theme": Object {
|
||||
"getTheme": [MockFunction],
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
|
|
|
@ -93,6 +93,7 @@ export const StorybookContext: React.FC<{ storyContext?: Parameters<DecoratorFn>
|
|||
settings: getSettings(),
|
||||
theme: {
|
||||
theme$: EMPTY,
|
||||
getTheme: () => ({ darkMode: false }),
|
||||
},
|
||||
plugins: {} as unknown as PluginsServiceStart,
|
||||
authz: {
|
||||
|
|
|
@ -7,8 +7,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CoreSetup, CoreStart, HttpSetup, ChromeStart, CoreTheme } from '@kbn/core/public';
|
||||
import type {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
HttpSetup,
|
||||
ChromeStart,
|
||||
ThemeServiceStart,
|
||||
} from '@kbn/core/public';
|
||||
|
||||
import { createKibanaReactContext, KibanaThemeProvider } from '../shared_imports';
|
||||
|
||||
|
@ -23,12 +28,12 @@ interface AppDependencies {
|
|||
settings: CoreStart['settings'];
|
||||
links: Links;
|
||||
chrome: ChromeStart;
|
||||
theme$: Observable<CoreTheme>;
|
||||
theme: ThemeServiceStart;
|
||||
}
|
||||
|
||||
export function renderApp(
|
||||
element: HTMLElement | null,
|
||||
{ http, I18nContext, uiSettings, links, chrome, theme$, settings }: AppDependencies
|
||||
{ http, I18nContext, uiSettings, links, chrome, theme, settings }: AppDependencies
|
||||
) {
|
||||
if (!element) {
|
||||
return () => undefined;
|
||||
|
@ -36,11 +41,11 @@ export function renderApp(
|
|||
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
|
||||
uiSettings,
|
||||
settings,
|
||||
theme: { theme$ },
|
||||
theme,
|
||||
});
|
||||
render(
|
||||
<I18nContext>
|
||||
<KibanaThemeProvider theme$={theme$}>
|
||||
<KibanaThemeProvider theme$={theme.theme$}>
|
||||
<KibanaReactContextProvider>
|
||||
<AppContextProvider value={{ http, links, chrome }}>
|
||||
<Main />
|
||||
|
|
|
@ -48,7 +48,7 @@ export class PainlessLabUIPlugin implements Plugin<void, void, PluginDependencie
|
|||
}),
|
||||
enableRouting: false,
|
||||
disabled: false,
|
||||
mount: async ({ element, theme$ }) => {
|
||||
mount: async ({ element }) => {
|
||||
const [core] = await getStartServices();
|
||||
|
||||
const {
|
||||
|
@ -57,6 +57,7 @@ export class PainlessLabUIPlugin implements Plugin<void, void, PluginDependencie
|
|||
docLinks,
|
||||
chrome,
|
||||
settings,
|
||||
theme,
|
||||
} = core;
|
||||
|
||||
const license = await firstValueFrom(licensing.license$);
|
||||
|
@ -75,7 +76,7 @@ export class PainlessLabUIPlugin implements Plugin<void, void, PluginDependencie
|
|||
uiSettings,
|
||||
links: getLinks(docLinks),
|
||||
chrome,
|
||||
theme$,
|
||||
theme,
|
||||
settings,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue