[Custom Branding] Add custom branding settings to Global settings (#150080)

## Summary

This PR registers custom branding settings from the `custom_branding`
plugin. Once registered, these settings can be viewed under "Global
settings".

UI changes:
<img width="1761" alt="Screenshot 2023-02-06 at 19 59 19"
src="https://user-images.githubusercontent.com/1937956/217060900-7e56c8e9-7d3d-4ac5-96b6-8a8a85d3c1c3.png">

I also removed the client-side version of the `custom_branding` plugin,
as it's not needed.

With this change, it became easier to test custom branding, so I made a
few changes where logo was not showing properly or image size was wrong
or test subjects were missing.

I am working with @gchaps on the exact wording, so that might change. 

### 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
- [X] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [X] 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)~
- [X] 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))
- [X] 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>
Co-authored-by: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com>
This commit is contained in:
Maja Grubic 2023-02-16 08:13:42 +01:00 committed by GitHub
parent 4e5595b92b
commit a7293f62b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 358 additions and 52 deletions

View file

@ -359,3 +359,4 @@ enabled:
- x-pack/performance/journeys/ecommerce_dashboard_saved_search_only.ts
- x-pack/performance/journeys/ecommerce_dashboard_tsvb_gauge_only.ts
- x-pack/performance/journeys/dashboard_listing_page.ts
- x-pack/test/custom_branding/config.ts

View file

@ -129,6 +129,7 @@ const AppLoadingPlaceholder: FC<{ showPlainSpinner: boolean }> = ({ showPlainSpi
return (
<EuiLoadingSpinner
size={'xxl'}
className="appContainer__loading"
data-test-subj="appContainer-loadingSpinner"
aria-label={i18n.translate('core.application.appContainer.loadingAriaLabel', {
defaultMessage: 'Loading application',

View file

@ -24,6 +24,7 @@ exports[`kbnLoadingIndicator shows logo image when customLogo is set 1`] = `
<EuiImage
alt="logo"
aria-label="User logo"
data-test-subj="globalLoadingIndicator-hidden"
size={24}
src="customLogo"
/>

View file

@ -104,7 +104,12 @@ export function HeaderLogo({ href, navigateToApp, loadingCount$, ...observables
>
<LoadingIndicator loadingCount$={loadingCount$!} customLogo={logo} />
{customizedLogo ? (
<img src={customizedLogo} width="200" height="84" alt="custom mark" />
<img
src={customizedLogo}
className="chrHeaderLogo__mark"
style={{ maxWidth: '200px', maxHeight: '84px' }}
alt="custom mark"
/>
) : (
<ElasticMark className="chrHeaderLogo__mark" aria-hidden={true} />
)}

View file

@ -71,6 +71,7 @@ export class LoadingIndicator extends React.Component<LoadingIndicatorProps, { v
const logoImage = this.props.customLogo ? (
<EuiImage
src={this.props.customLogo}
data-test-subj={testSubj}
size={24}
alt="logo"
aria-label={i18n.translate('core.ui.chrome.headerGlobalNav.customLogoAriaLabel', {

View file

@ -176,6 +176,7 @@ export class RenderingService {
customBranding: {
logo: branding?.logo,
customizedLogo: branding?.customizedLogo,
pageTitle: branding?.pageTitle,
},
csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers },
externalUrl: http.externalUrl,

View file

@ -23,7 +23,6 @@ pageLoadAssetSize:
controls: 40000
core: 435325
crossClusterReplication: 65408
customBranding: 16693
customIntegrations: 22034
dashboard: 82025
dashboardEnhanced: 65646

View file

@ -36,6 +36,12 @@ export class SettingsPageObject extends FtrService {
await this.testSubjects.existOrFail('managementSettingsTitle');
}
async clickKibanaGlobalSettings() {
await this.testSubjects.click('settings');
await this.header.waitUntilLoadingHasFinished();
await this.testSubjects.click('advancedSettingsTab-global-settings');
}
async clickKibanaSavedObjects() {
await this.testSubjects.click('objects');
await this.savedObjects.waitTableIsLoaded();
@ -134,6 +140,13 @@ export class SettingsPageObject extends FtrService {
await this.header.waitUntilLoadingHasFinished();
}
async setAdvancedSettingsImage(propertyName: string, path: string) {
const input = await this.testSubjects.find(`advancedSetting-editField-${propertyName}`);
await input.type(path);
await this.testSubjects.click(`advancedSetting-saveButton`);
await this.header.waitUntilLoadingHasFinished();
}
async toggleAdvancedSettingCheckbox(propertyName: string, value?: boolean) {
let curValue: string | undefined;
if (value !== undefined) {

View file

@ -6,7 +6,7 @@
"plugin": {
"id": "customBranding",
"server": true,
"browser": true,
"browser": false,
"requiredPlugins": [
"licensing",
"licenseApiGuard"

View file

@ -1,23 +0,0 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { CustomBrandingApp } from './components/app';
export const renderApp = (
{ notifications, http }: CoreStart,
{ appBasePath, element }: AppMountParameters
) => {
ReactDOM.render(
<CustomBrandingApp basename={appBasePath} notifications={notifications} http={http} />,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -1,20 +0,0 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { CoreStart } from '@kbn/core/public';
interface CustomBrandingAppDeps {
basename: string;
notifications: CoreStart['notifications'];
http: CoreStart['http'];
}
export const CustomBrandingApp = (props: CustomBrandingAppDeps) => {
return <div>Hello</div>;
};

0
x-pack/plugins/custom_branding/public/index.ts Executable file → Normal file
View file

0
x-pack/plugins/custom_branding/public/plugin.ts Executable file → Normal file
View file

0
x-pack/plugins/custom_branding/public/types.ts Executable file → Normal file
View file

View file

@ -20,9 +20,8 @@ import { License } from '@kbn/license-api-guard-plugin/server';
import { CustomBranding } from '@kbn/core-custom-branding-common';
import { Subscription } from 'rxjs';
import { PLUGIN } from '../common/constants';
import type { CustomBrandingRequestHandlerContext } from './types';
import { Dependencies } from './types';
import { registerRoutes } from './routes';
import { registerUiSettings } from './ui_settings';
const settingsKeys: Array<keyof CustomBranding> = [
'logo',
@ -49,8 +48,8 @@ export class CustomBrandingPlugin implements Plugin {
pluginName: PLUGIN.getI18nName(i18n),
logger: this.logger,
});
const router = core.http.createRouter<CustomBrandingRequestHandlerContext>();
registerRoutes(router);
registerUiSettings(core);
const fetchFn = async (
request: KibanaRequest,
@ -95,8 +94,9 @@ export class CustomBrandingPlugin implements Plugin {
const branding: CustomBranding = {};
for (let i = 0; i < settingsKeys!.length; i++) {
const key = settingsKeys[i];
const fullKey = `customBranding:${key}`;
const fullKey = `xpackCustomBranding:${key}`;
const value = await uiSettingsClient.get(fullKey);
this.logger.info(`Fetching custom branding key ${fullKey} with value ${value}`);
if (value) {
branding[key] = value;
}

View file

@ -0,0 +1,145 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreSetup } from '@kbn/core-lifecycle-server';
import { UiSettingsParams } from '@kbn/core-ui-settings-common';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
export const UI_SETTINGS_CUSTOM_LOGO = 'xpackCustomBranding:logo';
export const UI_SETTINGS_CUSTOMIZED_LOGO = 'xpackCustomBranding:customizedLogo';
export const UI_SETTINGS_PAGE_TITLE = 'xpackCustomBranding:pageTitle';
export const UI_SETTINGS_FAVICON_PNG = 'xpackCustomBranding:faviconPNG';
export const UI_SETTINGS_FAVICON_SVG = 'xpackCustomBranding:faviconSVG';
export const PLUGIN_ID = 'Custom Branding';
const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6);
const maxLogoSizeInBase64 = kbToBase64Length(200);
const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/;
const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif'];
const isImageData = (str: string) => {
const matches = str.match(dataurlRegex);
if (!matches) {
return false;
}
const [, mimetype, , , encoding] = matches;
const imageTypeIndex = imageTypes.indexOf(mimetype);
if (imageTypeIndex < 0 || encoding !== 'base64') {
return false;
}
return true;
};
const validateLogoBase64String = (str: string) => {
if (typeof str !== 'string' || !isImageData(str)) {
return i18n.translate('xpack.customBranding.uiSettings.validate.customLogo.badFile', {
defaultMessage: `Sorry, that file will not work. Please try a different image file.`,
});
}
if (str.length > maxLogoSizeInBase64) {
return i18n.translate('xpack.customBranding.uiSettings.validate.customLogo.tooLarge', {
defaultMessage: `Sorry, that file is too large. The image file must be less than 200 kilobytes.`,
});
}
};
export const ImageSchema = schema.nullable(schema.string({ validate: validateLogoBase64String }));
const subscriptionLink = `
<a href="https://www.elastic.co/subscriptions" target="_blank" rel="noopener noreferrer">
${i18n.translate('xpack.customBranding.settings.subscriptionRequiredLink.text', {
defaultMessage: 'Subscription required.',
})}
</a>
`;
export function registerUiSettings(core: CoreSetup<object, unknown>) {
core.uiSettings.registerGlobal({
[UI_SETTINGS_CUSTOM_LOGO]: {
name: i18n.translate('xpack.customBranding.customLogoLabel', {
defaultMessage: 'Logo icon',
}),
value: null,
description: i18n.translate('xpack.customBranding.customLogoDescription', {
defaultMessage: `Replaces the Elastic logo. Logos look best when they are no larger than 128 x 128 pixels and have a transparent background. {subscriptionLink}`,
values: { subscriptionLink },
}),
sensitive: true,
type: 'image',
order: 1,
requiresPageReload: true,
schema: ImageSchema,
category: [PLUGIN_ID],
},
[UI_SETTINGS_CUSTOMIZED_LOGO]: {
name: i18n.translate('xpack.customBranding.customizedLogoLabel', {
defaultMessage: 'Organization name',
}),
value: null,
description: i18n.translate('xpack.customBranding.customizedLogoDescription', {
defaultMessage: `Replaces the Elastic text. Images look best when they are no larger than 200 x 84 pixels and have a transparent background. {subscriptionLink}`,
values: { subscriptionLink },
}),
sensitive: true,
type: 'image',
order: 2,
requiresPageReload: true,
schema: ImageSchema,
category: [PLUGIN_ID],
},
[UI_SETTINGS_PAGE_TITLE]: {
name: i18n.translate('xpack.customBranding.pageTitleLabel', {
defaultMessage: 'Page title',
}),
value: null,
description: i18n.translate('xpack.customBranding.pageTitleDescription', {
defaultMessage: `The text that appears on browser tabs. {subscriptionLink}`,
values: { subscriptionLink },
}),
sensitive: true,
type: 'string',
order: 3,
requiresPageReload: true,
schema: schema.nullable(schema.string()),
category: [PLUGIN_ID],
},
[UI_SETTINGS_FAVICON_SVG]: {
name: i18n.translate('xpack.customBranding.faviconSVGTitle', {
defaultMessage: 'Favicon (SVG)',
}),
value: null,
description: i18n.translate('xpack.customBranding.faviconSVGDescription', {
defaultMessage: `A link to an icon that will appear on browser tabs. Recommended size is 16 x 16 pixels. {subscriptionLink}`,
values: { subscriptionLink },
}),
sensitive: true,
type: 'string',
order: 4,
requiresPageReload: true,
schema: schema.nullable(schema.string()),
category: [PLUGIN_ID],
},
[UI_SETTINGS_FAVICON_PNG]: {
name: i18n.translate('xpack.customBranding.faviconPNGTitle', {
defaultMessage: 'Favicon (PNG)',
}),
value: null,
description: i18n.translate('xpack.customBranding.faviconPNGDescription', {
defaultMessage: `An icon for use in browsers that dont support svg. {subscriptionLink}`,
values: { subscriptionLink },
}),
sensitive: true,
type: 'string',
order: 5,
requiresPageReload: true,
schema: schema.nullable(schema.string()),
category: [PLUGIN_ID],
},
} as Record<string, UiSettingsParams<null>>);
}

View file

@ -16,6 +16,9 @@
"@kbn/i18n",
"@kbn/core-http-request-handler-context-server",
"@kbn/core-custom-branding-common",
"@kbn/core-lifecycle-server",
"@kbn/core-ui-settings-common",
"@kbn/config-schema",
],
"exclude": [
"target/**/*",

View file

@ -324,6 +324,7 @@ exports[`LoginPage page renders as expected 1`] = `
/>
<span
className="loginWelcome__logo"
style={Object {}}
>
<EuiIcon
size="xxl"
@ -411,6 +412,11 @@ exports[`LoginPage page renders with custom branding 1`] = `
/>
<span
className="loginWelcome__logo"
style={
Object {
"padding": 0,
}
}
>
<EuiImage
alt="logo"

View file

@ -146,12 +146,16 @@ export class LoginPage extends Component<Props, State> {
) : (
<EuiIcon type="logoElastic" size="xxl" />
);
// custom logo needs to be centered
const logoStyle = customLogo ? { padding: 0 } : {};
return (
<div className="loginWelcome login-form">
<header className="loginWelcome__header">
<div className={contentHeaderClasses}>
<EuiSpacer size="xxl" />
<span className="loginWelcome__logo">{logo}</span>
<span className="loginWelcome__logo" style={logoStyle}>
{logo}
</span>
<EuiTitle size="m" className="loginWelcome__title" data-test-subj="loginWelcomeTitle">
<h1>
<FormattedMessage

View file

@ -0,0 +1,42 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
import { services, pageObjects } from './ftr_provider_context';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const kibanaFunctionalConfig = await readConfigFile(
require.resolve('../functional/config.base.js')
);
return {
testFiles: [require.resolve('./tests')],
servers: {
...kibanaFunctionalConfig.get('servers'),
},
services,
pageObjects,
junit: {
reportName: 'X-Pack Custom Branding Functional Tests',
},
esTestCluster: {
...kibanaFunctionalConfig.get('esTestCluster'),
license: 'trial',
serverArgs: [`xpack.license.self_generated.type='trial'`],
},
apps: {
...kibanaFunctionalConfig.get('apps'),
},
kbnTestServer: {
...kibanaFunctionalConfig.get('kbnTestServer'),
serverArgs: [...kibanaFunctionalConfig.get('kbnTestServer.serverArgs')],
},
};
}

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { GenericFtrProviderContext } from '@kbn/test';
import { services } from '../functional/services';
import { pageObjects } from '../functional/page_objects';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, typeof pageObjects>;
export { services, pageObjects };

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,14 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('custom branding - functional tests', function () {
loadTestFile(require.resolve('./settings'));
});
}

View file

@ -0,0 +1,100 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const find = getService('find');
const log = getService('log');
const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']);
describe('custom branding', function describeIndexTests() {
const resetSettings = async () => {
const advancedSetting = await PageObjects.settings.getAdvancedSettings(
'xpackCustomBranding:pageTitle'
);
if (advancedSetting !== '') {
await PageObjects.settings.clearAdvancedSettings('xpackCustomBranding:pageTitle');
}
try {
await find.byCssSelector('img[alt="xpackCustomBranding:logo"]');
await PageObjects.settings.clearAdvancedSettings('xpackCustomBranding:logo');
} catch (e) {
log.debug('It is expected not to find custom branding properties set');
}
try {
await find.byCssSelector('img[alt="xpackCustomBranding:customizedLogo"]');
await PageObjects.settings.clearAdvancedSettings('xpackCustomBranding:customizedLogo');
} catch (e) {
log.debug('It is expected not to find custom branding properties set');
}
};
const goToSettings = async () => {
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaGlobalSettings();
};
before(async function () {
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
await kibanaServer.uiSettings.replace({});
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaGlobalSettings();
// clear settings before tests start
await resetSettings();
});
after(async function () {
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
await PageObjects.settings.navigateTo();
await PageObjects.settings.clickKibanaGlobalSettings();
await resetSettings();
});
beforeEach(async function () {
await goToSettings();
});
it('should allow setting custom page title through advanced settings', async function () {
const pageTitle = 'Custom Page Title';
const settingName = 'xpackCustomBranding:pageTitle';
await PageObjects.settings.setAdvancedSettingsInput(settingName, pageTitle);
const advancedSetting = await PageObjects.settings.getAdvancedSettings(settingName);
expect(advancedSetting).to.be(pageTitle);
});
it('should allow setting custom logo through advanced settings', async function () {
const settingName = 'xpackCustomBranding:logo';
await PageObjects.settings.setAdvancedSettingsImage(
settingName,
require.resolve('./acme_logo.png')
);
await goToSettings();
const img = await find.byCssSelector('img[alt="logo"]');
const imgSrc = await img.getAttribute('src');
expect(imgSrc.startsWith('data:image/png')).to.be(true);
});
it('should allow setting custom logo text through advanced settings', async function () {
const settingName = 'xpackCustomBranding:customizedLogo';
await PageObjects.settings.setAdvancedSettingsImage(
settingName,
require.resolve('./acme_text.png')
);
await goToSettings();
const logo = await testSubjects.find('logo');
const img = await logo.findByCssSelector('.chrHeaderLogo__mark');
const imgSrc = await img.getAttribute('src');
expect(imgSrc.startsWith('data:image/png')).to.be(true);
});
});
}