[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:
Maja Grubic 2023-01-11 15:28:10 +01:00 committed by GitHub
parent d2788bcad7
commit 4522e04287
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
114 changed files with 1252 additions and 52 deletions

7
.github/CODEOWNERS vendored
View file

@ -724,6 +724,13 @@ packages/core/chrome/core-chrome-browser @elastic/kibana-core
packages/core/chrome/core-chrome-browser-internal @elastic/kibana-core
packages/core/chrome/core-chrome-browser-mocks @elastic/kibana-core
packages/core/config/core-config-server-internal @elastic/kibana-core
packages/core/custom-branding/core-custom-branding-browser @elastic/kibana-global-experience
packages/core/custom-branding/core-custom-branding-browser-internal @elastic/kibana-global-experience
packages/core/custom-branding/core-custom-branding-browser-mocks @elastic/kibana-global-experience
packages/core/custom-branding/core-custom-branding-common @elastic/kibana-global-experience
packages/core/custom-branding/core-custom-branding-server @elastic/kibana-global-experience
packages/core/custom-branding/core-custom-branding-server-internal @elastic/kibana-global-experience
packages/core/custom-branding/core-custom-branding-server-mocks @elastic/kibana-global-experience
packages/core/deprecations/core-deprecations-browser @elastic/kibana-core
packages/core/deprecations/core-deprecations-browser-internal @elastic/kibana-core
packages/core/deprecations/core-deprecations-browser-mocks @elastic/kibana-core

View file

@ -180,6 +180,13 @@
"@kbn/core-chrome-browser-internal": "link:packages/core/chrome/core-chrome-browser-internal",
"@kbn/core-chrome-browser-mocks": "link:packages/core/chrome/core-chrome-browser-mocks",
"@kbn/core-config-server-internal": "link:packages/core/config/core-config-server-internal",
"@kbn/core-custom-branding-browser": "link:packages/core/custom-branding/core-custom-branding-browser",
"@kbn/core-custom-branding-browser-internal": "link:packages/core/custom-branding/core-custom-branding-browser-internal",
"@kbn/core-custom-branding-browser-mocks": "link:packages/core/custom-branding/core-custom-branding-browser-mocks",
"@kbn/core-custom-branding-common": "link:packages/core/custom-branding/core-custom-branding-common",
"@kbn/core-custom-branding-server": "link:packages/core/custom-branding/core-custom-branding-server",
"@kbn/core-custom-branding-server-internal": "link:packages/core/custom-branding/core-custom-branding-server-internal",
"@kbn/core-custom-branding-server-mocks": "link:packages/core/custom-branding/core-custom-branding-server-mocks",
"@kbn/core-deprecations-browser": "link:packages/core/deprecations/core-deprecations-browser",
"@kbn/core-deprecations-browser-internal": "link:packages/core/deprecations/core-deprecations-browser-internal",
"@kbn/core-deprecations-browser-mocks": "link:packages/core/deprecations/core-deprecations-browser-mocks",

View file

@ -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();
});

View file

@ -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],

View file

@ -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();
});

View file

@ -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$}
/>
);
},

View file

@ -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 = {

View file

@ -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"

View file

@ -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,
}}
/>
);
}}

View file

@ -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/**/*",

View file

@ -17,6 +17,7 @@ import type { App, PublicAppInfo } from '@kbn/core-application-browser';
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
import { getAppInfo } from '@kbn/core-application-browser-internal';
import { ChromeService } from './chrome_service';
@ -46,6 +47,7 @@ function defaultStartDeps(availableApps?: App[]) {
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
customBranding: customBrandingServiceMock.createStartContract(),
};
if (availableApps) {

View file

@ -27,6 +27,7 @@ import type {
ChromeHelpExtension,
ChromeUserBanner,
} from '@kbn/core-chrome-browser';
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
import { KIBANA_ASK_ELASTIC_LINK } from './constants';
import { DocTitleService } from './doc_title';
import { NavControlsService } from './nav_controls';
@ -49,6 +50,7 @@ export interface StartDeps {
http: HttpStart;
injectedMetadata: InternalInjectedMetadataStart;
notifications: NotificationsStart;
customBranding: CustomBrandingStart;
}
/** @internal */
@ -101,6 +103,7 @@ export class ChromeService {
http,
injectedMetadata,
notifications,
customBranding,
}: StartDeps): Promise<InternalChromeStart> {
this.initVisibility(application);
@ -143,6 +146,7 @@ export class ChromeService {
const navLinks = this.navLinks.start({ application, http });
const recentlyAccessed = await this.recentlyAccessed.start({ http });
const docTitle = this.docTitle.start({ document: window.document });
const { customBranding$ } = customBranding;
// erase chrome fields from a previous app while switching to a next app
application.currentAppId$.subscribe(() => {
@ -231,6 +235,7 @@ export class ChromeService {
navControlsExtension$={navControls.getExtension$()}
onIsLockedUpdate={setIsNavDrawerLocked}
isLocked$={getIsNavDrawerLocked$}
customBranding$={customBranding$}
/>
),

View file

@ -19,3 +19,12 @@ exports[`kbnLoadingIndicator is visible when loadingCount is > 0 1`] = `
type="logoElastic"
/>
`;
exports[`kbnLoadingIndicator shows EuiLoadingSpinner when showPlainSpinner is true 1`] = `
<EuiLoadingSpinner
aria-hidden={false}
aria-label="Loading content"
data-test-subj="globalLoadingIndicator"
size="l"
/>
`;

View file

@ -28,6 +28,7 @@ function mockProps() {
breadcrumbsAppendExtension$: new BehaviorSubject(undefined),
homeHref: '/',
isVisible$: new BehaviorSubject(true),
customBranding$: new BehaviorSubject({}),
kibanaDocLink: '/docs',
navLinks$: new BehaviorSubject([]),
customNavLink$: new BehaviorSubject(undefined),

View file

@ -35,6 +35,7 @@ import type {
ChromeGlobalHelpExtensionMenuLink,
ChromeUserBanner,
} from '@kbn/core-chrome-browser';
import { CustomBranding } from '@kbn/core-custom-branding-common';
import { LoadingIndicator } from '../loading_indicator';
import type { OnIsLockedUpdate } from './types';
import { CollapsibleNav } from './collapsible_nav';
@ -72,6 +73,7 @@ export interface HeaderProps {
isLocked$: Observable<boolean>;
loadingCount$: ReturnType<HttpStart['getLoadingCount$']>;
onIsLockedUpdate: OnIsLockedUpdate;
customBranding$: Observable<CustomBranding>;
}
export function Header({
@ -83,6 +85,7 @@ export function Header({
homeHref,
breadcrumbsAppendExtension$,
globalHelpExtensionMenuLinks$,
customBranding$,
...observables
}: HeaderProps) {
const isVisible = useObservable(observables.isVisible$, false);
@ -122,6 +125,7 @@ export function Header({
navLinks$={observables.navLinks$}
navigateToApp={application.navigateToApp}
loadingCount$={observables.loadingCount$}
customBranding$={customBranding$}
/>,
],
borders: 'none',

View file

@ -12,6 +12,7 @@ import React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { Observable } from 'rxjs';
import Url from 'url';
import { CustomBranding } from '@kbn/core-custom-branding-common';
import type { HttpStart } from '@kbn/core-http-browser';
import type { ChromeNavLink } from '@kbn/core-chrome-browser';
import { ElasticMark } from './elastic_mark';
@ -83,12 +84,14 @@ interface Props {
forceNavigation$: Observable<boolean>;
navigateToApp: (appId: string) => void;
loadingCount$?: ReturnType<HttpStart['getLoadingCount$']>;
customBranding$: Observable<CustomBranding>;
}
export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables }: Props) {
const forceNavigation = useObservable(observables.forceNavigation$, false);
const navLinks = useObservable(observables.navLinks$, []);
const customBranding = useObservable(observables.customBranding$, {});
const { customizedLogo, logo } = customBranding;
return (
<a
onClick={(e) => onClick(e, forceNavigation, navLinks, navigateToApp)}
@ -99,8 +102,15 @@ export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables
defaultMessage: 'Elastic home',
})}
>
<LoadingIndicator loadingCount$={loadingCount$!} />
<ElasticMark className="chrHeaderLogo__mark" aria-hidden={true} />
<LoadingIndicator
loadingCount$={loadingCount$!}
showPlainSpinner={Boolean(logo || customizedLogo)}
/>
{customizedLogo ? (
<img src={customizedLogo} width="200" height="84" alt="custom mark" />
) : (
<ElasticMark className="chrHeaderLogo__mark" aria-hidden={true} />
)}
</a>
);
}

View file

@ -27,4 +27,15 @@ describe('kbnLoadingIndicator', () => {
}, 300);
expect(wrapper).toMatchSnapshot();
});
it('shows EuiLoadingSpinner when showPlainSpinner is true', () => {
const wrapper = shallow(
<LoadingIndicator loadingCount$={new BehaviorSubject(1)} showPlainSpinner={true} />
);
// Pause the check beyond the 250ms delay that it has
setTimeout(() => {
expect(wrapper.prop('data-test-subj')).toBe('globalLoadingIndicator');
}, 300);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -18,6 +18,7 @@ import './loading_indicator.scss';
export interface LoadingIndicatorProps {
loadingCount$: ReturnType<HttpStart['getLoadingCount$']>;
showAsBar?: boolean;
showPlainSpinner?: boolean;
}
export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { visible: boolean }> {
@ -57,34 +58,36 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
render() {
const className = classNames(!this.state.visible && 'kbnLoadingIndicator-hidden');
const testSubj = this.state.visible
? 'globalLoadingIndicator'
: 'globalLoadingIndicator-hidden';
const testSubj =
this.state.visible || this.props.showPlainSpinner
? 'globalLoadingIndicator'
: 'globalLoadingIndicator-hidden';
const ariaHidden = this.state.visible ? false : true;
const ariaHidden = !this.state.visible;
const ariaLabel = i18n.translate('core.ui.loadingIndicatorAriaLabel', {
defaultMessage: 'Loading content',
});
const logo = this.state.visible ? (
<EuiLoadingSpinner
size="l"
data-test-subj={testSubj}
aria-hidden={false}
aria-label={ariaLabel}
/>
) : (
<EuiIcon
type="logoElastic"
size="l"
data-test-subj={testSubj}
className="chrHeaderLogo__cluster"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.logoAriaLabel', {
defaultMessage: 'Elastic Logo',
})}
/>
);
const logo =
this.state.visible || this.props.showPlainSpinner ? (
<EuiLoadingSpinner
size="l"
data-test-subj={testSubj}
aria-hidden={false}
aria-label={ariaLabel}
/>
) : (
<EuiIcon
type={'logoElastic'}
size="l"
data-test-subj={testSubj}
className="chrHeaderLogo__cluster"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.logoAriaLabel', {
defaultMessage: 'Elastic Logo',
})}
/>
);
return !this.props.showAsBar ? (
logo

View file

@ -33,6 +33,9 @@
"@kbn/test-jest-helpers",
"@kbn/core-application-common",
"@kbn/core-mount-utils-browser",
"@kbn/core-custom-branding-browser-mocks",
"@kbn/core-custom-branding-browser",
"@kbn/core-custom-branding-common",
],
"exclude": [
"target/**/*",

View file

@ -0,0 +1,3 @@
# @kbn/core-custom-branding-browser-internal
This package contains the implementation and internal types of the browser-side `customBranding` service.

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 { CustomBrandingService } from './src/custom_branding_service';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../',
roots: ['<rootDir>/packages/core/custom-branding/core-custom-branding-browser-internal'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-browser-internal",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-browser-internal",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,91 @@
/*
* 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 { take } from 'rxjs';
import { CustomBrandingSetupDeps } from '@kbn/core-custom-branding-browser';
import { CustomBrandingService } from './custom_branding_service';
describe('custom branding service', () => {
const injectedMetadata = {
getCustomBranding: () => {
return { customizedLogo: 'customizedLogo' };
},
};
describe('#start', () => {
let service: CustomBrandingService;
beforeEach(() => {
service = new CustomBrandingService();
service.setup({ injectedMetadata } as CustomBrandingSetupDeps);
});
it('hasCustomBranding$ returns the correct value', async () => {
const { hasCustomBranding$ } = service.start();
const hasCustomBranding = await hasCustomBranding$.pipe(take(1)).toPromise();
expect(hasCustomBranding).toEqual(true);
});
it('customBranding$ returns the correct value', async () => {
const { customBranding$ } = service.start();
const customBranding = await customBranding$.pipe(take(1)).toPromise();
expect(customBranding).toEqual({ customizedLogo: 'customizedLogo' });
});
it('throws if called before setup', async () => {
const customBrandingService = new CustomBrandingService();
expect(() => customBrandingService.start()).toThrow('Setup needs to be called before start');
});
});
describe('#setup', () => {
it('customBranding$ returns the correct value', async () => {
const service = new CustomBrandingService();
const { customBranding$, hasCustomBranding$ } = service.setup({
injectedMetadata,
} as CustomBrandingSetupDeps);
const customBranding = await customBranding$.pipe(take(1)).toPromise();
expect(customBranding).toEqual({ customizedLogo: 'customizedLogo' });
const hasCustomBranding = await hasCustomBranding$.pipe(take(1)).toPromise();
expect(hasCustomBranding).toBe(true);
});
});
describe('#stop', () => {
it('runs fine if service never set up', () => {
const service = new CustomBrandingService();
expect(() => service.stop()).not.toThrowError();
});
it('stops customBranding$ and hasCustomBranding$', async () => {
const service = new CustomBrandingService();
service.setup({ injectedMetadata } as CustomBrandingSetupDeps);
const { hasCustomBranding$, customBranding$ } = service.start();
let hasCustomBrandingCompleted = false;
let customBrandingCompleted = false;
hasCustomBranding$.subscribe({
complete: () => {
hasCustomBrandingCompleted = true;
},
});
customBranding$.subscribe({
complete: () => {
customBrandingCompleted = true;
},
});
service.stop();
expect(customBrandingCompleted).toEqual(true);
expect(hasCustomBrandingCompleted).toEqual(true);
});
});
});

View file

@ -0,0 +1,58 @@
/*
* 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 { Subject, BehaviorSubject } from 'rxjs';
import { shareReplay, takeUntil, map } from 'rxjs/operators';
import type {
CustomBrandingStart,
CustomBrandingSetup,
CustomBrandingSetupDeps,
} from '@kbn/core-custom-branding-browser';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
export class CustomBrandingService {
private customBranding$: BehaviorSubject<CustomBranding> | undefined;
private stop$ = new Subject<void>();
/**
* @public
*/
public setup({ injectedMetadata }: CustomBrandingSetupDeps): CustomBrandingSetup {
const customBranding = injectedMetadata.getCustomBranding();
this.customBranding$ = new BehaviorSubject<CustomBranding>(customBranding);
return {
customBranding$: this.customBranding$.pipe(takeUntil(this.stop$), shareReplay(1)),
hasCustomBranding$: this.customBranding$.pipe(
takeUntil(this.stop$),
map((cb) => Object.keys(cb).length > 0),
shareReplay(1)
),
};
}
/**
* @public
*/
public start(): CustomBrandingStart {
if (!this.customBranding$) {
throw new Error('Setup needs to be called before start');
}
return {
customBranding$: this.customBranding$.pipe(takeUntil(this.stop$), shareReplay(1)),
hasCustomBranding$: this.customBranding$.pipe(
takeUntil(this.stop$),
map((cb) => Object.keys(cb).length > 0),
shareReplay(1)
),
};
}
public stop() {
this.stop$.next();
}
}

View file

@ -0,0 +1,21 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
]
},
"kbn_references": [
"@kbn/core-custom-branding-browser",
"@kbn/core-custom-branding-common"
],
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,4 @@
# @kbn/core-custom-branding-browser-mocks
Contains the mocks for Core's internal `customBranding` browser-side service.

View file

@ -0,0 +1,45 @@
/*
* 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 { of } from 'rxjs';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import { serviceContractMock } from './service_contract.mock';
const mockCustomBranding: CustomBranding = {
logo: 'img.jpg',
};
const createCustomBrandingMock = (): CustomBranding => {
return { ...mockCustomBranding };
};
const createSetupContractMock = () => {
return {
customBranding$: of(createCustomBrandingMock()),
hasCustomBranding$: of(false),
};
};
const createStartContractMock = () => {
return {
customBranding$: of(createCustomBrandingMock()),
hasCustomBranding$: of(false),
};
};
const createMock = () => {
const mocked = serviceContractMock();
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const customBrandingServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

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 { customBrandingServiceMock } from './custom_branding_service.mock';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/packages/core/custom-branding/core-custom-branding-browser-mocks'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-browser-mocks",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-browser-mocks",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,15 @@
/*
* 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 const serviceContractMock = (): jest.Mocked<any> => {
return {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
};

View file

@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
]
},
"kbn_references": [
"@kbn/core-custom-branding-common",
],
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/core-custom-branding-browser
Contains the public types of Core's browser-side `customBranding` service.

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 type { CustomBrandingStart, CustomBrandingSetup, CustomBrandingSetupDeps } from './types';

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-browser",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-browser",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,21 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
]
},
"kbn_references": [
"@kbn/core-injected-metadata-browser-internal",
"@kbn/core-custom-branding-common"
],
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,25 @@
/*
* 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 { Observable } from 'rxjs';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
export interface CustomBrandingStart {
customBranding$: Observable<CustomBranding>;
hasCustomBranding$: Observable<boolean>;
}
export interface CustomBrandingSetup {
customBranding$: Observable<CustomBranding>;
hasCustomBranding$: Observable<boolean>;
}
export interface CustomBrandingSetupDeps {
injectedMetadata: InternalInjectedMetadataSetup;
}

View file

@ -0,0 +1,3 @@
# @kbn/core-custom-branding-common
Contains public types to be used on both the server and the browser for Core\'s `customBranding` service.

View file

@ -0,0 +1,31 @@
/*
* 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 interface CustomBranding {
/**
* Custom replacement for the Elastic logo in the top lef *
* */
logo?: string;
/**
* Custom replacement for favicon in SVG format
*/
faviconSVG?: string;
/**
* Custom page title
*/
pageTitle?: string;
/**
* Custom replacement for Elastic Mark
* @link packages/core/chrome/core-chrome-browser-internal/src/ui/header/elastic_mark.tsx
*/
customizedLogo?: string;
/**
* Custom replacement for favicon in PNG format
*/
faviconPNG?: string;
}

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-common",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-common",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"node"
]
},
"kbn_references": [
],
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/core-custom-branding-server-internal
Contains the implementation and internal types of the server-side `customBranding` service.

View file

@ -0,0 +1,64 @@
/*
* 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 { mockCoreContext } from '@kbn/core-base-server-mocks';
import type { KibanaRequest } from '@kbn/core-http-server';
import { CustomBrandingService } from './custom_branding_service';
describe('#setup', () => {
const coreContext: ReturnType<typeof mockCoreContext.create> = mockCoreContext.create();
it('registers plugin correctly', () => {
const service = new CustomBrandingService(coreContext);
const { register } = service.setup();
const fetchFn = jest.fn();
register('pluginName', fetchFn);
expect(() => {
register('anotherPlugin', fetchFn);
}).toThrow('Another plugin already registered');
});
it('throws if `getBrandingFor` called before #start', async () => {
const service = new CustomBrandingService(coreContext);
const { register, getBrandingFor } = service.setup();
const fetchFn = jest.fn();
register('customBranding', fetchFn);
const kibanaRequest: jest.Mocked<KibanaRequest> = {} as unknown as jest.Mocked<KibanaRequest>;
try {
await getBrandingFor(kibanaRequest);
} catch (e) {
expect(e.message).toMatch('Cannot be called before #start');
}
});
it('throws if `fetchFn` not provided with register', async () => {
const service = new CustomBrandingService(coreContext);
const { register } = service.setup();
try {
// @ts-expect-error
register('customBranding');
} catch (e) {
expect(e.message).toMatch(
'Both plugin name and fetch function need to be provided when registering a plugin'
);
}
});
it('calls fetchFn correctly', async () => {
const service = new CustomBrandingService(coreContext);
const { register, getBrandingFor } = service.setup();
service.start();
const fetchFn = jest.fn();
fetchFn.mockImplementation(() => Promise.resolve({ logo: 'myLogo' }));
register('customBranding', fetchFn);
const kibanaRequest: jest.Mocked<KibanaRequest> = {} as unknown as jest.Mocked<KibanaRequest>;
const customBranding = await getBrandingFor(kibanaRequest);
expect(fetchFn).toHaveBeenCalledTimes(1);
expect(customBranding).toEqual({ logo: 'myLogo' });
});
});

View file

@ -0,0 +1,67 @@
/*
* 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 { CustomBrandingFetchFn, CustomBrandingStart } from '@kbn/core-custom-branding-server';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import type { KibanaRequest } from '@kbn/core-http-server';
import type { CoreContext } from '@kbn/core-base-server-internal';
import type { Logger } from '@kbn/logging';
/**
* @internal
*/
export interface InternalCustomBrandingSetup {
register: (pluginName: string, fetchFn: CustomBrandingFetchFn) => void;
getBrandingFor: (request: KibanaRequest) => Promise<CustomBranding>;
}
export class CustomBrandingService {
private pluginName?: string;
private logger: Logger;
private fetchFn?: CustomBrandingFetchFn;
private startCalled: boolean = false;
constructor(coreContext: CoreContext) {
this.logger = coreContext.logger.get('custom-branding-service');
}
public setup(): InternalCustomBrandingSetup {
return {
register: (pluginName, fetchFn) => {
this.logger.info('CustomBrandingService registering plugin: ' + pluginName);
if (this.pluginName) {
throw new Error('Another plugin already registered');
}
if (!pluginName || !fetchFn) {
throw new Error(
'Both plugin name and fetch function need to be provided when registering a plugin'
);
}
this.pluginName = pluginName;
this.fetchFn = fetchFn;
},
getBrandingFor: this.getBrandingFor,
};
}
public start(): CustomBrandingStart {
this.startCalled = true;
return {};
}
public stop() {}
private getBrandingFor = async (request: KibanaRequest): Promise<CustomBranding> => {
if (!this.startCalled) {
throw new Error('Cannot be called before #start');
}
if (!this.pluginName || this.pluginName !== 'customBranding' || !this.fetchFn) {
return {};
}
return this.fetchFn!(request);
};
}

View file

@ -0,0 +1,10 @@
/*
* 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 { CustomBrandingService } from './custom_branding_service';
export type { InternalCustomBrandingSetup } from './custom_branding_service';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../../',
roots: ['<rootDir>/packages/core/custom-branding/core-custom-branding-server-internal'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-server-internal",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-server-internal",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,24 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"kbn_references": [
"@kbn/core-custom-branding-common",
"@kbn/core-custom-branding-server",
"@kbn/core-base-server-mocks",
"@kbn/core-http-server",
"@kbn/core-base-server-internal",
"@kbn/logging",
],
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/core-custom-branding-server-mocks
Contains the mocks for Core's internal `customBranding` server-side service.

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 { customBrandingServiceMock } from './src/custom_branding_service.mock';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '../../../..',
roots: ['<rootDir>/packages/core/custom-branding/core-custom-branding-server-mocks'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-server-mocks",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-server-mocks",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

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 { serviceContractMock } from './service_contract.mock';
const createSetupContractMock = () => {
return {
register: jest.fn(),
getBrandingFor: jest.fn(),
};
};
const createStartContractMock = () => {
return {};
};
const createMock = () => {
const mocked = serviceContractMock();
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const customBrandingServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};

View file

@ -0,0 +1,16 @@
/*
* 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 { CustomBrandingService } from '@kbn/core-custom-branding-server-internal';
export const serviceContractMock = (): jest.Mocked<CustomBrandingService> => {
return {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
} as unknown as jest.Mocked<CustomBrandingService>;
};

View file

@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"kbn_references": [
"@kbn/core-custom-branding-server-internal",
],
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/core-custom-branding-server
Contains the public types of Core's server-side `customBranding` service.

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 type { CustomBrandingStart, CustomBrandingSetup, CustomBrandingFetchFn } from './types';

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/core-custom-branding-server",
"owner": "@elastic/kibana-global-experience",
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-custom-branding-server",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"node"
]
},
"kbn_references": [
"@kbn/core-custom-branding-common",
"@kbn/core-http-server",
"@kbn/utility-types",
],
"include": [
"**/*.ts",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,21 @@
/*
* 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 { KibanaRequest } from '@kbn/core-http-server';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
import type { MaybePromise } from '@kbn/utility-types';
/** @public */
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomBrandingStart {}
export type CustomBrandingFetchFn = (request: KibanaRequest) => MaybePromise<CustomBranding>;
/** @public */
export interface CustomBrandingSetup {
register: (fetchFn: CustomBrandingFetchFn) => void;
}

View file

@ -95,6 +95,10 @@ export class InjectedMetadataService {
getElasticsearchInfo: () => {
return this.state.clusterInfo;
},
getCustomBranding: () => {
return this.state.customBranding;
},
};
}
}

View file

@ -13,6 +13,7 @@ import {
InjectedMetadataExternalUrlPolicy,
InjectedMetadataPlugin,
} from '@kbn/core-injected-metadata-common-internal';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
/** @internal */
export interface InjectedMetadataParams {
@ -61,6 +62,7 @@ export interface InternalInjectedMetadataSetup {
getInjectedVars: () => {
[key: string]: unknown;
};
getCustomBranding: () => CustomBranding;
}
/** @internal */

View file

@ -15,9 +15,10 @@
"@kbn/std",
"@kbn/ui-shared-deps-npm",
"@kbn/core-base-common",
"@kbn/core-injected-metadata-common-internal"
"@kbn/core-injected-metadata-common-internal",
"@kbn/core-custom-branding-common",
],
"exclude": [
"target/**/*",
"target/**/*"
]
}

View file

@ -29,6 +29,7 @@ const createSetupContractMock = () => {
getInjectedVar: jest.fn(),
getInjectedVars: jest.fn(),
getKibanaBuildNumber: jest.fn(),
getCustomBranding: jest.fn(),
};
setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
setupContract.getExternalUrlConfig.mockReturnValue({ policy: [] });

View file

@ -9,6 +9,7 @@
import type { PluginName, DiscoveredPlugin } from '@kbn/core-base-common';
import type { ThemeVersion } from '@kbn/ui-shared-deps-npm';
import type { EnvironmentMode, PackageInfo } from '@kbn/config';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
/** @internal */
export interface InjectedMetadataClusterInfo {
@ -70,4 +71,5 @@ export interface InjectedMetadata {
user: Record<string, any>; // unreferencing UserProvidedValues here
};
};
customBranding: Pick<CustomBranding, 'logo' | 'customizedLogo'>;
}

View file

@ -14,7 +14,8 @@
"kbn_references": [
"@kbn/config",
"@kbn/ui-shared-deps-npm",
"@kbn/core-base-common"
"@kbn/core-base-common",
"@kbn/core-custom-branding-common"
],
"exclude": [
"target/**/*",

View file

@ -16,6 +16,7 @@ import { uiSettingsServiceMock, settingsServiceMock } from '@kbn/core-ui-setting
import { deprecationsServiceMock } from '@kbn/core-deprecations-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
import { createCoreStartMock } from './core_start.mock';
export function createCoreSetupMock({
@ -30,6 +31,7 @@ export function createCoreSetupMock({
const mock = {
analytics: analyticsServiceMock.createAnalyticsServiceSetup(),
application: applicationServiceMock.createSetupContract(),
customBranding: customBrandingServiceMock.createSetupContract(),
docLinks: docLinksServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),

View file

@ -20,12 +20,14 @@ import { savedObjectsServiceMock } from '@kbn/core-saved-objects-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
export function createCoreStartMock({ basePath = '' } = {}) {
const mock = {
analytics: analyticsServiceMock.createAnalyticsServiceStart(),
application: applicationServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
customBranding: customBrandingServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createStartContract(),
http: httpServiceMock.createStartContract({ basePath }),

View file

@ -25,7 +25,8 @@
"@kbn/core-saved-objects-browser-mocks",
"@kbn/core-notifications-browser-mocks",
"@kbn/core-application-browser-mocks",
"@kbn/core-chrome-browser-mocks"
"@kbn/core-chrome-browser-mocks",
"@kbn/core-custom-branding-browser-mocks"
],
"exclude": [
"target/**/*",

View file

@ -14,6 +14,7 @@ import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser';
import type { IUiSettingsClient, SettingsStart } from '@kbn/core-ui-settings-browser';
import type { NotificationsSetup } from '@kbn/core-notifications-browser';
import type { ApplicationSetup } from '@kbn/core-application-browser';
import type { CustomBrandingSetup } from '@kbn/core-custom-branding-browser';
import type { CoreStart } from './core_start';
/**
@ -35,6 +36,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
analytics: AnalyticsServiceSetup;
/** {@link ApplicationSetup} */
application: ApplicationSetup;
/** {@link CustomBrandingSetup} */
customBranding: CustomBrandingSetup;
/** {@link FatalErrorsSetup} */
fatalErrors: FatalErrorsSetup;
/** {@link HttpSetup} */

View file

@ -20,6 +20,7 @@ import type { SavedObjectsStart } from '@kbn/core-saved-objects-browser';
import type { NotificationsStart } from '@kbn/core-notifications-browser';
import type { ApplicationStart } from '@kbn/core-application-browser';
import type { ChromeStart } from '@kbn/core-chrome-browser';
import type { CustomBrandingStart } from '@kbn/core-custom-branding-browser';
/**
* Core services exposed to the `Plugin` start lifecycle
@ -37,6 +38,8 @@ export interface CoreStart {
application: ApplicationStart;
/** {@link ChromeStart} */
chrome: ChromeStart;
/** {@link CustomBrandingStart} */
customBranding: CustomBrandingStart;
/** {@link DocLinksStart} */
docLinks: DocLinksStart;
/** {@link ExecutionContextStart} */

View file

@ -25,7 +25,8 @@
"@kbn/core-deprecations-browser",
"@kbn/core-overlays-browser",
"@kbn/core-saved-objects-browser",
"@kbn/core-chrome-browser"
"@kbn/core-chrome-browser",
"@kbn/core-custom-branding-browser"
],
"exclude": [
"target/**/*",

View file

@ -24,6 +24,7 @@ import type { InternalSavedObjectsServiceSetup } from '@kbn/core-saved-objects-s
import type { InternalStatusServiceSetup } from '@kbn/core-status-server-internal';
import type { InternalUiSettingsServiceSetup } from '@kbn/core-ui-settings-server-internal';
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
import type { InternalCustomBrandingSetup } from '@kbn/core-custom-branding-server-internal';
/** @internal */
export interface InternalCoreSetup {
@ -45,4 +46,5 @@ export interface InternalCoreSetup {
metrics: InternalMetricsServiceSetup;
deprecations: InternalDeprecationsServiceSetup;
coreUsageData: InternalCoreUsageDataSetup;
customBranding: InternalCustomBrandingSetup;
}

View file

@ -17,6 +17,7 @@ import type { InternalMetricsServiceStart } from '@kbn/core-metrics-server-inter
import type { InternalSavedObjectsServiceStart } from '@kbn/core-saved-objects-server-internal';
import type { InternalUiSettingsServiceStart } from '@kbn/core-ui-settings-server-internal';
import type { CoreUsageDataStart } from '@kbn/core-usage-data-server';
import type { CustomBrandingStart } from '@kbn/core-custom-branding-server';
/**
* @internal
@ -33,4 +34,5 @@ export interface InternalCoreStart {
coreUsageData: CoreUsageDataStart;
executionContext: InternalExecutionContextStart;
deprecations: InternalDeprecationsServiceStart;
customBranding: CustomBrandingStart;
}

View file

@ -30,7 +30,9 @@
"@kbn/core-saved-objects-server-internal",
"@kbn/core-status-server-internal",
"@kbn/core-usage-data-base-server-internal",
"@kbn/core-usage-data-server"
"@kbn/core-usage-data-server",
"@kbn/core-custom-branding-server-internal",
"@kbn/core-custom-branding-server"
],
"exclude": [
"target/**/*",

View file

@ -24,6 +24,7 @@ import { metricsServiceMock } from '@kbn/core-metrics-server-mocks';
import { deprecationsServiceMock } from '@kbn/core-deprecations-server-mocks';
import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks';
import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
import { createCoreStartMock } from './core_start.mock';
type CoreSetupMockType = MockedKeys<CoreSetup> & {
@ -51,6 +52,7 @@ export function createCoreSetupMock({
const mock: CoreSetupMockType = {
analytics: analyticsServiceMock.createAnalyticsServiceSetup(),
capabilities: capabilitiesServiceMock.createSetupContract(),
customBranding: customBrandingServiceMock.createSetupContract(),
docLinks: docLinksServiceMock.createSetupContract(),
elasticsearch: elasticsearchServiceMock.createSetup(),
http: httpMock,

View file

@ -18,6 +18,7 @@ import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks';
import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
import type { MockedKeys } from '@kbn/utility-types-jest';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
export function createCoreStartMock() {
const mock: MockedKeys<CoreStart> = {
@ -31,6 +32,7 @@ export function createCoreStartMock() {
uiSettings: uiSettingsServiceMock.createStartContract(),
coreUsageData: coreUsageDataServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createInternalStartContract(),
customBranding: customBrandingServiceMock.createStartContract(),
};
return mock;

View file

@ -24,6 +24,7 @@ import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks';
import { statusServiceMock } from '@kbn/core-status-server-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks';
import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
export function createInternalCoreSetupMock() {
const setupDeps = {
@ -45,6 +46,7 @@ export function createInternalCoreSetupMock() {
deprecations: deprecationsServiceMock.createInternalSetupContract(),
executionContext: executionContextServiceMock.createInternalSetupContract(),
coreUsageData: coreUsageDataServiceMock.createSetupContract(),
customBranding: customBrandingServiceMock.createSetupContract(),
};
return setupDeps;
}

View file

@ -17,6 +17,7 @@ import { metricsServiceMock } from '@kbn/core-metrics-server-mocks';
import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks';
import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
export function createInternalCoreStartMock() {
const startDeps = {
@ -31,6 +32,7 @@ export function createInternalCoreStartMock() {
coreUsageData: coreUsageDataServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createInternalStartContract(),
deprecations: deprecationsServiceMock.createInternalStartContract(),
customBranding: customBrandingServiceMock.createStartContract(),
};
return startDeps;
}

View file

@ -33,6 +33,7 @@
"@kbn/core-usage-data-server-mocks",
"@kbn/core-http-request-handler-context-server",
"@kbn/core-logging-server-mocks",
"@kbn/core-custom-branding-server-mocks",
],
"exclude": [
"target/**/*",

View file

@ -22,6 +22,7 @@ import { SavedObjectsServiceSetup } from '@kbn/core-saved-objects-server';
import { StatusServiceSetup } from '@kbn/core-status-server';
import { UiSettingsServiceSetup } from '@kbn/core-ui-settings-server';
import { CoreUsageDataSetup } from '@kbn/core-usage-data-server';
import { CustomBrandingSetup } from '@kbn/core-custom-branding-server';
import { CoreStart } from './core_start';
/**
@ -38,6 +39,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
analytics: AnalyticsServiceSetup;
/** {@link CapabilitiesSetup} */
capabilities: CapabilitiesSetup;
/** {@link CustomBrandingSetup} */
customBranding: CustomBrandingSetup;
/** {@link DocLinksServiceSetup} */
docLinks: DocLinksServiceSetup;
/** {@link ElasticsearchServiceSetup} */

View file

@ -16,6 +16,7 @@ import { MetricsServiceStart } from '@kbn/core-metrics-server';
import { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server';
import { UiSettingsServiceStart } from '@kbn/core-ui-settings-server';
import { CoreUsageDataStart } from '@kbn/core-usage-data-server';
import { CustomBrandingStart } from '@kbn/core-custom-branding-server';
/**
* Context passed to the plugins `start` method.
@ -27,6 +28,8 @@ export interface CoreStart {
analytics: AnalyticsServiceStart;
/** {@link CapabilitiesStart} */
capabilities: CapabilitiesStart;
/** {@link CustomBrandingStart} */
customBranding: CustomBrandingStart;
/** {@link DocLinksServiceStart} */
docLinks: DocLinksServiceStart;
/** {@link ElasticsearchServiceStart} */

View file

@ -27,7 +27,8 @@
"@kbn/core-saved-objects-server",
"@kbn/core-status-server",
"@kbn/core-ui-settings-server",
"@kbn/core-usage-data-server"
"@kbn/core-usage-data-server",
"@kbn/core-custom-branding-server"
],
"exclude": [
"target/**/*",

View file

@ -74,6 +74,7 @@ export function createPluginSetupContext<
register: (app) => deps.application.register(plugin.opaqueId, app),
registerAppUpdater: (statusUpdater$) => deps.application.registerAppUpdater(statusUpdater$),
},
customBranding: deps.customBranding,
fatalErrors: deps.fatalErrors,
executionContext: deps.executionContext,
http: deps.http,
@ -115,6 +116,7 @@ export function createPluginStartContext<
navigateToUrl: deps.application.navigateToUrl,
getUrlForApp: deps.application.getUrlForApp,
},
customBranding: deps.customBranding,
docLinks: deps.docLinks,
executionContext: deps.executionContext,
http: deps.http,

View file

@ -194,6 +194,11 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
registerProvider: deps.capabilities.registerProvider,
registerSwitcher: deps.capabilities.registerSwitcher,
},
customBranding: {
register: (fetchFn) => {
deps.customBranding.register(plugin.name, fetchFn);
},
},
docLinks: deps.docLinks,
elasticsearch: {
legacy: deps.elasticsearch.legacy,
@ -286,6 +291,7 @@ export function createPluginStartContext<TPlugin, TPluginDependencies>(
capabilities: {
resolveCapabilities: deps.capabilities.resolveCapabilities,
},
customBranding: deps.customBranding,
docLinks: deps.docLinks,
elasticsearch: {
client: deps.elasticsearch.client,

View file

@ -10,6 +10,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -70,6 +71,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -134,6 +136,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -194,6 +197,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -254,6 +258,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -318,6 +323,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -378,6 +384,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -438,6 +445,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -502,6 +510,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -570,6 +579,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -630,6 +640,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -694,6 +705,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -762,6 +774,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,
@ -826,6 +839,7 @@ Object {
"csp": Object {
"warnLegacyBrowsers": true,
},
"customBranding": Object {},
"env": Object {
"mode": Object {
"dev": Any<Boolean>,

View file

@ -17,6 +17,7 @@ import type { CoreContext } from '@kbn/core-base-server-internal';
import type { KibanaRequest, HttpAuth } from '@kbn/core-http-server';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
import { CustomBranding } from '@kbn/core-custom-branding-common';
import { Template } from './views';
import {
IRenderOptions,
@ -32,8 +33,8 @@ import { filterUiPlugins } from './filter_ui_plugins';
import type { InternalRenderingRequestHandlerContext } from './internal_types';
type RenderOptions =
| (RenderingPrebootDeps & { status?: never; elasticsearch?: never })
| RenderingSetupDeps;
| RenderingSetupDeps
| (RenderingPrebootDeps & { status?: never; elasticsearch?: never; customBranding?: never });
/** @internal */
export class RenderingService {
@ -65,6 +66,7 @@ export class RenderingService {
http,
status,
uiPlugins,
customBranding,
}: RenderingSetupDeps): Promise<InternalRenderingServiceSetup> {
registerBootstrapRoute({
router: http.createRouter<InternalRenderingRequestHandlerContext>(''),
@ -77,12 +79,12 @@ export class RenderingService {
});
return {
render: this.render.bind(this, { elasticsearch, http, uiPlugins, status }),
render: this.render.bind(this, { elasticsearch, http, uiPlugins, status, customBranding }),
};
}
private async render(
{ elasticsearch, http, uiPlugins, status }: RenderOptions,
renderOptions: RenderOptions,
request: KibanaRequest,
uiSettings: {
client: IUiSettingsClient;
@ -90,6 +92,8 @@ export class RenderingService {
},
{ isAnonymousPage = false, vars, includeExposedConfigKeys }: IRenderOptions = {}
) {
const { elasticsearch, http, uiPlugins, status, customBranding } = renderOptions;
const env = {
mode: this.coreContext.env.mode,
packageInfo: this.coreContext.env.packageInfo,
@ -105,8 +109,8 @@ export class RenderingService {
defaults: uiSettings.globalClient?.getRegistered() ?? {},
user: isAnonymousPage ? {} : await uiSettings.globalClient?.getUserProvided(),
};
let clusterInfo = {};
let branding: CustomBranding = {};
try {
// Only provide the clusterInfo if the request is authenticated and the elasticsearch service is available.
if (isAuthenticated(http.auth, request) && elasticsearch) {
@ -116,6 +120,7 @@ export class RenderingService {
catchError(() => of({}))
)
);
branding = await customBranding?.getBrandingFor(request);
}
} catch (err) {
// swallow error
@ -142,6 +147,11 @@ export class RenderingService {
darkMode,
themeVersion,
stylesheetPaths,
customBranding: {
faviconSVG: branding?.faviconSVG,
faviconPNG: branding?.faviconPNG,
pageTitle: branding?.pageTitle,
},
injectedMetadata: {
version: env.packageInfo.version,
buildNumber: env.packageInfo.buildNum,
@ -159,6 +169,10 @@ export class RenderingService {
darkMode,
version: themeVersion,
},
customBranding: {
logo: branding?.logo,
customizedLogo: branding?.customizedLogo,
},
csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers },
externalUrl: http.externalUrl,
vars: vars ?? {},

View file

@ -10,12 +10,14 @@ import { mockCoreContext } from '@kbn/core-base-server-mocks';
import { httpServiceMock } from '@kbn/core-http-server-mocks';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { statusServiceMock } from '@kbn/core-status-server-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
const context = mockCoreContext.create();
const httpPreboot = httpServiceMock.createInternalPrebootContract();
const httpSetup = httpServiceMock.createInternalSetupContract();
const status = statusServiceMock.createInternalSetupContract();
const elasticsearch = elasticsearchServiceMock.createInternalSetup();
const customBranding = customBrandingServiceMock.createSetupContract();
function createUiPlugins() {
return {
@ -34,5 +36,6 @@ export const mockRenderingSetupDeps = {
elasticsearch,
http: httpSetup,
uiPlugins: createUiPlugins(),
customBranding,
status,
};

View file

@ -18,6 +18,8 @@ import type { InternalElasticsearchServiceSetup } from '@kbn/core-elasticsearch-
import type { InternalStatusServiceSetup } from '@kbn/core-status-server-internal';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
import type { UiPlugins } from '@kbn/core-plugins-base-server-internal';
import type { InternalCustomBrandingSetup } from '@kbn/core-custom-branding-server-internal';
import type { CustomBranding } from '@kbn/core-custom-branding-common';
/** @internal */
export interface RenderingMetadata {
@ -30,6 +32,7 @@ export interface RenderingMetadata {
themeVersion: ThemeVersion;
stylesheetPaths: string[];
injectedMetadata: InjectedMetadata;
customBranding: CustomBranding;
}
/** @internal */
@ -44,6 +47,7 @@ export interface RenderingSetupDeps {
http: InternalHttpServiceSetup;
status: InternalStatusServiceSetup;
uiPlugins: UiPlugins;
customBranding: InternalCustomBrandingSetup;
}
/** @internal */

View file

@ -28,19 +28,23 @@ export const Template: FunctionComponent<Props> = ({
i18n,
bootstrapScriptUrl,
strictCsp,
customBranding,
},
}) => {
const title = customBranding.pageTitle ?? 'Elastic';
const favIcon = customBranding.faviconSVG ?? `${uiPublicUrl}/favicons/favicon.svg`;
const favIconPng = customBranding.faviconPNG ?? `${uiPublicUrl}/favicons/favicon.png`;
return (
<html lang={locale}>
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width" />
<title>Elastic</title>
<title>{title}</title>
<Fonts url={uiPublicUrl} />
{/* The alternate icon is a fallback for Safari which does not yet support SVG favicons */}
<link rel="alternate icon" type="image/png" href={`${uiPublicUrl}/favicons/favicon.png`} />
<link rel="icon" type="image/svg+xml" href={`${uiPublicUrl}/favicons/favicon.svg`} />
<link rel="alternate icon" type="image/png" href={favIconPng} />
<link rel="icon" type="image/svg+xml" href={favIcon} />
<meta name="theme-color" content="#ffffff" />
<meta name="color-scheme" content="light dark" />
{/* Inject EUI reset and global styles before all other component styles */}

View file

@ -34,6 +34,9 @@
"@kbn/core-elasticsearch-server-mocks",
"@kbn/core-status-server-mocks",
"@kbn/utility-types",
"@kbn/core-custom-branding-server-internal",
"@kbn/core-custom-branding-common",
"@kbn/core-custom-branding-server-mocks",
],
"exclude": [
"target/**/*",

View file

@ -24,6 +24,7 @@ import { renderingServiceMock } from '@kbn/core-rendering-browser-mocks';
import { integrationsServiceMock } from '@kbn/core-integrations-browser-mocks';
import { coreAppsMock } from '@kbn/core-apps-browser-mocks';
import { loggingSystemMock } from '@kbn/core-logging-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
export const analyticsServiceStartMock = analyticsServiceMock.createAnalyticsServiceStart();
export const MockAnalyticsService = analyticsServiceMock.create();
@ -85,6 +86,13 @@ jest.doMock('@kbn/core-ui-settings-browser-internal', () => ({
SettingsService: SettingsServiceConstructor,
}));
export const MockCustomBrandingService = customBrandingServiceMock.create();
export const CustomBrandingServiceConstructor = jest
.fn()
.mockImplementation(() => MockCustomBrandingService);
jest.doMock('@kbn/core-custom-branding-browser-internal', () => ({
CustomBrandingService: CustomBrandingServiceConstructor,
}));
export const MockChromeService = chromeServiceMock.create();
export const ChromeServiceConstructor = jest.fn().mockImplementation(() => MockChromeService);
jest.doMock('@kbn/core-chrome-browser-internal', () => ({

Some files were not shown because too many files have changed in this diff Show more