mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SharedUX] Custom branding service (#148273)
## Summary This PR adds a new `CustomBranding` service and exposes it from core, both on the server and client side. The purpose of the service is to retrieve custom branding properties and propagate them to the appropriate core service (`chrome` on the client-side and `rendering` on the server-side). The client side receives server-side properties through `injectedMetadata`. Note that the service itself is not responsible for reading the properties from `uiSettings`; this task is offloaded to `customBranding` plugin. I deployed one of the previous commits [here]([ttps://majagrubic-pr-148273-custom-branding-service-server.kbndev.co/aiy/app/home#/](https://majagrubic-pr-148273-custom-branding-service-server.kbndev.co/aiy/app/home#/)), so you can see a custom logo set (client-side) and page title set (server-side). ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ~- [] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/))~ ~- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~ ~- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~ ~- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))~ ~- [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)~ ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d2788bcad7
commit
4522e04287
114 changed files with 1252 additions and 52 deletions
|
@ -15,6 +15,7 @@ import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
|||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import type { AppMountParameters, AppUpdater } from '@kbn/core-application-browser';
|
||||
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
|
||||
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
|
||||
import type { MountPoint } from '@kbn/core-mount-utils-browser';
|
||||
import type { MockLifecycle } from '../src/test_helpers/test_types';
|
||||
import { ApplicationService } from '../src/application_service';
|
||||
|
@ -48,6 +49,7 @@ describe('ApplicationService', () => {
|
|||
http,
|
||||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
|
|
@ -67,6 +67,14 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = `
|
|||
"thrownError": null,
|
||||
}
|
||||
}
|
||||
hasCustomBranding$={
|
||||
Observable {
|
||||
"operator": [Function],
|
||||
"source": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
history={
|
||||
Object {
|
||||
"push": [MockFunction],
|
||||
|
|
|
@ -20,6 +20,7 @@ import { mount, shallow } from 'enzyme';
|
|||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
|
||||
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
|
||||
import { MockLifecycle } from './test_helpers/test_types';
|
||||
import { ApplicationService } from './application_service';
|
||||
import {
|
||||
|
@ -56,6 +57,7 @@ describe('#setup()', () => {
|
|||
http,
|
||||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
@ -480,6 +482,7 @@ describe('#start()', () => {
|
|||
http,
|
||||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
@ -1189,6 +1192,7 @@ describe('#stop()', () => {
|
|||
http,
|
||||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
|
|
@ -29,6 +29,7 @@ import type {
|
|||
} from '@kbn/core-application-browser';
|
||||
import { CapabilitiesService } from '@kbn/core-capabilities-browser-internal';
|
||||
import { AppStatus, AppNavLinkStatus } from '@kbn/core-application-browser';
|
||||
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
|
||||
import { AppRouter } from './ui';
|
||||
import type { InternalApplicationSetup, InternalApplicationStart, Mounter } from './types';
|
||||
|
||||
|
@ -47,6 +48,7 @@ export interface StartDeps {
|
|||
http: HttpStart;
|
||||
theme: ThemeServiceStart;
|
||||
overlays: OverlayStart;
|
||||
customBranding: CustomBrandingStart;
|
||||
}
|
||||
|
||||
function filterAvailable<T>(m: Map<string, T>, capabilities: Capabilities) {
|
||||
|
@ -105,6 +107,7 @@ export class ApplicationService {
|
|||
private openInNewTab?: (url: string) => void;
|
||||
private redirectTo?: (url: string) => void;
|
||||
private overlayStart$ = new Subject<OverlayStart>();
|
||||
private hasCustomBranding$: Observable<boolean> | undefined;
|
||||
|
||||
public setup({
|
||||
http: { basePath },
|
||||
|
@ -204,13 +207,18 @@ export class ApplicationService {
|
|||
};
|
||||
}
|
||||
|
||||
public async start({ http, overlays, theme }: StartDeps): Promise<InternalApplicationStart> {
|
||||
public async start({
|
||||
http,
|
||||
overlays,
|
||||
theme,
|
||||
customBranding,
|
||||
}: StartDeps): Promise<InternalApplicationStart> {
|
||||
if (!this.redirectTo) {
|
||||
throw new Error('ApplicationService#setup() must be invoked before start.');
|
||||
}
|
||||
|
||||
this.overlayStart$.next(overlays);
|
||||
|
||||
this.hasCustomBranding$ = customBranding.hasCustomBranding$.pipe(takeUntil(this.stop$));
|
||||
const httpLoadingCount$ = new BehaviorSubject(0);
|
||||
http.addLoadingCountSource(httpLoadingCount$);
|
||||
|
||||
|
@ -345,6 +353,7 @@ export class ApplicationService {
|
|||
setAppLeaveHandler={this.setAppLeaveHandler}
|
||||
setAppActionMenu={this.setAppActionMenu}
|
||||
setIsMounting={(isMounting) => httpLoadingCount$.next(isMounting ? 1 : 0)}
|
||||
hasCustomBranding$={this.hasCustomBranding$}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -184,6 +184,30 @@ describe('AppContainer', () => {
|
|||
expect(setIsMounting).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('should show plain spinner', async () => {
|
||||
const [waitPromise] = createResolver();
|
||||
const mounter = createMounter(waitPromise);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<AppContainer
|
||||
appPath={`/app/${appId}`}
|
||||
appId={appId}
|
||||
appStatus={AppStatus.accessible}
|
||||
mounter={mounter}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
setAppActionMenu={setAppActionMenu}
|
||||
setIsMounting={setIsMounting}
|
||||
createScopedHistory={(appPath: string) =>
|
||||
// Create a history using the appPath as the current location
|
||||
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
|
||||
}
|
||||
theme$={theme$}
|
||||
showPlainSpinner={true}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="appContainer-loadingSpinner"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call setIsMounting(false) if mounting throws', async () => {
|
||||
const [waitPromise, resolvePromise] = createResolver();
|
||||
const mounter = {
|
||||
|
|
|
@ -10,7 +10,7 @@ import './app_container.scss';
|
|||
|
||||
import { Observable } from 'rxjs';
|
||||
import React, { Fragment, FC, useLayoutEffect, useRef, useState, MutableRefObject } from 'react';
|
||||
import { EuiLoadingElastic } from '@elastic/eui';
|
||||
import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CoreTheme } from '@kbn/core-theme-browser';
|
||||
|
@ -36,6 +36,7 @@ interface Props {
|
|||
setAppActionMenu: (appId: string, mount: MountPoint | undefined) => void;
|
||||
createScopedHistory: (appUrl: string) => ScopedHistory;
|
||||
setIsMounting: (isMounting: boolean) => void;
|
||||
showPlainSpinner?: boolean;
|
||||
}
|
||||
|
||||
export const AppContainer: FC<Props> = ({
|
||||
|
@ -48,6 +49,7 @@ export const AppContainer: FC<Props> = ({
|
|||
appStatus,
|
||||
setIsMounting,
|
||||
theme$,
|
||||
showPlainSpinner,
|
||||
}: Props) => {
|
||||
const [showSpinner, setShowSpinner] = useState(true);
|
||||
const [appNotFound, setAppNotFound] = useState(false);
|
||||
|
@ -114,13 +116,18 @@ export const AppContainer: FC<Props> = ({
|
|||
return (
|
||||
<Fragment>
|
||||
{appNotFound && <AppNotFound />}
|
||||
{showSpinner && !appNotFound && <AppLoadingPlaceholder />}
|
||||
{showSpinner && !appNotFound && (
|
||||
<AppLoadingPlaceholder showPlainSpinner={Boolean(showPlainSpinner)} />
|
||||
)}
|
||||
<div className={APP_WRAPPER_CLASS} key={appId} ref={elementRef} aria-busy={showSpinner} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const AppLoadingPlaceholder: FC = () => {
|
||||
const AppLoadingPlaceholder: FC<{ showPlainSpinner: boolean }> = ({ showPlainSpinner }) => {
|
||||
if (showPlainSpinner) {
|
||||
return <EuiLoadingSpinner size={'xxl'} data-test-subj="appContainer-loadingSpinner" />;
|
||||
}
|
||||
return (
|
||||
<EuiLoadingElastic
|
||||
className="appContainer__loading"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom';
|
||||
import { History } from 'history';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import type { CoreTheme } from '@kbn/core-theme-browser';
|
||||
|
@ -27,6 +27,7 @@ interface Props {
|
|||
setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void;
|
||||
setAppActionMenu: (appId: string, mount: MountPoint | undefined) => void;
|
||||
setIsMounting: (isMounting: boolean) => void;
|
||||
hasCustomBranding$?: Observable<boolean>;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
|
@ -41,6 +42,7 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
setAppActionMenu,
|
||||
appStatuses$,
|
||||
setIsMounting,
|
||||
hasCustomBranding$,
|
||||
}) => {
|
||||
const appStatuses = useObservable(appStatuses$, new Map());
|
||||
const createScopedHistory = useMemo(
|
||||
|
@ -48,6 +50,8 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
[history]
|
||||
);
|
||||
|
||||
const showPlainSpinner = useObservable(hasCustomBranding$ ?? EMPTY, false);
|
||||
|
||||
return (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
|
@ -61,7 +65,15 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
appPath={path}
|
||||
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
|
||||
createScopedHistory={createScopedHistory}
|
||||
{...{ appId, mounter, setAppLeaveHandler, setAppActionMenu, setIsMounting, theme$ }}
|
||||
{...{
|
||||
appId,
|
||||
mounter,
|
||||
setAppLeaveHandler,
|
||||
setAppActionMenu,
|
||||
setIsMounting,
|
||||
theme$,
|
||||
showPlainSpinner,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -83,7 +95,14 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
appId={id ?? appId}
|
||||
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
|
||||
createScopedHistory={createScopedHistory}
|
||||
{...{ mounter, setAppLeaveHandler, setAppActionMenu, setIsMounting, theme$ }}
|
||||
{...{
|
||||
mounter,
|
||||
setAppLeaveHandler,
|
||||
setAppActionMenu,
|
||||
setIsMounting,
|
||||
theme$,
|
||||
showPlainSpinner,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-http-browser-internal",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/core-custom-branding-browser",
|
||||
"@kbn/core-custom-branding-browser-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue