Use KibanaThemeProvider for security/spaces apps (#119124)

This commit is contained in:
Joe Portner 2021-11-23 12:03:46 -05:00 committed by GitHub
parent 3c17852eb1
commit e5b279f318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 532 additions and 218 deletions

View file

@ -9,7 +9,7 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { ClusterAddressForm } from './cluster_address_form';
import { Providers } from './plugin';
@ -21,6 +21,8 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
describe('ClusterAddressForm', () => {
jest.setTimeout(20_000);
const theme$ = themeServiceMock.createTheme$();
it('calls enrollment API when submitting form', async () => {
const coreStart = coreMock.createStart();
coreStart.http.post.mockResolvedValue({});
@ -28,7 +30,7 @@ describe('ClusterAddressForm', () => {
const onSuccess = jest.fn();
const { findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<ClusterAddressForm onSuccess={onSuccess} />
</Providers>
);
@ -52,7 +54,7 @@ describe('ClusterAddressForm', () => {
const onSuccess = jest.fn();
const { findAllByText, findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<ClusterAddressForm onSuccess={onSuccess} />
</Providers>
);

View file

@ -9,7 +9,7 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { ClusterConfigurationForm } from './cluster_configuration_form';
import { Providers } from './plugin';
@ -21,6 +21,8 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
describe('ClusterConfigurationForm', () => {
jest.setTimeout(20_000);
const theme$ = themeServiceMock.createTheme$();
it('calls enrollment API for https addresses when submitting form', async () => {
const coreStart = coreMock.createStart();
coreStart.http.post.mockResolvedValue({});
@ -28,7 +30,7 @@ describe('ClusterConfigurationForm', () => {
const onSuccess = jest.fn();
const { findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<ClusterConfigurationForm
host="https://localhost:9200"
authRequired
@ -78,7 +80,7 @@ describe('ClusterConfigurationForm', () => {
const onSuccess = jest.fn();
const { findByRole } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<ClusterConfigurationForm
host="http://localhost:9200"
authRequired={false}
@ -107,7 +109,7 @@ describe('ClusterConfigurationForm', () => {
const onSuccess = jest.fn();
const { findAllByText, findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<ClusterConfigurationForm
host="https://localhost:9200"
authRequired

View file

@ -9,7 +9,7 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import type { EnrollmentToken } from '../common';
import { decodeEnrollmentToken, EnrollmentTokenForm } from './enrollment_token_form';
@ -29,6 +29,8 @@ const token: EnrollmentToken = {
describe('EnrollmentTokenForm', () => {
jest.setTimeout(20_000);
const theme$ = themeServiceMock.createTheme$();
it('calls enrollment API when submitting form', async () => {
const coreStart = coreMock.createStart();
coreStart.http.post.mockResolvedValue({});
@ -36,7 +38,7 @@ describe('EnrollmentTokenForm', () => {
const onSuccess = jest.fn();
const { findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<EnrollmentTokenForm onSuccess={onSuccess} />
</Providers>
);
@ -62,7 +64,7 @@ describe('EnrollmentTokenForm', () => {
const onSuccess = jest.fn();
const { findAllByText, findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<EnrollmentTokenForm onSuccess={onSuccess} />
</Providers>
);

View file

@ -9,11 +9,13 @@
import type { FunctionComponent } from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import type { Observable } from 'rxjs';
import { I18nProvider } from '@kbn/i18n/react';
import type { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import type { CoreSetup, CoreStart, CoreTheme, Plugin } from 'src/core/public';
import { App } from './app';
import { KibanaThemeProvider } from './theme'; // TODO: replace this with the one exported from `kibana_react` after https://github.com/elastic/kibana/issues/119204 is implemented.
import { KibanaProvider } from './use_kibana';
import { VerificationProvider } from './use_verification';
@ -24,7 +26,7 @@ export class InteractiveSetupPlugin implements Plugin<void, void, {}, {}> {
title: 'Configure Elastic to get started',
appRoute: '/',
chromeless: true,
mount: async (params) => {
mount: async ({ element, theme$ }) => {
const url = new URL(window.location.href);
const defaultCode = url.searchParams.get('code') || undefined;
const onSuccess = () => {
@ -34,12 +36,12 @@ export class InteractiveSetupPlugin implements Plugin<void, void, {}, {}> {
const [services] = await core.getStartServices();
ReactDOM.render(
<Providers defaultCode={defaultCode} services={services}>
<Providers defaultCode={defaultCode} services={services} theme$={theme$}>
<App onSuccess={onSuccess} />
</Providers>,
params.element
element
);
return () => ReactDOM.unmountComponentAtNode(params.element);
return () => ReactDOM.unmountComponentAtNode(element);
},
});
}
@ -49,17 +51,21 @@ export class InteractiveSetupPlugin implements Plugin<void, void, {}, {}> {
export interface ProvidersProps {
services: CoreStart;
theme$: Observable<CoreTheme>;
defaultCode?: string;
}
export const Providers: FunctionComponent<ProvidersProps> = ({
defaultCode,
services,
theme$,
children,
}) => (
<I18nProvider>
<KibanaProvider services={services}>
<VerificationProvider defaultCode={defaultCode}>{children}</VerificationProvider>
</KibanaProvider>
<KibanaThemeProvider theme$={theme$}>
<KibanaProvider services={services}>
<VerificationProvider defaultCode={defaultCode}>{children}</VerificationProvider>
</KibanaProvider>
</KibanaThemeProvider>
</I18nProvider>
);

View file

@ -0,0 +1,9 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
export { KibanaThemeProvider } from './kibana_theme_provider';

View file

@ -0,0 +1,88 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { useEuiTheme } from '@elastic/eui';
import type { ReactWrapper } from 'enzyme';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { BehaviorSubject, of } from 'rxjs';
import { mountWithIntl } from '@kbn/test/jest';
import type { CoreTheme } from 'src/core/public';
import { KibanaThemeProvider } from './kibana_theme_provider';
describe('KibanaThemeProvider', () => {
let euiTheme: ReturnType<typeof useEuiTheme> | undefined;
beforeEach(() => {
euiTheme = undefined;
});
const flushPromises = async () => {
await new Promise<void>(async (resolve, reject) => {
try {
setImmediate(() => resolve());
} catch (error) {
reject(error);
}
});
};
const InnerComponent: FC = () => {
const theme = useEuiTheme();
useEffect(() => {
euiTheme = theme;
}, [theme]);
return <div>foo</div>;
};
const refresh = async (wrapper: ReactWrapper<unknown>) => {
await act(async () => {
await flushPromises();
wrapper.update();
});
};
it('exposes the EUI theme provider', async () => {
const coreTheme: CoreTheme = { darkMode: true };
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={of(coreTheme)}>
<InnerComponent />
</KibanaThemeProvider>
);
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('DARK');
});
it('propagates changes of the coreTheme observable', async () => {
const coreTheme$ = new BehaviorSubject<CoreTheme>({ darkMode: true });
const wrapper = mountWithIntl(
<KibanaThemeProvider theme$={coreTheme$}>
<InnerComponent />
</KibanaThemeProvider>
);
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('DARK');
await act(async () => {
coreTheme$.next({ darkMode: false });
});
await refresh(wrapper);
expect(euiTheme!.colorMode).toEqual('LIGHT');
});
});

View file

@ -0,0 +1,33 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { EuiThemeProvider } from '@elastic/eui';
import type { FC } from 'react';
import React, { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import type { CoreTheme } from '../../../../core/public';
import { getColorMode } from './utils';
interface KibanaThemeProviderProps {
theme$: Observable<CoreTheme>;
}
const defaultTheme: CoreTheme = {
darkMode: false,
};
/**
* Copied from the `kibana_react` plugin, remove once https://github.com/elastic/kibana/issues/119204 is implemented.
*/
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, children }) => {
const theme = useObservable(theme$, defaultTheme);
const colorMode = useMemo(() => getColorMode(theme), [theme]);
return <EuiThemeProvider colorMode={colorMode}>{children}</EuiThemeProvider>;
};

View file

@ -0,0 +1,19 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { getColorMode } from './utils';
describe('getColorMode', () => {
it('returns the correct `colorMode` when `darkMode` is enabled', () => {
expect(getColorMode({ darkMode: true })).toEqual('DARK');
});
it('returns the correct `colorMode` when `darkMode` is disabled', () => {
expect(getColorMode({ darkMode: false })).toEqual('LIGHT');
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import type { EuiThemeColorMode } from '@elastic/eui/src/services/theme/types';
import type { CoreTheme } from '../../../../core/public';
/**
* Copied from the `kibana_react` plugin, remove once https://github.com/elastic/kibana/issues/119204 is implemented.
*/
export const getColorMode = (theme: CoreTheme): EuiThemeColorMode => {
// COLOR_MODES_STANDARD is not exported from eui
return theme.darkMode ? 'DARK' : 'LIGHT';
};

View file

@ -9,7 +9,7 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { Providers } from './plugin';
import { VerificationCodeForm } from './verification_code_form';
@ -21,6 +21,8 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
describe('VerificationCodeForm', () => {
jest.setTimeout(20_000);
const theme$ = themeServiceMock.createTheme$();
it('calls enrollment API when submitting form', async () => {
const coreStart = coreMock.createStart();
coreStart.http.post.mockResolvedValue({});
@ -28,7 +30,7 @@ describe('VerificationCodeForm', () => {
const onSuccess = jest.fn();
const { findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<VerificationCodeForm onSuccess={onSuccess} />
</Providers>
);
@ -65,7 +67,7 @@ describe('VerificationCodeForm', () => {
const onSuccess = jest.fn();
const { findAllByText, findByRole, findByLabelText } = render(
<Providers services={coreStart}>
<Providers services={coreStart} theme$={theme$}>
<VerificationCodeForm onSuccess={onSuccess} />
</Providers>
);

View file

@ -21,6 +21,9 @@ const defaultTheme: CoreTheme = {
darkMode: false,
};
// IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
// That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.
export const KibanaThemeProvider: FC<KibanaThemeProviderProps> = ({ theme$, children }) => {
const theme = useObservable(theme$, defaultTheme);
const colorMode = useMemo(() => getColorMode(theme), [theme]);

View file

@ -9,6 +9,9 @@
import type { EuiThemeColorMode } from '@elastic/eui/src/services/theme/types';
import type { CoreTheme } from '../../../../core/public';
// IMPORTANT: This code has been copied to the `interactive_setup` plugin, any changes here should be applied there too.
// That copy and this comment can be removed once https://github.com/elastic/kibana/issues/119204 is implemented.
export const getColorMode = (theme: CoreTheme): EuiThemeColorMode => {
// COLOR_MODES_STANDARD is not exported from eui
return theme.darkMode ? 'DARK' : 'LIGHT';

View file

@ -43,7 +43,6 @@ describe('accountManagementApp', () => {
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
const authcMock = securityMock.createSetup().authc;
const containerMock = document.createElement('div');
accountManagementApp.create({
application: coreSetupMock.application,
@ -52,14 +51,15 @@ describe('accountManagementApp', () => {
});
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
await (mount as AppMount)({
element: containerMock,
const appMountParams = {
element: document.createElement('div'),
appBasePath: '',
onAppLeave: jest.fn(),
setHeaderActionMenu: jest.fn(),
history: scopedHistoryMock.create(),
theme$: themeServiceMock.createTheme$(),
});
};
await (mount as AppMount)(appMountParams);
expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalledTimes(1);
expect(coreStartMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([
@ -68,10 +68,14 @@ describe('accountManagementApp', () => {
const mockRenderApp = jest.requireMock('./account_management_page').renderAccountManagementPage;
expect(mockRenderApp).toHaveBeenCalledTimes(1);
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
userAPIClient: expect.any(UserAPIClient),
authc: authcMock,
notifications: coreStartMock.notifications,
});
expect(mockRenderApp).toHaveBeenCalledWith(
coreStartMock.i18n,
{ element: appMountParams.element, theme$: appMountParams.theme$ },
{
userAPIClient: expect.any(UserAPIClient),
authc: authcMock,
notifications: coreStartMock.notifications,
}
);
});
});

View file

@ -28,18 +28,22 @@ export const accountManagementApp = Object.freeze({
title,
navLinkStatus: AppNavLinkStatus.hidden,
appRoute: '/security/account',
async mount({ element }: AppMountParameters) {
async mount({ element, theme$ }: AppMountParameters) {
const [[coreStart], { renderAccountManagementPage }, { UserAPIClient }] = await Promise.all(
[getStartServices(), import('./account_management_page'), import('../management')]
);
coreStart.chrome.setBreadcrumbs([{ text: title }]);
return renderAccountManagementPage(coreStart.i18n, element, {
authc,
notifications: coreStart.notifications,
userAPIClient: new UserAPIClient(coreStart.http),
});
return renderAccountManagementPage(
coreStart.i18n,
{ element, theme$ },
{
authc,
notifications: coreStart.notifications,
userAPIClient: new UserAPIClient(coreStart.http),
}
);
},
});
},

View file

@ -11,8 +11,9 @@ import ReactDOM from 'react-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { CoreStart, NotificationsStart } from 'src/core/public';
import type { AppMountParameters, CoreStart, NotificationsStart } from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import type { AuthenticatedUser } from '../../common/model';
import { getUserDisplayName } from '../../common/model';
import type { AuthenticationServiceSetup } from '../authentication';
@ -67,12 +68,14 @@ export const AccountManagementPage = ({ userAPIClient, authc, notifications }: P
export function renderAccountManagementPage(
i18nStart: CoreStart['i18n'],
element: Element,
{ element, theme$ }: Pick<AppMountParameters, 'element' | 'theme$'>,
props: Props
) {
ReactDOM.render(
<i18nStart.Context>
<AccountManagementPage {...props} />
<KibanaThemeProvider theme$={theme$}>
<AccountManagementPage {...props} />
</KibanaThemeProvider>
</i18nStart.Context>,
element
);

View file

@ -37,7 +37,6 @@ describe('accessAgreementApp', () => {
const coreSetupMock = coreMock.createSetup();
const coreStartMock = coreMock.createStart();
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
const containerMock = document.createElement('div');
accessAgreementApp.create({
application: coreSetupMock.application,
@ -45,21 +44,26 @@ describe('accessAgreementApp', () => {
});
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
await (mount as AppMount)({
element: containerMock,
const appMountParams = {
element: document.createElement('div'),
appBasePath: '',
onAppLeave: jest.fn(),
setHeaderActionMenu: jest.fn(),
history: scopedHistoryMock.create(),
theme$: themeServiceMock.createTheme$(),
});
};
await (mount as AppMount)(appMountParams);
const mockRenderApp = jest.requireMock('./access_agreement_page').renderAccessAgreementPage;
expect(mockRenderApp).toHaveBeenCalledTimes(1);
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
http: coreStartMock.http,
notifications: coreStartMock.notifications,
fatalErrors: coreStartMock.fatalErrors,
});
expect(mockRenderApp).toHaveBeenCalledWith(
coreStartMock.i18n,
{ element: appMountParams.element, theme$: appMountParams.theme$ },
{
http: coreStartMock.http,
notifications: coreStartMock.notifications,
fatalErrors: coreStartMock.fatalErrors,
}
);
});
});

View file

@ -23,16 +23,20 @@ export const accessAgreementApp = Object.freeze({
}),
chromeless: true,
appRoute: '/security/access_agreement',
async mount({ element }: AppMountParameters) {
async mount({ element, theme$ }: AppMountParameters) {
const [[coreStart], { renderAccessAgreementPage }] = await Promise.all([
getStartServices(),
import('./access_agreement_page'),
]);
return renderAccessAgreementPage(coreStart.i18n, element, {
http: coreStart.http,
notifications: coreStart.notifications,
fatalErrors: coreStart.fatalErrors,
});
return renderAccessAgreementPage(
coreStart.i18n,
{ element, theme$ },
{
http: coreStart.http,
notifications: coreStart.notifications,
fatalErrors: coreStart.fatalErrors,
}
);
},
});
},

View file

@ -23,8 +23,15 @@ import ReactMarkdown from 'react-markdown';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public';
import type {
AppMountParameters,
CoreStart,
FatalErrorsStart,
HttpStart,
NotificationsStart,
} from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public';
import { parseNext } from '../../../common/parse_next';
import { AuthenticationStatePage } from '../components';
@ -122,12 +129,14 @@ export function AccessAgreementPage({ http, fatalErrors, notifications }: Props)
export function renderAccessAgreementPage(
i18nStart: CoreStart['i18n'],
element: Element,
{ element, theme$ }: Pick<AppMountParameters, 'element' | 'theme$'>,
props: Props
) {
ReactDOM.render(
<i18nStart.Context>
<AccessAgreementPage {...props} />
<KibanaThemeProvider theme$={theme$}>
<AccessAgreementPage {...props} />
</KibanaThemeProvider>
</i18nStart.Context>,
element
);

View file

@ -38,24 +38,25 @@ describe('loggedOutApp', () => {
const coreStartMock = coreMock.createStart();
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
const containerMock = document.createElement('div');
loggedOutApp.create(coreSetupMock);
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
await (mount as AppMount)({
element: containerMock,
const appMountParams = {
element: document.createElement('div'),
appBasePath: '',
onAppLeave: jest.fn(),
setHeaderActionMenu: jest.fn(),
history: scopedHistoryMock.create(),
theme$: themeServiceMock.createTheme$(),
});
};
await (mount as AppMount)(appMountParams);
const mockRenderApp = jest.requireMock('./logged_out_page').renderLoggedOutPage;
expect(mockRenderApp).toHaveBeenCalledTimes(1);
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
basePath: coreStartMock.http.basePath,
});
expect(mockRenderApp).toHaveBeenCalledWith(
coreStartMock.i18n,
{ element: appMountParams.element, theme$: appMountParams.theme$ },
{ basePath: coreStartMock.http.basePath }
);
});
});

View file

@ -28,12 +28,16 @@ export const loggedOutApp = Object.freeze({
title: i18n.translate('xpack.security.loggedOutAppTitle', { defaultMessage: 'Logged out' }),
chromeless: true,
appRoute: '/security/logged_out',
async mount({ element }: AppMountParameters) {
async mount({ element, theme$ }: AppMountParameters) {
const [[coreStart], { renderLoggedOutPage }] = await Promise.all([
getStartServices(),
import('./logged_out_page'),
]);
return renderLoggedOutPage(coreStart.i18n, element, { basePath: coreStart.http.basePath });
return renderLoggedOutPage(
coreStart.i18n,
{ element, theme$ },
{ basePath: coreStart.http.basePath }
);
},
});
},

View file

@ -10,8 +10,9 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, IBasePath } from 'src/core/public';
import type { AppMountParameters, CoreStart, IBasePath } from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public';
import { parseNext } from '../../../common/parse_next';
import { AuthenticationStatePage } from '../components';
@ -36,10 +37,16 @@ export function LoggedOutPage({ basePath }: Props) {
);
}
export function renderLoggedOutPage(i18nStart: CoreStart['i18n'], element: Element, props: Props) {
export function renderLoggedOutPage(
i18nStart: CoreStart['i18n'],
{ element, theme$ }: Pick<AppMountParameters, 'element' | 'theme$'>,
props: Props
) {
ReactDOM.render(
<i18nStart.Context>
<LoggedOutPage {...props} />
<KibanaThemeProvider theme$={theme$}>
<LoggedOutPage {...props} />
</KibanaThemeProvider>
</i18nStart.Context>,
element
);

View file

@ -40,7 +40,6 @@ describe('loginApp', () => {
const coreSetupMock = coreMock.createSetup();
const coreStartMock = coreMock.createStart();
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
const containerMock = document.createElement('div');
loginApp.create({
...coreSetupMock,
@ -48,22 +47,27 @@ describe('loginApp', () => {
});
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
await (mount as AppMount)({
element: containerMock,
const appMountParams = {
element: document.createElement('div'),
appBasePath: '',
onAppLeave: jest.fn(),
setHeaderActionMenu: jest.fn(),
history: scopedHistoryMock.create(),
theme$: themeServiceMock.createTheme$(),
});
};
await (mount as AppMount)(appMountParams);
const mockRenderApp = jest.requireMock('./login_page').renderLoginPage;
expect(mockRenderApp).toHaveBeenCalledTimes(1);
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
http: coreStartMock.http,
notifications: coreStartMock.notifications,
fatalErrors: coreStartMock.fatalErrors,
loginAssistanceMessage: 'some-message',
});
expect(mockRenderApp).toHaveBeenCalledWith(
coreStartMock.i18n,
{ element: appMountParams.element, theme$: appMountParams.theme$ },
{
http: coreStartMock.http,
notifications: coreStartMock.notifications,
fatalErrors: coreStartMock.fatalErrors,
loginAssistanceMessage: 'some-message',
}
);
});
});

View file

@ -31,17 +31,21 @@ export const loginApp = Object.freeze({
title: i18n.translate('xpack.security.loginAppTitle', { defaultMessage: 'Login' }),
chromeless: true,
appRoute: '/login',
async mount({ element }: AppMountParameters) {
async mount({ element, theme$ }: AppMountParameters) {
const [[coreStart], { renderLoginPage }] = await Promise.all([
getStartServices(),
import('./login_page'),
]);
return renderLoginPage(coreStart.i18n, element, {
http: coreStart.http,
notifications: coreStart.notifications,
fatalErrors: coreStart.fatalErrors,
loginAssistanceMessage: config.loginAssistanceMessage,
});
return renderLoginPage(
coreStart.i18n,
{ element, theme$ },
{
http: coreStart.http,
notifications: coreStart.notifications,
fatalErrors: coreStart.fatalErrors,
loginAssistanceMessage: config.loginAssistanceMessage,
}
);
},
});
},

View file

@ -15,8 +15,15 @@ import { BehaviorSubject } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public';
import type {
AppMountParameters,
CoreStart,
FatalErrorsStart,
HttpStart,
NotificationsStart,
} from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public';
import {
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
LOGOUT_REASON_QUERY_STRING_PARAMETER,
@ -251,10 +258,16 @@ export class LoginPage extends Component<Props, State> {
};
}
export function renderLoginPage(i18nStart: CoreStart['i18n'], element: Element, props: Props) {
export function renderLoginPage(
i18nStart: CoreStart['i18n'],
{ element, theme$ }: Pick<AppMountParameters, 'element' | 'theme$'>,
props: Props
) {
ReactDOM.render(
<i18nStart.Context>
<LoginPage {...props} />
<KibanaThemeProvider theme$={theme$}>
<LoginPage {...props} />
</KibanaThemeProvider>
</i18nStart.Context>,
element
);

View file

@ -41,8 +41,6 @@ describe('overwrittenSessionApp', () => {
coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]);
const authcMock = securityMock.createSetup().authc;
const containerMock = document.createElement('div');
overwrittenSessionApp.create({
application: coreSetupMock.application,
getStartServices: coreSetupMock.getStartServices,
@ -50,22 +48,24 @@ describe('overwrittenSessionApp', () => {
});
const [[{ mount }]] = coreSetupMock.application.register.mock.calls;
await (mount as AppMount)({
element: containerMock,
const appMountParams = {
element: document.createElement('div'),
appBasePath: '',
onAppLeave: jest.fn(),
setHeaderActionMenu: jest.fn(),
history: scopedHistoryMock.create(),
theme$: themeServiceMock.createTheme$(),
});
};
await (mount as AppMount)(appMountParams);
const mockRenderApp = jest.requireMock(
'./overwritten_session_page'
).renderOverwrittenSessionPage;
expect(mockRenderApp).toHaveBeenCalledTimes(1);
expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, {
authc: authcMock,
basePath: coreStartMock.http.basePath,
});
expect(mockRenderApp).toHaveBeenCalledWith(
coreStartMock.i18n,
{ element: appMountParams.element, theme$: appMountParams.theme$ },
{ authc: authcMock, basePath: coreStartMock.http.basePath }
);
});
});

View file

@ -26,15 +26,19 @@ export const overwrittenSessionApp = Object.freeze({
}),
chromeless: true,
appRoute: '/security/overwritten_session',
async mount({ element }: AppMountParameters) {
async mount({ element, theme$ }: AppMountParameters) {
const [[coreStart], { renderOverwrittenSessionPage }] = await Promise.all([
getStartServices(),
import('./overwritten_session_page'),
]);
return renderOverwrittenSessionPage(coreStart.i18n, element, {
authc,
basePath: coreStart.http.basePath,
});
return renderOverwrittenSessionPage(
coreStart.i18n,
{ element, theme$ },
{
authc,
basePath: coreStart.http.basePath,
}
);
},
});
},

View file

@ -10,8 +10,9 @@ import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, IBasePath } from 'src/core/public';
import type { AppMountParameters, CoreStart, IBasePath } from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public';
import { parseNext } from '../../../common/parse_next';
import type { AuthenticationServiceSetup } from '../authentication_service';
import { AuthenticationStatePage } from '../components';
@ -53,12 +54,14 @@ export function OverwrittenSessionPage({ authc, basePath }: Props) {
export function renderOverwrittenSessionPage(
i18nStart: CoreStart['i18n'],
element: Element,
{ element, theme$ }: Pick<AppMountParameters, 'element' | 'theme$'>,
props: Props
) {
ReactDOM.render(
<i18nStart.Context>
<OverwrittenSessionPage {...props} />
<KibanaThemeProvider theme$={theme$}>
<OverwrittenSessionPage {...props} />
</KibanaThemeProvider>
</i18nStart.Context>,
element
);

View file

@ -9,7 +9,7 @@ import { render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { coreMock, themeServiceMock } from '../../../../../../../src/core/public/mocks';
import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock';
import { securityMock } from '../../../mocks';
import { Providers } from '../api_keys_management_app';
@ -33,6 +33,7 @@ describe('APIKeysGridPage', () => {
const consoleWarnMock = jest.spyOn(console, 'error').mockImplementation();
const coreStart = coreMock.createStart();
const theme$ = themeServiceMock.createTheme$();
const apiClientMock = apiKeysAPIClientMock.create();
const { authc } = securityMock.createSetup();
@ -85,7 +86,7 @@ describe('APIKeysGridPage', () => {
const history = createMemoryHistory({ initialEntries: ['/'] });
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
@ -113,7 +114,7 @@ describe('APIKeysGridPage', () => {
});
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
@ -135,7 +136,7 @@ describe('APIKeysGridPage', () => {
});
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}
@ -159,7 +160,7 @@ describe('APIKeysGridPage', () => {
const history = createMemoryHistory({ initialEntries: ['/'] });
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<APIKeysGridPage
apiKeysAPIClient={apiClientMock}
notifications={coreStart.notifications}

View file

@ -10,12 +10,20 @@ import type { FunctionComponent } from 'react';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Router } from 'react-router-dom';
import type { Observable } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import type { CoreStart, StartServicesAccessor } from '../../../../../../src/core/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import type {
CoreStart,
CoreTheme,
StartServicesAccessor,
} from '../../../../../../src/core/public';
import {
KibanaContextProvider,
KibanaThemeProvider,
} from '../../../../../../src/plugins/kibana_react/public';
import type { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public';
import type { AuthenticationServiceSetup } from '../../authentication';
import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb';
@ -41,7 +49,7 @@ export const apiKeysManagementApp = Object.freeze({
title: i18n.translate('xpack.security.management.apiKeysTitle', {
defaultMessage: 'API keys',
}),
async mount({ element, setBreadcrumbs, history }) {
async mount({ element, theme$, setBreadcrumbs, history }) {
const [[coreStart], { APIKeysGridPage }, { APIKeysAPIClient }] = await Promise.all([
getStartServices(),
import('./api_keys_grid'),
@ -51,6 +59,7 @@ export const apiKeysManagementApp = Object.freeze({
render(
<Providers
services={coreStart}
theme$={theme$}
history={history}
authc={authc}
onChange={createBreadcrumbsChangeHandler(coreStart.chrome, setBreadcrumbs)}
@ -81,6 +90,7 @@ export const apiKeysManagementApp = Object.freeze({
export interface ProvidersProps {
services: CoreStart;
theme$: Observable<CoreTheme>;
history: History;
authc: AuthenticationServiceSetup;
onChange?: BreadcrumbsChangeHandler;
@ -88,6 +98,7 @@ export interface ProvidersProps {
export const Providers: FunctionComponent<ProvidersProps> = ({
services,
theme$,
history,
authc,
onChange,
@ -96,9 +107,11 @@ export const Providers: FunctionComponent<ProvidersProps> = ({
<KibanaContextProvider services={services}>
<AuthenticationProvider authc={authc}>
<I18nProvider>
<Router history={history}>
<BreadcrumbsProvider onChange={onChange}>{children}</BreadcrumbsProvider>
</Router>
<KibanaThemeProvider theme$={theme$}>
<Router history={history}>
<BreadcrumbsProvider onChange={onChange}>{children}</BreadcrumbsProvider>
</Router>
</KibanaThemeProvider>
</I18nProvider>
</AuthenticationProvider>
</KibanaContextProvider>

View file

@ -13,7 +13,10 @@ import { i18n } from '@kbn/i18n';
import type { StartServicesAccessor } from 'src/core/public';
import type { RegisterManagementAppArgs } from 'src/plugins/management/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import {
KibanaContextProvider,
KibanaThemeProvider,
} from '../../../../../../src/plugins/kibana_react/public';
import {
Breadcrumb,
BreadcrumbsProvider,
@ -37,7 +40,7 @@ export const roleMappingsManagementApp = Object.freeze({
id: this.id,
order: 40,
title,
async mount({ element, setBreadcrumbs, history }) {
async mount({ element, theme$, setBreadcrumbs, history }) {
const [
[core],
{ RoleMappingsGridPage },
@ -90,30 +93,32 @@ export const roleMappingsManagementApp = Object.freeze({
render(
<KibanaContextProvider services={core}>
<core.i18n.Context>
<Router history={history}>
<BreadcrumbsProvider
onChange={createBreadcrumbsChangeHandler(core.chrome, setBreadcrumbs)}
>
<Breadcrumb text={title} href="/">
<Route path={['/', '']} exact={true}>
<RoleMappingsGridPage
notifications={core.notifications}
rolesAPIClient={new RolesAPIClient(core.http)}
roleMappingsAPI={roleMappingsAPIClient}
docLinks={core.docLinks}
history={history}
navigateToApp={core.application.navigateToApp}
/>
</Route>
<Route path="/edit/:name?">
<EditRoleMappingsPageWithBreadcrumbs action="edit" />
</Route>
<Route path="/clone/:name">
<EditRoleMappingsPageWithBreadcrumbs action="clone" />
</Route>
</Breadcrumb>
</BreadcrumbsProvider>
</Router>
<KibanaThemeProvider theme$={theme$}>
<Router history={history}>
<BreadcrumbsProvider
onChange={createBreadcrumbsChangeHandler(core.chrome, setBreadcrumbs)}
>
<Breadcrumb text={title} href="/">
<Route path={['/', '']} exact={true}>
<RoleMappingsGridPage
notifications={core.notifications}
rolesAPIClient={new RolesAPIClient(core.http)}
roleMappingsAPI={roleMappingsAPIClient}
docLinks={core.docLinks}
history={history}
navigateToApp={core.application.navigateToApp}
/>
</Route>
<Route path="/edit/:name?">
<EditRoleMappingsPageWithBreadcrumbs action="edit" />
</Route>
<Route path="/clone/:name">
<EditRoleMappingsPageWithBreadcrumbs action="clone" />
</Route>
</Breadcrumb>
</BreadcrumbsProvider>
</Router>
</KibanaThemeProvider>
</core.i18n.Context>
</KibanaContextProvider>,
element

View file

@ -13,7 +13,10 @@ import { i18n } from '@kbn/i18n';
import type { FatalErrorsSetup, StartServicesAccessor } from 'src/core/public';
import type { RegisterManagementAppArgs } from 'src/plugins/management/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import {
KibanaContextProvider,
KibanaThemeProvider,
} from '../../../../../../src/plugins/kibana_react/public';
import type { SecurityLicense } from '../../../common/licensing';
import {
Breadcrumb,
@ -39,7 +42,7 @@ export const rolesManagementApp = Object.freeze({
id: this.id,
order: 20,
title,
async mount({ element, setBreadcrumbs, history }) {
async mount({ element, theme$, setBreadcrumbs, history }) {
const [
[startServices, { data, features, spaces }],
{ RolesGridPage },
@ -116,27 +119,29 @@ export const rolesManagementApp = Object.freeze({
render(
<KibanaContextProvider services={startServices}>
<i18nStart.Context>
<Router history={history}>
<BreadcrumbsProvider
onChange={createBreadcrumbsChangeHandler(chrome, setBreadcrumbs)}
>
<Breadcrumb text={title} href="/">
<Route path={['/', '']} exact={true}>
<RolesGridPage
notifications={notifications}
rolesAPIClient={rolesAPIClient}
history={history}
/>
</Route>
<Route path="/edit/:roleName?">
<EditRolePageWithBreadcrumbs action="edit" />
</Route>
<Route path="/clone/:roleName">
<EditRolePageWithBreadcrumbs action="clone" />
</Route>
</Breadcrumb>
</BreadcrumbsProvider>
</Router>
<KibanaThemeProvider theme$={theme$}>
<Router history={history}>
<BreadcrumbsProvider
onChange={createBreadcrumbsChangeHandler(chrome, setBreadcrumbs)}
>
<Breadcrumb text={title} href="/">
<Route path={['/', '']} exact={true}>
<RolesGridPage
notifications={notifications}
rolesAPIClient={rolesAPIClient}
history={history}
/>
</Route>
<Route path="/edit/:roleName?">
<EditRolePageWithBreadcrumbs action="edit" />
</Route>
<Route path="/clone/:roleName">
<EditRolePageWithBreadcrumbs action="clone" />
</Route>
</Breadcrumb>
</BreadcrumbsProvider>
</Router>
</KibanaThemeProvider>
</i18nStart.Context>
</KibanaContextProvider>,
element

View file

@ -9,7 +9,7 @@ import { fireEvent, render, waitFor, within } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { securityMock } from '../../../mocks';
import { Providers } from '../users_management_app';
@ -22,6 +22,8 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
describe('CreateUserPage', () => {
jest.setTimeout(15_000);
const theme$ = themeServiceMock.createTheme$();
it('creates user when submitting form and redirects back', async () => {
const coreStart = coreMock.createStart();
const history = createMemoryHistory({ initialEntries: ['/create'] });
@ -29,7 +31,7 @@ describe('CreateUserPage', () => {
coreStart.http.post.mockResolvedValue({});
const { findByRole, findByLabelText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<CreateUserPage />
</Providers>
);
@ -72,7 +74,7 @@ describe('CreateUserPage', () => {
]);
const { findAllByText, findByRole, findByLabelText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<CreateUserPage />
</Providers>
);

View file

@ -9,7 +9,7 @@ import { fireEvent, render } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { coreMock, themeServiceMock } from 'src/core/public/mocks';
import { securityMock } from '../../../mocks';
import { Providers } from '../users_management_app';
@ -25,6 +25,7 @@ const userMock = {
describe('EditUserPage', () => {
const coreStart = coreMock.createStart();
const theme$ = themeServiceMock.createTheme$();
let history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] });
const authc = securityMock.createSetup().authc;
@ -46,7 +47,7 @@ describe('EditUserPage', () => {
coreStart.http.get.mockResolvedValueOnce([]);
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
@ -66,7 +67,7 @@ describe('EditUserPage', () => {
coreStart.http.get.mockResolvedValueOnce([]);
const { findByRole, findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
@ -87,7 +88,7 @@ describe('EditUserPage', () => {
coreStart.http.get.mockResolvedValueOnce([]);
const { findByRole, findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);
@ -117,7 +118,7 @@ describe('EditUserPage', () => {
]);
const { findByText } = render(
<Providers services={coreStart} authc={authc} history={history}>
<Providers services={coreStart} theme$={theme$} authc={authc} history={history}>
<EditUserPage username={userMock.username} />
</Providers>
);

View file

@ -11,13 +11,17 @@ import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import type { RouteComponentProps } from 'react-router-dom';
import { Redirect, Route, Router, Switch } from 'react-router-dom';
import type { Observable } from 'rxjs';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import type { CoreStart, StartServicesAccessor } from 'src/core/public';
import type { CoreStart, CoreTheme, StartServicesAccessor } from 'src/core/public';
import type { RegisterManagementAppArgs } from 'src/plugins/management/public';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import {
KibanaContextProvider,
KibanaThemeProvider,
} from '../../../../../../src/plugins/kibana_react/public';
import type { AuthenticationServiceSetup } from '../../authentication';
import type { BreadcrumbsChangeHandler } from '../../components/breadcrumb';
import {
@ -48,7 +52,7 @@ export const usersManagementApp = Object.freeze({
id: this.id,
order: 10,
title,
async mount({ element, setBreadcrumbs, history }) {
async mount({ element, theme$, setBreadcrumbs, history }) {
const [
[coreStart],
{ UsersGridPage },
@ -66,6 +70,7 @@ export const usersManagementApp = Object.freeze({
render(
<Providers
services={coreStart}
theme$={theme$}
history={history}
authc={authc}
onChange={createBreadcrumbsChangeHandler(coreStart.chrome, setBreadcrumbs)}
@ -128,6 +133,7 @@ export const usersManagementApp = Object.freeze({
export interface ProvidersProps {
services: CoreStart;
theme$: Observable<CoreTheme>;
history: History;
authc: AuthenticationServiceSetup;
onChange?: BreadcrumbsChangeHandler;
@ -135,6 +141,7 @@ export interface ProvidersProps {
export const Providers: FunctionComponent<ProvidersProps> = ({
services,
theme$,
history,
authc,
onChange,
@ -143,9 +150,11 @@ export const Providers: FunctionComponent<ProvidersProps> = ({
<KibanaContextProvider services={services}>
<AuthenticationProvider authc={authc}>
<I18nProvider>
<Router history={history}>
<BreadcrumbsProvider onChange={onChange}>{children}</BreadcrumbsProvider>
</Router>
<KibanaThemeProvider theme$={theme$}>
<Router history={history}>
<BreadcrumbsProvider onChange={onChange}>{children}</BreadcrumbsProvider>
</Router>
</KibanaThemeProvider>
</I18nProvider>
</AuthenticationProvider>
</KibanaContextProvider>

View file

@ -14,6 +14,7 @@ import { map, takeUntil } from 'rxjs/operators';
import type { CoreStart } from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import type { SecurityLicense } from '../../common/licensing';
import type { AuthenticationServiceSetup } from '../authentication';
import type { UserMenuLink } from './nav_control_component';
@ -110,8 +111,9 @@ export class SecurityNavControlService {
}
private registerSecurityNavControl(
core: Pick<CoreStart, 'chrome' | 'http' | 'i18n' | 'injectedMetadata' | 'application'>
core: Pick<CoreStart, 'chrome' | 'http' | 'i18n' | 'injectedMetadata' | 'application' | 'theme'>
) {
const { theme$ } = core.theme;
const currentUserPromise = this.authc.getCurrentUser();
core.chrome.navControls.registerRight({
order: 2000,
@ -126,7 +128,9 @@ export class SecurityNavControlService {
};
ReactDOM.render(
<I18nContext>
<SecurityNavControl {...props} />
<KibanaThemeProvider theme$={theme$}>
<SecurityNavControl {...props} />
</KibanaThemeProvider>
</I18nContext>,
el
);

View file

@ -16,6 +16,7 @@ import type { RegisterManagementAppArgs } from 'src/plugins/management/public';
import { APP_WRAPPER_CLASS } from '../../../../../src/core/public';
import {
KibanaContextProvider,
KibanaThemeProvider,
RedirectAppLinks,
} from '../../../../../src/plugins/kibana_react/public';
import type { Space } from '../../common';
@ -39,7 +40,7 @@ export const spacesManagementApp = Object.freeze({
order: 2,
title,
async mount({ element, setBreadcrumbs, history }) {
async mount({ element, theme$, setBreadcrumbs, history }) {
const [[coreStart, { features }], { SpacesGridPage }, { ManageSpacePage }] =
await Promise.all([getStartServices(), import('./spaces_grid'), import('./edit_space')]);
@ -114,21 +115,23 @@ export const spacesManagementApp = Object.freeze({
render(
<KibanaContextProvider services={coreStart}>
<i18nStart.Context>
<RedirectAppLinks application={application} className={APP_WRAPPER_CLASS}>
<Router history={history}>
<Switch>
<Route path={['', '/']} exact>
<SpacesGridPageWithBreadcrumbs />
</Route>
<Route path="/create">
<CreateSpacePageWithBreadcrumbs />
</Route>
<Route path="/edit/:spaceId">
<EditSpacePageWithBreadcrumbs />
</Route>
</Switch>
</Router>
</RedirectAppLinks>
<KibanaThemeProvider theme$={theme$}>
<RedirectAppLinks application={application} className={APP_WRAPPER_CLASS}>
<Router history={history}>
<Switch>
<Route path={['', '/']} exact>
<SpacesGridPageWithBreadcrumbs />
</Route>
<Route path="/create">
<CreateSpacePageWithBreadcrumbs />
</Route>
<Route path="/edit/:spaceId">
<EditSpacePageWithBreadcrumbs />
</Route>
</Switch>
</Router>
</RedirectAppLinks>
</KibanaThemeProvider>
</i18nStart.Context>
</KibanaContextProvider>,
element

View file

@ -11,10 +11,12 @@ import ReactDOM from 'react-dom';
import type { CoreStart } from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import type { SpacesManager } from '../spaces_manager';
export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreStart) {
const I18nContext = core.i18n.Context;
const { theme$ } = core.theme;
core.chrome.navControls.registerLeft({
order: 1000,
mount(targetDomElement: HTMLElement) {
@ -30,15 +32,17 @@ export function initSpacesNavControl(spacesManager: SpacesManager, core: CoreSta
ReactDOM.render(
<I18nContext>
<Suspense fallback={<EuiLoadingSpinner />}>
<LazyNavControlPopover
spacesManager={spacesManager}
serverBasePath={core.http.basePath.serverBasePath}
anchorPosition="downLeft"
capabilities={core.application.capabilities}
navigateToApp={core.application.navigateToApp}
/>
</Suspense>
<KibanaThemeProvider theme$={theme$}>
<Suspense fallback={<EuiLoadingSpinner />}>
<LazyNavControlPopover
spacesManager={spacesManager}
serverBasePath={core.http.basePath.serverBasePath}
anchorPosition="downLeft"
capabilities={core.application.capabilities}
navigateToApp={core.application.navigateToApp}
/>
</Suspense>
</KibanaThemeProvider>
</I18nContext>,
targetDomElement
);

View file

@ -27,8 +27,9 @@ import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart } from 'src/core/public';
import type { AppMountParameters, CoreStart } from 'src/core/public';
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
import type { Space } from '../../common';
import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../common/constants';
import type { SpacesManager } from '../spaces_manager';
@ -213,12 +214,18 @@ export class SpaceSelector extends Component<Props, State> {
};
}
export const renderSpaceSelectorApp = (i18nStart: CoreStart['i18n'], el: Element, props: Props) => {
export const renderSpaceSelectorApp = (
i18nStart: CoreStart['i18n'],
{ element, theme$ }: Pick<AppMountParameters, 'element' | 'theme$'>,
props: Props
) => {
ReactDOM.render(
<i18nStart.Context>
<SpaceSelector {...props} />
<KibanaThemeProvider theme$={theme$}>
<SpaceSelector {...props} />
</KibanaThemeProvider>
</i18nStart.Context>,
el
element
);
return () => ReactDOM.unmountComponentAtNode(el);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -26,15 +26,19 @@ export const spaceSelectorApp = Object.freeze({
}),
chromeless: true,
appRoute: '/spaces/space_selector',
mount: async (params: AppMountParameters) => {
mount: async ({ element, theme$ }: AppMountParameters) => {
const [[coreStart], { renderSpaceSelectorApp }] = await Promise.all([
getStartServices(),
import('./space_selector'),
]);
return renderSpaceSelectorApp(coreStart.i18n, params.element, {
spacesManager,
serverBasePath: coreStart.http.basePath.serverBasePath,
});
return renderSpaceSelectorApp(
coreStart.i18n,
{ element, theme$ },
{
spacesManager,
serverBasePath: coreStart.http.basePath.serverBasePath,
}
);
},
});
},