From 0ded3cb094be322d47c8828e5549eb7d06ca5272 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 24 Dec 2019 12:31:18 +0300 Subject: [PATCH 01/11] Licensing plugin functional tests (#53580) * NP licensing add functional tests (#53002) * fix comment * introduce core provider plugin for integration tests * platform functional tests use core_provider_plugin for testing * add 3 scenario for licensing plugins: server, client, legacy * remove unused code * run all licensing_plugin tests on CI * remove duplicated config * address comments * declare global type for core provider * remove potentially dangerous operation Co-authored-by: Elastic Machine --- test/plugin_functional/config.js | 5 +- .../plugins/core_plugin_b/public/plugin.tsx | 15 +- .../plugins/core_provider_plugin/index.ts | 36 ++++ .../plugins/core_provider_plugin/package.json | 17 ++ .../public/index.ts | 11 +- .../core_provider_plugin/tsconfig.json | 14 ++ .../types.ts} | 28 +-- .../plugins/ui_settings_plugin/kibana.json | 2 +- .../plugins/ui_settings_plugin/tsconfig.json | 3 - .../test_suites/core_plugins/ui_plugins.ts | 45 ++--- .../test_suites/core_plugins/ui_settings.ts | 20 +- x-pack/scripts/functional_tests.js | 2 + x-pack/test/licensing_plugin/apis/changes.ts | 177 ------------------ x-pack/test/licensing_plugin/config.legacy.ts | 15 ++ x-pack/test/licensing_plugin/config.public.ts | 29 +++ x-pack/test/licensing_plugin/config.ts | 4 +- x-pack/test/licensing_plugin/legacy/index.ts | 16 ++ .../test/licensing_plugin/legacy/updates.ts | 66 +++++++ x-pack/test/licensing_plugin/public/index.ts | 16 ++ .../test/licensing_plugin/public/updates.ts | 94 ++++++++++ x-pack/test/licensing_plugin/scenario.ts | 91 +++++++++ .../{apis => server}/header.ts | 1 + .../{apis => server}/index.ts | 5 +- .../licensing_plugin/{apis => server}/info.ts | 1 + .../test/licensing_plugin/server/updates.ts | 66 +++++++ 25 files changed, 544 insertions(+), 235 deletions(-) create mode 100644 test/plugin_functional/plugins/core_provider_plugin/index.ts create mode 100644 test/plugin_functional/plugins/core_provider_plugin/package.json rename test/plugin_functional/plugins/{ui_settings_plugin => core_provider_plugin}/public/index.ts (78%) create mode 100644 test/plugin_functional/plugins/core_provider_plugin/tsconfig.json rename test/plugin_functional/plugins/{ui_settings_plugin/public/plugin.tsx => core_provider_plugin/types.ts} (66%) delete mode 100644 x-pack/test/licensing_plugin/apis/changes.ts create mode 100644 x-pack/test/licensing_plugin/config.legacy.ts create mode 100644 x-pack/test/licensing_plugin/config.public.ts create mode 100644 x-pack/test/licensing_plugin/legacy/index.ts create mode 100644 x-pack/test/licensing_plugin/legacy/updates.ts create mode 100644 x-pack/test/licensing_plugin/public/index.ts create mode 100644 x-pack/test/licensing_plugin/public/updates.ts create mode 100644 x-pack/test/licensing_plugin/scenario.ts rename x-pack/test/licensing_plugin/{apis => server}/header.ts (93%) rename x-pack/test/licensing_plugin/{apis => server}/index.ts (76%) rename x-pack/test/licensing_plugin/{apis => server}/info.ts (95%) create mode 100644 x-pack/test/licensing_plugin/server/updates.ts diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 172dec9a1d111..87026ce25d9aa 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -57,11 +57,12 @@ export default async function({ readConfigFile }) { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', ...plugins.map( pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), - // Required to load new platform plugins via `--plugin-path` flag. - '--env.name=development', ], }, }; diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 5c8e1d03d5a4a..bda1557bdaf91 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -22,9 +22,6 @@ import { CorePluginAPluginSetup } from '../../core_plugin_a/public/plugin'; declare global { interface Window { - corePluginB?: string; - hasAccessToInjectedMetadata?: boolean; - receivedStartServices?: boolean; env?: PluginInitializerContext['env']; } } @@ -39,12 +36,6 @@ export class CorePluginBPlugin window.env = pluginContext.env; } public setup(core: CoreSetup, deps: CorePluginBDeps) { - window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; - window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata; - core.getStartServices().then(([coreStart, plugins]) => { - window.receivedStartServices = 'overlays' in coreStart; - }); - core.application.register({ id: 'bar', title: 'Bar', @@ -53,6 +44,12 @@ export class CorePluginBPlugin return renderApp(context, params); }, }); + + return { + sayHi() { + return `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; + }, + }; } public start() {} diff --git a/test/plugin_functional/plugins/core_provider_plugin/index.ts b/test/plugin_functional/plugins/core_provider_plugin/index.ts new file mode 100644 index 0000000000000..01f3a67c6b554 --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { Legacy } from '../../../../kibana'; + +// eslint-disable-next-line import/no-default-export +export default function CoreProviderPlugin(kibana: any) { + const config: Legacy.PluginSpecOptions = { + id: 'core-provider', + require: [], + publicDir: resolve(__dirname, 'public'), + init: (server: Legacy.Server) => ({}), + uiExports: { + hacks: [resolve(__dirname, 'public/index')], + }, + }; + + return new kibana.Plugin(config); +} diff --git a/test/plugin_functional/plugins/core_provider_plugin/package.json b/test/plugin_functional/plugins/core_provider_plugin/package.json new file mode 100644 index 0000000000000..941503b934cbb --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/package.json @@ -0,0 +1,17 @@ +{ + "name": "core_provider_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_provider_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts similarity index 78% rename from test/plugin_functional/plugins/ui_settings_plugin/public/index.ts rename to test/plugin_functional/plugins/core_provider_plugin/public/index.ts index 3c5997132d460..c74928203db56 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/public/index.ts +++ b/test/plugin_functional/plugins/core_provider_plugin/public/index.ts @@ -16,6 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { UiSettingsPlugin } from './plugin'; +import { npSetup, npStart } from 'ui/new_platform'; +import '../types'; -export const plugin = () => new UiSettingsPlugin(); +window.__coreProvider = { + setup: npSetup, + start: npStart, + testUtils: { + delay: (ms: number) => new Promise(res => setTimeout(res, ms)), + }, +}; diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json new file mode 100644 index 0000000000000..c29959197958d --- /dev/null +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "types.ts", + "public/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx b/test/plugin_functional/plugins/core_provider_plugin/types.ts similarity index 66% rename from test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx rename to test/plugin_functional/plugins/core_provider_plugin/types.ts index 883d203b4c37a..bf19578c37baa 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_provider_plugin/types.ts @@ -16,22 +16,22 @@ * specific language governing permissions and limitations * under the License. */ - -import { CoreSetup, Plugin } from 'kibana/public'; +import { LegacyCoreSetup, LegacyCoreStart } from 'kibana/public'; declare global { interface Window { - uiSettingsPlugin?: Record; - uiSettingsPluginValue?: string; + __coreProvider: { + setup: { + core: LegacyCoreSetup; + plugins: Record; + }; + start: { + core: LegacyCoreStart; + plugins: Record; + }; + testUtils: { + delay: (ms: number) => Promise; + }; + }; } } - -export class UiSettingsPlugin implements Plugin { - public setup(core: CoreSetup) { - window.uiSettingsPlugin = core.uiSettings.getAll().ui_settings_plugin; - window.uiSettingsPluginValue = core.uiSettings.get('ui_settings_plugin'); - } - - public start() {} - public stop() {} -} diff --git a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json index 05d2dca0af937..35e4c35490e2f 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/kibana.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/kibana.json @@ -4,5 +4,5 @@ "kibanaVersion": "kibana", "configPath": ["ui_settings_plugin"], "server": true, - "ui": true + "ui": false } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 1ba21f11b7de2..7c170405bbfc7 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -5,9 +5,6 @@ "skipLibCheck": true }, "include": [ - "index.ts", - "public/**/*.ts", - "public/**/*.tsx", "server/**/*.ts", "../../../../typings/**/*", ], diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index ff53583546487..b76463ee76739 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -19,6 +19,7 @@ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -31,22 +32,35 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider await PageObjects.common.navigateToApp('settings'); }); - it('should attach string to window.corePluginB', async () => { - const corePluginB = await browser.execute('return window.corePluginB'); - expect(corePluginB).to.equal(`Plugin A said: Hello from Plugin A!`); + it('should run the new platform plugins', async () => { + expect( + await browser.execute(() => { + return window.__coreProvider.setup.plugins.core_plugin_b.sayHi(); + }) + ).to.be('Plugin A said: Hello from Plugin A!'); }); }); - describe('have injectedMetadata service provided', function describeIndexTests() { + describe('should have access to the core services', function describeIndexTests() { before(async () => { - await PageObjects.common.navigateToApp('bar'); + await PageObjects.common.navigateToApp('settings'); + }); + + it('to injectedMetadata service', async () => { + expect( + await browser.execute(() => { + return window.__coreProvider.setup.core.injectedMetadata.getKibanaBuildNumber(); + }) + ).to.be.a('number'); }); - it('should attach boolean to window.hasAccessToInjectedMetadata', async () => { - const hasAccessToInjectedMetadata = await browser.execute( - 'return window.hasAccessToInjectedMetadata' - ); - expect(hasAccessToInjectedMetadata).to.equal(true); + it('to start services via coreSetup.getStartServices', async () => { + expect( + await browser.executeAsync(async cb => { + const [coreStart] = await window.__coreProvider.setup.core.getStartServices(); + cb(Boolean(coreStart.overlays)); + }) + ).to.be(true); }); }); @@ -61,16 +75,5 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(envData.packageInfo.version).to.be.a('string'); }); }); - - describe('have access to start services via coreSetup.getStartServices', function describeIndexTests() { - before(async () => { - await PageObjects.common.navigateToApp('bar'); - }); - - it('should attach boolean to window.receivedStartServices', async () => { - const receivedStartServices = await browser.execute('return window.receivedStartServices'); - expect(receivedStartServices).to.equal(true); - }); - }); }); } diff --git a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts index 2b4227ee798e3..dec79fd15f4dd 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts @@ -18,6 +18,7 @@ */ import expect from '@kbn/expect'; import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { @@ -31,15 +32,30 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider }); it('client plugins have access to registered settings', async () => { - const settings = await browser.execute('return window.uiSettingsPlugin'); + const settings = await browser.execute(() => { + return window.__coreProvider.setup.core.uiSettings.getAll().ui_settings_plugin; + }); + expect(settings).to.eql({ category: ['any'], description: 'just for testing', name: 'from_ui_settings_plugin', value: '2', }); - const settingsValue = await browser.execute('return window.uiSettingsPluginValue'); + + const settingsValue = await browser.execute(() => { + return window.__coreProvider.setup.core.uiSettings.get('ui_settings_plugin'); + }); + expect(settingsValue).to.be('2'); + + const settingsValueViaObservables = await browser.executeAsync(async (callback: Function) => { + window.__coreProvider.setup.core.uiSettings + .get$('ui_settings_plugin') + .subscribe(v => callback(v)); + }); + + expect(settingsValueViaObservables).to.be('2'); }); it('server plugins have access to registered settings', async () => { diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index f3e9db0053ad6..2b92e70fb30af 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -36,4 +36,6 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/ui_capabilities/spaces_only/config'), require.resolve('../test/upgrade_assistant_integration/config'), require.resolve('../test/licensing_plugin/config'), + require.resolve('../test/licensing_plugin/config.public'), + require.resolve('../test/licensing_plugin/config.legacy'), ]); diff --git a/x-pack/test/licensing_plugin/apis/changes.ts b/x-pack/test/licensing_plugin/apis/changes.ts deleted file mode 100644 index cf4fecfa32d94..0000000000000 --- a/x-pack/test/licensing_plugin/apis/changes.ts +++ /dev/null @@ -1,177 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../services'; -import { PublicLicenseJSON } from '../../../plugins/licensing/server'; - -const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); - -export default function({ getService, getPageObjects }: FtrProviderContext) { - const supertest = getService('supertest'); - const esSupertestWithoutAuth = getService('esSupertestWithoutAuth'); - const security = getService('security'); - const PageObjects = getPageObjects(['common', 'security']); - const testSubjects = getService('testSubjects'); - - const scenario = { - async setup() { - await security.role.create('license_manager-role', { - elasticsearch: { - cluster: ['all'], - }, - kibana: [ - { - base: ['all'], - spaces: ['*'], - }, - ], - }); - - await security.user.create('license_manager_user', { - password: 'license_manager_user-password', - roles: ['license_manager-role'], - full_name: 'license_manager user', - }); - - // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.forceLogout(); - await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); - }, - - async teardown() { - await security.role.delete('license_manager-role'); - }, - - async startBasic() { - const response = await esSupertestWithoutAuth - .post('/_license/start_basic?acknowledge=true') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.basic_was_started).to.be(true); - }, - - async startTrial() { - const response = await esSupertestWithoutAuth - .post('/_license/start_trial?acknowledge=true') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.trial_was_started).to.be(true); - }, - - async deleteLicense() { - const response = await esSupertestWithoutAuth - .delete('/_license') - .auth('license_manager_user', 'license_manager_user-password') - .expect(200); - - expect(response.body.acknowledged).to.be(true); - }, - - async getLicense(): Promise { - // > --xpack.licensing.api_polling_frequency set in test config - // to wait for Kibana server to re-fetch the license from Elasticsearch - await delay(1000); - - const { body } = await supertest.get('/api/licensing/info').expect(200); - return body; - }, - }; - - describe('changes in license types', () => { - after(async () => { - await scenario.startBasic(); - }); - - it('provides changes in license types', async () => { - await scenario.setup(); - const initialLicense = await scenario.getLicense(); - expect(initialLicense.license?.type).to.be('basic'); - // security enabled explicitly in test config - expect(initialLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { - body: legacyInitialLicense, - headers: legacyInitialLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyInitialLicense.license?.type).to.be('basic'); - expect(legacyInitialLicense.features).to.have.property('security'); - expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); - - // license hasn't changed - const refetchedLicense = await scenario.getLicense(); - expect(refetchedLicense.license?.type).to.be('basic'); - expect(refetchedLicense.signature).to.be(initialLicense.signature); - - const { - body: legacyRefetchedLicense, - headers: legacyRefetchedLicenseHeaders, - } = await supertest.get('/api/xpack/v1/info').expect(200); - - expect(legacyRefetchedLicense.license?.type).to.be('basic'); - expect(legacyRefetchedLicenseHeaders['kbn-xpack-sig']).to.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - // server allows to request trial only once. - // other attempts will throw 403 - await scenario.startTrial(); - const trialLicense = await scenario.getLicense(); - expect(trialLicense.license?.type).to.be('trial'); - expect(trialLicense.signature).to.not.be(initialLicense.signature); - - expect(trialLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - - expect(legacyTrialLicense.license?.type).to.be('trial'); - expect(legacyTrialLicense.features).to.have.property('security'); - expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.startBasic(); - const basicLicense = await scenario.getLicense(); - expect(basicLicense.license?.type).to.be('basic'); - expect(basicLicense.signature).not.to.be(initialLicense.signature); - - expect(basicLicense.features?.security).to.eql({ - isAvailable: true, - isEnabled: true, - }); - - const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest - .get('/api/xpack/v1/info') - .expect(200); - expect(legacyBasicLicense.license?.type).to.be('basic'); - expect(legacyBasicLicense.features).to.have.property('security'); - expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( - legacyInitialLicenseHeaders['kbn-xpack-sig'] - ); - - await scenario.deleteLicense(); - const inactiveLicense = await scenario.getLicense(); - expect(inactiveLicense.signature).to.not.be(initialLicense.signature); - expect(inactiveLicense).to.not.have.property('license'); - expect(inactiveLicense.features?.security).to.eql({ - isAvailable: false, - isEnabled: true, - }); - // banner shown only when license expired not just deleted - await testSubjects.missingOrFail('licenseExpiredBanner'); - }); - }); -} diff --git a/x-pack/test/licensing_plugin/config.legacy.ts b/x-pack/test/licensing_plugin/config.legacy.ts new file mode 100644 index 0000000000000..27dc3df9944ad --- /dev/null +++ b/x-pack/test/licensing_plugin/config.legacy.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const commonConfig = await readConfigFile(require.resolve('./config')); + + return { + ...commonConfig.getAll(), + testFiles: [require.resolve('./legacy')], + }; +} diff --git a/x-pack/test/licensing_plugin/config.public.ts b/x-pack/test/licensing_plugin/config.public.ts new file mode 100644 index 0000000000000..42209aa49bcb4 --- /dev/null +++ b/x-pack/test/licensing_plugin/config.public.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import path from 'path'; +import { KIBANA_ROOT } from '@kbn/test'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const commonConfig = await readConfigFile(require.resolve('./config')); + + return { + ...commonConfig.getAll(), + testFiles: [require.resolve('./public')], + kbnTestServer: { + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + + // Required to load new platform plugin provider via `--plugin-path` flag. + '--env.name=development', + `--plugin-path=${path.resolve( + KIBANA_ROOT, + 'test/plugin_functional/plugins/core_provider_plugin' + )}`, + ], + }, + }; +} diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 9a83a6f6b5a0b..60d44cbd4c47f 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -22,7 +22,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { }; return { - testFiles: [require.resolve('./apis')], + testFiles: [require.resolve('./server')], servers, services, pageObjects, @@ -43,7 +43,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...functionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...functionalTestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.licensing.api_polling_frequency=300', + '--xpack.licensing.api_polling_frequency=100', ], }, diff --git a/x-pack/test/licensing_plugin/legacy/index.ts b/x-pack/test/licensing_plugin/legacy/index.ts new file mode 100644 index 0000000000000..5c45b8f097baf --- /dev/null +++ b/x-pack/test/licensing_plugin/legacy/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: FtrProviderContext) { + describe('Legacy licensing plugin', function() { + this.tags('ciGroup2'); + // MUST BE LAST! CHANGES LICENSE TYPE! + loadTestFile(require.resolve('./updates')); + }); +} diff --git a/x-pack/test/licensing_plugin/legacy/updates.ts b/x-pack/test/licensing_plugin/legacy/updates.ts new file mode 100644 index 0000000000000..efd5df5d14511 --- /dev/null +++ b/x-pack/test/licensing_plugin/legacy/updates.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const supertest = getService('supertest'); + const testSubjects = getService('testSubjects'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { + body: legacyInitialLicense, + headers: legacyInitialLicenseHeaders, + } = await supertest.get('/api/xpack/v1/info').expect(200); + + expect(legacyInitialLicense.license?.type).to.be('basic'); + expect(legacyInitialLicense.features).to.have.property('security'); + expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + + expect(legacyTrialLicense.license?.type).to.be('trial'); + expect(legacyTrialLicense.features).to.have.property('security'); + expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + expect(legacyBasicLicense.license?.type).to.be('basic'); + expect(legacyBasicLicense.features).to.have.property('security'); + expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/public/index.ts b/x-pack/test/licensing_plugin/public/index.ts new file mode 100644 index 0000000000000..3e1445d9a4aab --- /dev/null +++ b/x-pack/test/licensing_plugin/public/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../services'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: FtrProviderContext) { + describe('Licensing plugin public client', function() { + this.tags('ciGroup2'); + // MUST BE LAST! CHANGES LICENSE TYPE! + loadTestFile(require.resolve('./updates')); + }); +} diff --git a/x-pack/test/licensing_plugin/public/updates.ts b/x-pack/test/licensing_plugin/public/updates.ts new file mode 100644 index 0000000000000..6d2253fe83868 --- /dev/null +++ b/x-pack/test/licensing_plugin/public/updates.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { LicensingPluginSetup } from '../../../plugins/licensing/public'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + // license hasn't changed + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('trial'); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + + expect( + await browser.executeAsync(async (cb: Function) => { + const { setup, testUtils } = window.__coreProvider; + // this call enforces signature check to detect license update + // and causes license re-fetch + await setup.core.http.get('/'); + await testUtils.delay(100); + + const licensing: LicensingPluginSetup = setup.plugins.licensing; + licensing.license$.subscribe(license => cb(license.type)); + }) + ).to.be('basic'); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} diff --git a/x-pack/test/licensing_plugin/scenario.ts b/x-pack/test/licensing_plugin/scenario.ts new file mode 100644 index 0000000000000..46837dfc1be91 --- /dev/null +++ b/x-pack/test/licensing_plugin/scenario.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from './services'; +import { PublicLicenseJSON } from '../../plugins/licensing/server'; +import '../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + +export function createScenario({ getService, getPageObjects }: FtrProviderContext) { + const supertest = getService('supertest'); + const esSupertestWithoutAuth = getService('esSupertestWithoutAuth'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'security']); + + const scenario = { + async setup() { + await security.role.create('license_manager-role', { + elasticsearch: { + cluster: ['all'], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create('license_manager_user', { + password: 'license_manager_user-password', + roles: ['license_manager-role'], + full_name: 'license_manager user', + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); + }, + + // make sure a license is present, otherwise the security is not available anymore. + async teardown() { + await security.role.delete('license_manager-role'); + await security.user.delete('license_manager_user'); + }, + + // elasticsearch allows to downgrade a license only once. other attempts will throw 403. + async startBasic() { + const response = await esSupertestWithoutAuth + .post('/_license/start_basic?acknowledge=true') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.basic_was_started).to.be(true); + }, + + // elasticsearch allows to request trial only once. other attempts will throw 403. + async startTrial() { + const response = await esSupertestWithoutAuth + .post('/_license/start_trial?acknowledge=true') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.trial_was_started).to.be(true); + }, + + async deleteLicense() { + const response = await esSupertestWithoutAuth + .delete('/_license') + .auth('license_manager_user', 'license_manager_user-password') + .expect(200); + + expect(response.body.acknowledged).to.be(true); + }, + + async getLicense(): Promise { + const { body } = await supertest.get('/api/licensing/info').expect(200); + return body; + }, + + async waitForPluginToDetectLicenseUpdate() { + // > --xpack.licensing.api_polling_frequency set in test config + // to wait for Kibana server to re-fetch the license from Elasticsearch + await delay(500); + }, + }; + return scenario; +} diff --git a/x-pack/test/licensing_plugin/apis/header.ts b/x-pack/test/licensing_plugin/server/header.ts similarity index 93% rename from x-pack/test/licensing_plugin/apis/header.ts rename to x-pack/test/licensing_plugin/server/header.ts index 8d95054feaaf2..d2073e8773f18 100644 --- a/x-pack/test/licensing_plugin/apis/header.ts +++ b/x-pack/test/licensing_plugin/server/header.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/licensing_plugin/apis/index.ts b/x-pack/test/licensing_plugin/server/index.ts similarity index 76% rename from x-pack/test/licensing_plugin/apis/index.ts rename to x-pack/test/licensing_plugin/server/index.ts index fbc0449dcd8fc..374bfcc0aa6b4 100644 --- a/x-pack/test/licensing_plugin/apis/index.ts +++ b/x-pack/test/licensing_plugin/server/index.ts @@ -6,13 +6,14 @@ import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ loadTestFile }: FtrProviderContext) { - describe('Licensing plugin', function() { + describe('Licensing plugin server client', function() { this.tags('ciGroup2'); loadTestFile(require.resolve('./info')); loadTestFile(require.resolve('./header')); // MUST BE LAST! CHANGES LICENSE TYPE! - loadTestFile(require.resolve('./changes')); + loadTestFile(require.resolve('./updates')); }); } diff --git a/x-pack/test/licensing_plugin/apis/info.ts b/x-pack/test/licensing_plugin/server/info.ts similarity index 95% rename from x-pack/test/licensing_plugin/apis/info.ts rename to x-pack/test/licensing_plugin/server/info.ts index 7ec009d85cd09..cce042c718b7b 100644 --- a/x-pack/test/licensing_plugin/apis/info.ts +++ b/x-pack/test/licensing_plugin/server/info.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../services'; +// eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/licensing_plugin/server/updates.ts b/x-pack/test/licensing_plugin/server/updates.ts new file mode 100644 index 0000000000000..37edb78608a68 --- /dev/null +++ b/x-pack/test/licensing_plugin/server/updates.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../services'; +import { createScenario } from '../scenario'; +import '../../../../test/plugin_functional/plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function(ftrContext: FtrProviderContext) { + const { getService } = ftrContext; + const testSubjects = getService('testSubjects'); + + const scenario = createScenario(ftrContext); + + describe('changes in license types', () => { + after(async () => { + await scenario.teardown(); + }); + + it('provides changes in license types', async () => { + await scenario.setup(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const initialLicense = await scenario.getLicense(); + expect(initialLicense.license?.type).to.be('basic'); + // security enabled explicitly in test config + expect(initialLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + // license hasn't changed + await scenario.waitForPluginToDetectLicenseUpdate(); + const refetchedLicense = await scenario.getLicense(); + expect(refetchedLicense.license?.type).to.be('basic'); + expect(refetchedLicense.signature).to.be(initialLicense.signature); + + await scenario.startTrial(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const trialLicense = await scenario.getLicense(); + expect(trialLicense.license?.type).to.be('trial'); + expect(trialLicense.signature).to.not.be(initialLicense.signature); + + expect(trialLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + await scenario.startBasic(); + await scenario.waitForPluginToDetectLicenseUpdate(); + const basicLicense = await scenario.getLicense(); + expect(basicLicense.license?.type).to.be('basic'); + expect(basicLicense.signature).not.to.be(initialLicense.signature); + + expect(basicLicense.features?.security).to.eql({ + isAvailable: true, + isEnabled: true, + }); + + // banner shown only when license expired not just deleted + await testSubjects.missingOrFail('licenseExpiredBanner'); + }); + }); +} From 3b0cce00357bf3858144d414288347c7c74f1a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 24 Dec 2019 10:33:20 +0100 Subject: [PATCH 02/11] [APM] Transaction page throws unhandled exception if transactions doesn't have `http.request` (#53760) * Making http.request optional * changing unit test --- .../DetailView/index.test.tsx | 27 +++++++++++++++++++ .../ErrorGroupDetails/DetailView/index.tsx | 2 +- .../Summary/TransactionSummary.test.tsx | 18 +++++++++++-- .../shared/Summary/TransactionSummary.tsx | 2 +- .../Summary/__fixtures__/transactions.ts | 22 +++++++++++++++ .../apm/typings/es_schemas/raw/fields/Http.ts | 2 +- 6 files changed, 68 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx index af4c129d61b60..7fe1386ba6414 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx @@ -126,4 +126,31 @@ describe('DetailView', () => { expect(wrapper.exists()).toBe(true); expect(wrapper).toMatchSnapshot(); }); + + it('should render without http request info', () => { + const errorGroup = { + occurrencesCount: 10, + transaction: undefined, + error: { + timestamp: { + us: 0 + }, + http: { response: { status_code: 404 } }, + url: { full: 'myUrl' }, + service: { name: 'myService' }, + user: { id: 'myUserId' }, + error: { exception: { handled: true } }, + transaction: { id: 'myTransactionId', sampled: true } + } as any + }; + expect(() => + shallow( + + ) + ).not.toThrowError(); + }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 879733685c5f7..29c5d3329d16b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -81,7 +81,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) { const errorUrl = error.error.page?.url || error.url?.full; - const method = error.http?.request.method; + const method = error.http?.request?.method; const status = error.http?.response?.status_code; return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx index 19f626187cdd1..80ecea3cf9b33 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx @@ -7,12 +7,26 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TransactionSummary } from './TransactionSummary'; -import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import * as exampleTransactions from './__fixtures__/transactions'; describe('TransactionSummary', () => { describe('render', () => { - const transaction: Transaction = exampleTransactions.httpOk; + const transaction = exampleTransactions.httpOk; + + const props = { + errorCount: 0, + totalDuration: 0, + transaction + }; + + it('renders', () => { + expect(() => + shallow() + ).not.toThrowError(); + }); + }); + describe('renders RUM transaction without request info', () => { + const transaction = exampleTransactions.httpRumOK; const props = { errorCount: 0, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index c24435d381008..8b7380a18edc3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -28,7 +28,7 @@ const getTransactionResultSummaryItem = (transaction: Transaction) => { : transaction.url?.full; if (url) { - const method = transaction.http?.request.method; + const method = transaction.http?.request?.method; const status = transaction.http?.response?.status_code; return ; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts index 59496dad16572..f4346e47f23c2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts @@ -25,3 +25,25 @@ export const httpOk: Transaction = { duration: { us: 0 } } }; + +export const httpRumOK: Transaction = { + '@timestamp': '0', + agent: { name: 'rum-js', version: '0' }, + http: { + response: { status_code: 200 } + }, + processor: { event: 'transaction', name: 'transaction' }, + service: { name: 'testServiceName' }, + timestamp: { us: 0 }, + trace: { id: 'testTrace' }, + transaction: { + page: { + url: 'elastic.co' + }, + name: 'testTransaction', + id: 'testId', + sampled: false, + type: 'testType', + duration: { us: 0 } + } +}; diff --git a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts index cb63499a98186..be54e66c3d01b 100644 --- a/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts +++ b/x-pack/legacy/plugins/apm/typings/es_schemas/raw/fields/Http.ts @@ -5,7 +5,7 @@ */ export interface Http { - request: { method: string; [key: string]: unknown }; + request?: { method: string; [key: string]: unknown }; response?: { status_code: number; [key: string]: unknown }; version?: string; } From 53513f6b7b85f1140352a99f65ff53f5cdb1ec79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 24 Dec 2019 12:39:08 +0100 Subject: [PATCH 03/11] [APM] Add log statements for flaky test (#53775) * [APM] Add log statements for flaky test * Improve logging * Improve logging * Log full index on error --- .../routes/settings/agent_configuration.ts | 25 ++++++++++++-- .../apis/apm/feature_controls.ts | 34 ++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts index a670911079040..ddd6a27025131 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts @@ -117,10 +117,19 @@ export const createAgentConfigurationRoute = createRoute(() => ({ }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return await createOrUpdateConfiguration({ - configuration: context.params.body, + const configuration = context.params.body; + + // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) + context.logger.info( + `Hitting: /api/apm/settings/agent-configuration/new with ${configuration.service.name}/${configuration.service.environment}` + ); + const res = await createOrUpdateConfiguration({ + configuration, setup }); + context.logger.info(`Created agent configuration`); + + return res; } })); @@ -161,8 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ }) }, handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); const { body } = context.params; + + // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) + context.logger.info( + `Hitting: /api/apm/settings/agent-configuration/search for ${body.service.name}/${body.service.environment}` + ); + + const setup = await setupRequest(context, request); const config = await searchConfigurations({ serviceName: body.service.name, environment: body.service.environment, @@ -176,6 +191,10 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ throw new Boom('Not found', { statusCode: 404 }); } + context.logger.info( + `Config was found for ${body.service.name}/${body.service.environment}` + ); + // update `applied_by_agent` field if etags match if (body.etag === config._source.etag && !config._source.applied_by_agent) { markAppliedByAgent({ id: config._id, body: config._source, setup }); diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts index 839d4ff420a76..ec2bbca23ddd2 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable no-console */ + import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -13,6 +15,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const security = getService('security'); const spaces = getService('spaces'); const log = getService('log'); + const es = getService('legacyEs'); const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); const end = encodeURIComponent(new Date().toISOString()); @@ -35,6 +38,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }; expectForbidden: (result: any) => void; expectResponse: (result: any) => void; + onExpectationFail?: () => Promise; } const endpoints: Endpoint[] = [ { @@ -139,10 +143,17 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }, expectForbidden: expect404, expectResponse: expect200, + onExpectationFail: async () => { + const res = await es.search({ + index: '.apm-agent-configuration', + }); + + console.warn(JSON.stringify(res, null, 2)); + }, }, ]; - const elasticsearchRole = { + const elasticsearchPrivileges = { indices: [ { names: ['apm-*'], privileges: ['read', 'view_index_metadata'] }, { names: ['.apm-agent-configuration'], privileges: ['read', 'write', 'view_index_metadata'] }, @@ -205,8 +216,10 @@ export default function featureControlsTests({ getService }: FtrProviderContext) spaceId?: string; }) { for (const endpoint of endpoints) { - log.debug(`hitting ${endpoint.req.url}`); + console.log(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`); const result = await executeAsUser(endpoint.req, username, password, spaceId); + console.log(`Responded: ${endpoint.req.url}`); + try { if (expectation === 'forbidden') { endpoint.expectForbidden(result); @@ -214,6 +227,10 @@ export default function featureControlsTests({ getService }: FtrProviderContext) endpoint.expectResponse(result); } } catch (e) { + if (endpoint.onExpectationFail) { + await endpoint.onExpectationFail(); + } + const { statusCode, body, req } = result.response; throw new Error( `Endpoint: ${req.method} ${req.path} @@ -229,7 +246,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) describe('apm feature controls', () => { let res: any; before(async () => { - log.debug('creating agent configuration'); + console.log(`Creating agent configuration`); res = await executeAsAdmin({ method: 'post', url: '/api/apm/settings/agent-configuration/new', @@ -238,10 +255,11 @@ export default function featureControlsTests({ getService }: FtrProviderContext) settings: { transaction_sample_rate: 0.5 }, }, }); + console.log(`Agent configuration created`); }); after(async () => { - log.debug('deleting agent configuration'); + console.log('deleting agent configuration'); const configurationId = res.body._id; await executeAsAdmin({ method: 'delete', @@ -255,7 +273,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const password = `${username}-password`; try { await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, }); await security.user.create(username, { @@ -277,7 +295,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const password = `${username}-password`; try { await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, kibana: [{ base: ['all'], spaces: ['*'] }], }); @@ -301,7 +319,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const password = `${username}-password`; try { await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, kibana: [{ feature: { dashboard: ['all'] }, spaces: ['*'] }], }); @@ -339,7 +357,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) disabledFeatures: [], }); await security.role.create(roleName, { - elasticsearch: elasticsearchRole, + elasticsearch: elasticsearchPrivileges, kibana: [ { feature: { apm: ['read'] }, spaces: [space1Id] }, { feature: { dashboard: ['all'] }, spaces: [space2Id] }, From 22b833526a9c8177ecd9db59479c6dd6d1242f28 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 25 Dec 2019 06:27:21 -0700 Subject: [PATCH 04/11] remove use of experimental fs.promises api (#53346) * remove use of experimental fs.promises api * remove one more usage of fs.promises * switch to an alternate fs module to maintain testing strategy Co-authored-by: Elastic Machine --- src/core/server/uuid/fs.ts | 24 +++++++++++++++++++++++ src/core/server/uuid/resolve_uuid.test.ts | 19 +++++------------- src/core/server/uuid/resolve_uuid.ts | 4 +--- utilities/visual_regression.js | 6 +++--- 4 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/core/server/uuid/fs.ts diff --git a/src/core/server/uuid/fs.ts b/src/core/server/uuid/fs.ts new file mode 100644 index 0000000000000..f10d6370c09d1 --- /dev/null +++ b/src/core/server/uuid/fs.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; +import { promisify } from 'util'; + +export const readFile = promisify(Fs.readFile); +export const writeFile = promisify(Fs.writeFile); diff --git a/src/core/server/uuid/resolve_uuid.test.ts b/src/core/server/uuid/resolve_uuid.test.ts index 1ddd667eacdee..d1332daa02057 100644 --- a/src/core/server/uuid/resolve_uuid.test.ts +++ b/src/core/server/uuid/resolve_uuid.test.ts @@ -17,31 +17,22 @@ * under the License. */ -import { promises } from 'fs'; import { join } from 'path'; +import { readFile, writeFile } from './fs'; import { resolveInstanceUuid } from './resolve_uuid'; import { configServiceMock } from '../config/config_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { BehaviorSubject } from 'rxjs'; import { Logger } from '../logging'; -const { readFile, writeFile } = promises; - jest.mock('uuid', () => ({ v4: () => 'NEW_UUID', })); -jest.mock('fs', () => { - const actual = jest.requireActual('fs'); - return { - ...actual, - promises: { - ...actual.promises, - readFile: jest.fn(() => Promise.resolve('')), - writeFile: jest.fn(() => Promise.resolve('')), - }, - }; -}); +jest.mock('./fs', () => ({ + readFile: jest.fn(() => Promise.resolve('')), + writeFile: jest.fn(() => Promise.resolve('')), +})); const DEFAULT_FILE_UUID = 'FILE_UUID'; const DEFAULT_CONFIG_UUID = 'CONFIG_UUID'; diff --git a/src/core/server/uuid/resolve_uuid.ts b/src/core/server/uuid/resolve_uuid.ts index 17412bfa0544c..3f5bdc7387392 100644 --- a/src/core/server/uuid/resolve_uuid.ts +++ b/src/core/server/uuid/resolve_uuid.ts @@ -18,16 +18,14 @@ */ import uuid from 'uuid'; -import { promises } from 'fs'; import { join } from 'path'; import { take } from 'rxjs/operators'; +import { readFile, writeFile } from './fs'; import { IConfigService } from '../config'; import { PathConfigType, config as pathConfigDef } from '../path'; import { HttpConfigType, config as httpConfigDef } from '../http'; import { Logger } from '../logging'; -const { readFile, writeFile } = promises; - const FILE_ENCODING = 'utf8'; const FILE_NAME = 'uuid'; diff --git a/utilities/visual_regression.js b/utilities/visual_regression.js index f174dc4764aed..d95b3018bea96 100644 --- a/utilities/visual_regression.js +++ b/utilities/visual_regression.js @@ -103,8 +103,8 @@ async function compareScreenshots() { const diffImagePath = path.resolve(DIFF_SCREENSHOTS_DIR, screenshot); - const sessionImage = PNG.sync.read(await fs.promises.readFile(sessionImagePath)); - const baselineImage = PNG.sync.read(await fs.promises.readFile(baselineImagePath)); + const sessionImage = PNG.sync.read(await readFileAsync(sessionImagePath)); + const baselineImage = PNG.sync.read(await readFileAsync(baselineImagePath)); const { width, height } = sessionImage; const diff = new PNG({ width, height }); @@ -117,7 +117,7 @@ async function compareScreenshots() { { threshold: 0 } ); - await fs.promises.writeFile(diffImagePath, PNG.sync.write(diff)); + await writeFileAsync(diffImagePath, PNG.sync.write(diff)); const change = numDiffPixels / (width * height); const changePercentage = (change * 100).toFixed(2); From 4b00bada235d4210bf5d60b843ec831351291b7b Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 26 Dec 2019 11:15:16 -0500 Subject: [PATCH 05/11] [Maps] Only show legend when layer is visible (#53781) --- .../widget_overlay/layer_control/layer_toc/toc_entry/view.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index b843d2d4758d4..c9f115c1ba4cc 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -46,7 +46,10 @@ export class TOCEntry extends React.Component { }; async _loadHasLegendDetails() { - const hasLegendDetails = await this.props.layer.hasLegendDetails(); + const hasLegendDetails = + (await this.props.layer.hasLegendDetails()) && + this.props.layer.isVisible() && + this.props.layer.showAtZoomLevel(this.props.zoom); if (this._isMounted && hasLegendDetails !== this.state.hasLegendDetails) { this.setState({ hasLegendDetails }); } From 6669111f86f2e493e6b4750cf191615412ccb205 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 26 Dec 2019 14:02:06 -0700 Subject: [PATCH 06/11] Update maps telemetry mappings to account for recent updates (#53803) --- x-pack/legacy/plugins/maps/mappings.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/maps/mappings.json b/x-pack/legacy/plugins/maps/mappings.json index 7f80512980f0a..5e2e8c2c7e6e5 100644 --- a/x-pack/legacy/plugins/maps/mappings.json +++ b/x-pack/legacy/plugins/maps/mappings.json @@ -26,6 +26,16 @@ }, "maps-telemetry": { "properties": { + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, "mapsTotalCount": { "type": "long" }, @@ -72,4 +82,4 @@ } } } -} \ No newline at end of file +} From 3ed5264cc307964ced5f90402264232724956602 Mon Sep 17 00:00:00 2001 From: friol Date: Fri, 27 Dec 2019 04:09:40 +0100 Subject: [PATCH 07/11] Possibility to filter when testing scripted fields (#35379) (#44220) * Possibility to filter when testing scripted fields * Possibility to filter when testing scripted fields * Now the i18n test should pass * use SearchBar instead of EuiFieldText input * clean up * test script design improvement * Fixed SearchBar reference and updated the help_flyout tests Co-authored-by: Elastic Machine Co-authored-by: Nathan Reese Co-authored-by: Elizabet Oliveira --- src/legacy/ui/public/field_editor/_index.scss | 1 + .../__snapshots__/help_flyout.test.js.snap | 1 - .../scripting_help/_test_script.scss | 5 ++ .../scripting_help/help_flyout.test.js | 6 +++ .../components/scripting_help/test_script.js | 54 +++++++++++++++---- .../field_editor/lib/validate_script.js | 5 ++ 6 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 src/legacy/ui/public/field_editor/components/scripting_help/_test_script.scss diff --git a/src/legacy/ui/public/field_editor/_index.scss b/src/legacy/ui/public/field_editor/_index.scss index 9c94861eb74aa..39f69c013d428 100644 --- a/src/legacy/ui/public/field_editor/_index.scss +++ b/src/legacy/ui/public/field_editor/_index.scss @@ -1 +1,2 @@ @import './components/field_format_editor/samples/index'; +@import './components/scripting_help/test_script'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap index b2ee13e865a9a..ca252b6d0147b 100644 --- a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap +++ b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap @@ -34,7 +34,6 @@ exports[`ScriptingHelpFlyout should render normally 1`] = ` executeScript={[Function]} indexPattern={Object {}} lang="painless" - name="myScriptedField" />, "data-test-subj": "testTab", "id": "test", diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/_test_script.scss b/src/legacy/ui/public/field_editor/components/scripting_help/_test_script.scss new file mode 100644 index 0000000000000..34e8a60d07074 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/_test_script.scss @@ -0,0 +1,5 @@ +.testScript__searchBar { + .globalQueryBar { + padding: $euiSize 0 0; + } +} diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js index 69e6f9430b4e1..2fac8c7641ddb 100644 --- a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js +++ b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js @@ -26,6 +26,12 @@ jest.mock('ui/documentation_links', () => ({ getDocLink: doc => `(docLink for ${doc})`, })); +jest.mock('./test_script', () => ({ + TestScript: () => { + return `
mockTestScript
`; + }, +})); + const indexPatternMock = {}; describe('ScriptingHelpFlyout', () => { diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js index 08d22af66e451..942f39fc98dec 100644 --- a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js +++ b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js @@ -31,6 +31,13 @@ import { EuiCallOut, } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; + +const { uiSettings } = npStart.core; + +import { esQuery } from '../../../../../../plugins/data/public'; + export class TestScript extends Component { state = { isLoading: false, @@ -43,7 +50,7 @@ export class TestScript extends Component { } } - previewScript = async () => { + previewScript = async searchContext => { const { indexPattern, lang, name, script, executeScript } = this.props; if (!script || script.length === 0) { @@ -54,11 +61,23 @@ export class TestScript extends Component { isLoading: true, }); + let query; + if (searchContext) { + const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); + query = esQuery.buildEsQuery( + this.props.indexPattern, + searchContext.query, + null, + esQueryConfigs + ); + } + const scriptResponse = await executeScript({ name, lang, script, indexPatternTitle: indexPattern.title, + query, additionalFields: this.state.additionalFields.map(option => { return option.value; }), @@ -161,24 +180,36 @@ export class TestScript extends Component { return ( - + - - Run script - +
+ + Run script + + } + /> +
); } @@ -191,7 +222,8 @@ export class TestScript extends Component {

Preview results

Run your script to preview the first 10 results. You can also select some additional - fields to include in your results to gain more context. + fields to include in your results to gain more context or add a query to filter on + specific documents.

diff --git a/src/legacy/ui/public/field_editor/lib/validate_script.js b/src/legacy/ui/public/field_editor/lib/validate_script.js index ee51fe8858bc0..47e2091565c30 100644 --- a/src/legacy/ui/public/field_editor/lib/validate_script.js +++ b/src/legacy/ui/public/field_editor/lib/validate_script.js @@ -24,6 +24,7 @@ export const executeScript = async ({ lang, script, indexPatternTitle, + query, additionalFields = [], }) => { // Using _msearch because _search with index name in path dorks everything up @@ -52,6 +53,10 @@ export const executeScript = async ({ search._source = additionalFields; } + if (query) { + search.query = query; + } + const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; const esResp = await kfetch({ method: 'POST', pathname: '/elasticsearch/_msearch', body }); // unwrap _msearch response From 054ec7036d01a1a2f817b0e50fa1b957f9562a5c Mon Sep 17 00:00:00 2001 From: Eli Perelman Date: Sun, 29 Dec 2019 02:10:55 -0600 Subject: [PATCH 08/11] Add server rendering service to enable standalone route rendering (#52161) * Add server rendering service to enable standalone route rendering * Update renovate config * Move legacy rendering functionality to legacy service * Use config for exposed variable in new platform * Lint changes from rebase * Rebase artifact * Remove RenderingProvider, add tests for legacy vars implementation, review notes * Add UI app functionality to legacy service * Update rendering snapshots * Update docs * Fix up functional tests * Clean up legacy types * Revise types from reverting injected metadata changes * Update translations and broken tests * Mock legacy internals in legacy tests * Add missing doc types * Rename InternalRenderOptions to LegacyRenderOptions * Remove extraneous legacy exports, review nits * Functional tests fixes * Rebase, attempt CI test fixes * Only allow specified appIds in testbed rendering integration test * Update snapshot names * Review nits --- ...a-plugin-public.savedobjectsclient.find.md | 2 +- ...kibana-plugin-public.savedobjectsclient.md | 2 +- .../kibana-plugin-server.basepath.get.md | 26 +- .../kibana-plugin-server.basepath.set.md | 26 +- ...rver.irenderoptions.includeusersettings.md | 13 + .../kibana-plugin-server.irenderoptions.md | 19 + ...lugin-server.irouter.handlelegacyerrors.md | 26 +- ...na-plugin-server.iscopedrenderingclient.md | 19 + ...in-server.iscopedrenderingclient.render.md | 41 + ...ugin-server.legacyservicesetupdeps.core.md | 4 +- ...na-plugin-server.legacyservicesetupdeps.md | 2 +- ...ugin-server.legacyservicestartdeps.core.md | 4 +- ...na-plugin-server.legacyservicestartdeps.md | 2 +- .../core/server/kibana-plugin-server.md | 4 +- ...lugin-server.requesthandlercontext.core.md | 1 + ...ana-plugin-server.requesthandlercontext.md | 4 +- ...bana-plugin-server.routeconfig.validate.md | 124 +-- ...rver.routevalidationerror._constructor_.md | 42 +- ...routevalidationresultfactory.badrequest.md | 26 +- ...-server.routevalidationresultfactory.ok.md | 26 +- ...gin-server.routevalidatoroptions.unsafe.md | 35 +- src/cli/cluster/cluster_manager.ts | 2 +- .../injected_metadata_service.ts | 3 - .../server/config/config.mock.ts} | 24 +- src/core/server/http/http_service.mock.ts | 13 +- src/core/server/index.ts | 15 +- src/core/server/internal_types.ts | 8 +- .../config/ensure_valid_configuration.ts | 2 +- .../config/get_unused_config_keys.test.ts | 5 +- .../legacy/config/get_unused_config_keys.ts | 5 +- src/core/server/legacy/config/index.ts | 6 - .../legacy_deprecation_adapters.test.ts | 4 +- .../config/legacy_deprecation_adapters.ts | 2 +- .../config/legacy_object_to_config_adapter.ts | 5 +- src/core/server/legacy/config/types.ts | 60 -- src/core/server/legacy/index.ts | 8 +- .../server/legacy/legacy_internals.test.ts | 211 +++++ src/core/server/legacy/legacy_internals.ts | 87 +++ src/core/server/legacy/legacy_service.mock.ts | 42 +- .../legacy/legacy_service.test.mocks.ts | 25 +- src/core/server/legacy/legacy_service.test.ts | 44 +- src/core/server/legacy/legacy_service.ts | 106 +-- .../logging/appenders/legacy_appender.ts | 3 +- .../legacy/logging/legacy_logging_server.ts | 7 +- .../server/legacy/merge_vars.test.ts} | 169 ++-- .../server/legacy/merge_vars.ts} | 29 +- .../plugins/find_legacy_plugin_specs.ts | 75 +- src/core/server/legacy/plugins/index.ts | 1 + src/core/server/legacy/types.ts | 222 ++++++ .../server/plugins/plugins_service.mock.ts | 40 +- src/core/server/rendering/__mocks__/params.ts | 35 + .../rendering/__mocks__/rendering_service.ts | 39 + .../rendering_service.test.ts.snap | 719 ++++++++++++++++++ src/core/server/rendering/index.ts | 21 + .../rendering/rendering_service.test.ts | 185 +++++ .../server/rendering/rendering_service.tsx | 120 +++ src/core/server/rendering/types.ts | 145 ++++ src/core/server/rendering/views/fonts.tsx | 336 ++++++++ .../server/rendering/views}/index.ts | 2 +- src/core/server/rendering/views/styles.tsx | 175 +++++ src/core/server/rendering/views/template.tsx | 144 ++++ .../migrations/core/build_index_map.test.ts | 2 +- .../migrations/core/build_index_map.ts | 2 +- .../migrations/kibana/kibana_migrator.ts | 2 +- .../saved_objects/saved_objects_service.ts | 2 +- .../server/saved_objects/schema/schema.ts | 2 +- .../saved_objects/service/lib/repository.ts | 2 +- .../lib/repository_create_repository.test.ts | 2 +- src/core/server/saved_objects/types.ts | 1 - src/core/server/server.api.md | 96 ++- src/core/server/server.test.mocks.ts | 14 +- src/core/server/server.test.ts | 4 + src/core/server/server.ts | 27 +- src/core/server/types.ts | 1 + .../ui_settings/ui_settings_service.mock.ts | 3 + src/legacy/core_plugins/telemetry/index.ts | 4 +- .../telemetry_config/replace_injected_vars.ts | 6 +- src/legacy/server/kbn_server.d.ts | 86 ++- src/legacy/server/kbn_server.js | 12 + .../ui_exports_replace_injected_vars.js | 160 ---- src/legacy/ui/ui_apps/ui_apps_mixin.js | 34 +- src/legacy/ui/ui_apps/ui_apps_mixin.test.js | 36 +- .../ui/ui_bundles/app_entry_template.js | 2 +- .../ui/ui_exports/collect_ui_exports.ts | 6 +- src/legacy/ui/ui_mixin.js | 2 - src/legacy/ui/ui_nav_links/index.js | 1 - src/legacy/ui/ui_render/ui_render_mixin.js | 172 +---- src/legacy/ui/ui_render/views/chrome.pug | 305 -------- src/legacy/ui/ui_render/views/ui_app.pug | 140 ---- src/plugins/testbed/server/index.ts | 24 + test/api_integration/apis/core/index.js | 5 + .../core_plugins/server_plugins.ts | 11 + .../plugins/monitoring/server/plugin.js | 2 +- .../translations/translations/ja-JP.json | 8 +- .../translations/translations/zh-CN.json | 8 +- 95 files changed, 3389 insertions(+), 1387 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md create mode 100644 docs/development/core/server/kibana-plugin-server.irenderoptions.md create mode 100644 docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md create mode 100644 docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md rename src/{legacy/ui/ui_nav_links/ui_nav_links_mixin.js => core/server/config/config.mock.ts} (61%) delete mode 100644 src/core/server/legacy/config/types.ts create mode 100644 src/core/server/legacy/legacy_internals.test.ts create mode 100644 src/core/server/legacy/legacy_internals.ts rename src/{legacy/ui/ui_render/lib/merge_variables.test.ts => core/server/legacy/merge_vars.test.ts} (58%) rename src/{legacy/ui/ui_render/lib/merge_variables.ts => core/server/legacy/merge_vars.ts} (65%) create mode 100644 src/core/server/legacy/types.ts create mode 100644 src/core/server/rendering/__mocks__/params.ts create mode 100644 src/core/server/rendering/__mocks__/rendering_service.ts create mode 100644 src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap create mode 100644 src/core/server/rendering/index.ts create mode 100644 src/core/server/rendering/rendering_service.test.ts create mode 100644 src/core/server/rendering/rendering_service.tsx create mode 100644 src/core/server/rendering/types.ts create mode 100644 src/core/server/rendering/views/fonts.tsx rename src/{legacy/ui/ui_render/lib => core/server/rendering/views}/index.ts (93%) create mode 100644 src/core/server/rendering/views/styles.tsx create mode 100644 src/core/server/rendering/views/template.tsx delete mode 100644 src/legacy/ui/__tests__/ui_exports_replace_injected_vars.js delete mode 100644 src/legacy/ui/ui_render/views/chrome.pug delete mode 100644 src/legacy/ui/ui_render/views/ui_app.pug diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index a4fa3f17d0d94..1ce18834f5319 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 3c4e33db4af91..6033c667c1866 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md index 3f7895dd72799..6ef7022f10e62 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.get.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md @@ -1,13 +1,13 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [get](./kibana-plugin-server.basepath.get.md) - -## BasePath.get property - -returns `basePath` value, specific for an incoming request. - -Signature: - -```typescript -(request: KibanaRequest | LegacyRequest) => string; -``` + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [get](./kibana-plugin-server.basepath.get.md) + +## BasePath.get property + +returns `basePath` value, specific for an incoming request. + +Signature: + +```typescript +get: (request: KibanaRequest | LegacyRequest) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md index 633765389e649..56a7f644d34cc 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.set.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md @@ -1,13 +1,13 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [set](./kibana-plugin-server.basepath.set.md) - -## BasePath.set property - -sets `basePath` value, specific for an incoming request. - -Signature: - -```typescript -(request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; -``` + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [BasePath](./kibana-plugin-server.basepath.md) > [set](./kibana-plugin-server.basepath.set.md) + +## BasePath.set property + +sets `basePath` value, specific for an incoming request. + +Signature: + +```typescript +set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md b/docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md new file mode 100644 index 0000000000000..cedf3d27d0887 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irenderoptions.includeusersettings.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRenderOptions](./kibana-plugin-server.irenderoptions.md) > [includeUserSettings](./kibana-plugin-server.irenderoptions.includeusersettings.md) + +## IRenderOptions.includeUserSettings property + +Set whether to output user settings in the page metadata. `true` by default. + +Signature: + +```typescript +includeUserSettings?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irenderoptions.md b/docs/development/core/server/kibana-plugin-server.irenderoptions.md new file mode 100644 index 0000000000000..34bed8b5e078c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irenderoptions.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRenderOptions](./kibana-plugin-server.irenderoptions.md) + +## IRenderOptions interface + + +Signature: + +```typescript +export interface IRenderOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [includeUserSettings](./kibana-plugin-server.irenderoptions.includeusersettings.md) | boolean | Set whether to output user settings in the page metadata. true by default. | + diff --git a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md index 238424b1df1d5..ff71f13466cf8 100644 --- a/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md +++ b/docs/development/core/server/kibana-plugin-server.irouter.handlelegacyerrors.md @@ -1,13 +1,13 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) - -## IRouter.handleLegacyErrors property - -Wrap a router handler to catch and converts legacy boom errors to proper custom errors. - -Signature: - -```typescript -(handler: RequestHandler) => RequestHandler; -``` + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [handleLegacyErrors](./kibana-plugin-server.irouter.handlelegacyerrors.md) + +## IRouter.handleLegacyErrors property + +Wrap a router handler to catch and converts legacy boom errors to proper custom errors. + +Signature: + +```typescript +handleLegacyErrors: (handler: RequestHandler) => RequestHandler; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md new file mode 100644 index 0000000000000..2e6daa58db25f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) + +## IScopedRenderingClient interface + + +Signature: + +```typescript +export interface IScopedRenderingClient +``` + +## Methods + +| Method | Description | +| --- | --- | +| [render(options)](./kibana-plugin-server.iscopedrenderingclient.render.md) | Generate a KibanaResponse which renders an HTML page bootstrapped with the core bundle. Intended as a response body for HTTP route handlers. | + diff --git a/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md new file mode 100644 index 0000000000000..1bc78dd84571d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iscopedrenderingclient.render.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) > [render](./kibana-plugin-server.iscopedrenderingclient.render.md) + +## IScopedRenderingClient.render() method + +Generate a `KibanaResponse` which renders an HTML page bootstrapped with the `core` bundle. Intended as a response body for HTTP route handlers. + +Signature: + +```typescript +render(options?: IRenderOptions): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | IRenderOptions | | + +Returns: + +`Promise` + +## Example + + +```ts +router.get( + { path: '/', validate: false }, + (context, request, response) => + response.ok({ + body: await context.core.rendering.render(), + headers: { + 'content-security-policy': context.core.http.csp.header, + }, + }) +); + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md index 09ebf1170715b..c4c043a903d06 100644 --- a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md +++ b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.core.md @@ -7,7 +7,5 @@ Signature: ```typescript -core: InternalCoreSetup & { - plugins: PluginsServiceSetup; - }; +core: LegacyCoreSetup; ``` diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md index 4475318522dfa..7961cedd2c054 100644 --- a/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md +++ b/docs/development/core/server/kibana-plugin-server.legacyservicesetupdeps.md @@ -18,6 +18,6 @@ export interface LegacyServiceSetupDeps | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-server.legacyservicesetupdeps.core.md) | InternalCoreSetup & {
plugins: PluginsServiceSetup;
} | | +| [core](./kibana-plugin-server.legacyservicesetupdeps.core.md) | LegacyCoreSetup | | | [plugins](./kibana-plugin-server.legacyservicesetupdeps.plugins.md) | Record<string, unknown> | | diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md index c5cf473aaa01a..47018f4594967 100644 --- a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md +++ b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.core.md @@ -7,7 +7,5 @@ Signature: ```typescript -core: InternalCoreStart & { - plugins: PluginsServiceStart; - }; +core: LegacyCoreStart; ``` diff --git a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md index 801138b64e46a..602fe5356d525 100644 --- a/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md +++ b/docs/development/core/server/kibana-plugin-server.legacyservicestartdeps.md @@ -18,6 +18,6 @@ export interface LegacyServiceStartDeps | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-server.legacyservicestartdeps.core.md) | InternalCoreStart & {
plugins: PluginsServiceStart;
} | | +| [core](./kibana-plugin-server.legacyservicestartdeps.core.md) | LegacyCoreStart | | | [plugins](./kibana-plugin-server.legacyservicestartdeps.plugins.md) | Record<string, unknown> | | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9c8aafb158bfd..5e7f84c55244d 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -70,7 +70,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IKibanaResponse](./kibana-plugin-server.ikibanaresponse.md) | A response data object, expected to returned as a result of [RequestHandler](./kibana-plugin-server.requesthandler.md) execution | | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [IndexSettingsDeprecationInfo](./kibana-plugin-server.indexsettingsdeprecationinfo.md) | | +| [IRenderOptions](./kibana-plugin-server.irenderoptions.md) | | | [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-server.routeconfig.md) and [RequestHandler](./kibana-plugin-server.requesthandler.md) for more information about arguments to route registrations. | +| [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) | | | [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | @@ -91,7 +93,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | -| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md index 2d8b27ecb6c67..d1760dafd5bb6 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md @@ -8,6 +8,7 @@ ```typescript core: { + rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; }; diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md index d9b781e1e550e..7c8625a5824ee 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request +Provides the following clients: - [rendering](./kibana-plugin-server.iscopedrenderingclient.md) - Rendering client which uses the data of the incoming request - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: @@ -18,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | +| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
rendering: IScopedRenderingClient;
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
uiSettings: {
client: IUiSettingsClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md index 4fbcf0981f114..23a72fc3c68b3 100644 --- a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md +++ b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md @@ -1,62 +1,62 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [validate](./kibana-plugin-server.routeconfig.validate.md) - -## RouteConfig.validate property - -A schema created with `@kbn/config-schema` that every request will be validated against. - -Signature: - -```typescript -RouteValidatorFullConfig | false; -``` - -## Remarks - -You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`; - -## Example - - -```ts - import { schema } from '@kbn/config-schema'; - router.get({ - path: 'path/{id}', - validate: { - params: schema.object({ - id: schema.string(), - }), - query: schema.object({...}), - body: schema.object({...}), - }, -}, -(context, req, res,) { - req.params; // type Readonly<{id: string}> - console.log(req.params.id); // value -}); - -router.get({ - path: 'path/{id}', - validate: false, // handler has no access to params, query, body values. -}, -(context, req, res,) { - req.params; // type Readonly<{}>; - console.log(req.params.id); // undefined -}); - -router.get({ - path: 'path/{id}', - validate: { - // handler has access to raw non-validated params in runtime - params: schema.object({}, { allowUnknowns: true }) - }, -}, -(context, req, res,) { - req.params; // type Readonly<{}>; - console.log(req.params.id); // value - myValidationLibrary.validate({ params: req.params }); -}); - -``` - + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteConfig](./kibana-plugin-server.routeconfig.md) > [validate](./kibana-plugin-server.routeconfig.validate.md) + +## RouteConfig.validate property + +A schema created with `@kbn/config-schema` that every request will be validated against. + +Signature: + +```typescript +validate: RouteValidatorFullConfig | false; +``` + +## Remarks + +You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`; + +## Example + + +```ts + import { schema } from '@kbn/config-schema'; + router.get({ + path: 'path/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + query: schema.object({...}), + body: schema.object({...}), + }, +}, +(context, req, res,) { + req.params; // type Readonly<{id: string}> + console.log(req.params.id); // value +}); + +router.get({ + path: 'path/{id}', + validate: false, // handler has no access to params, query, body values. +}, +(context, req, res,) { + req.params; // type Readonly<{}>; + console.log(req.params.id); // undefined +}); + +router.get({ + path: 'path/{id}', + validate: { + // handler has access to raw non-validated params in runtime + params: schema.object({}, { allowUnknowns: true }) + }, +}, +(context, req, res,) { + req.params; // type Readonly<{}>; + console.log(req.params.id); // value + myValidationLibrary.validate({ params: req.params }); +}); + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md b/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md index 31dc6ceb91995..551e13faaf154 100644 --- a/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md +++ b/docs/development/core/server/kibana-plugin-server.routevalidationerror._constructor_.md @@ -1,21 +1,21 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) > [(constructor)](./kibana-plugin-server.routevalidationerror._constructor_.md) - -## RouteValidationError.(constructor) - -Constructs a new instance of the `RouteValidationError` class - -Signature: - -```typescript -constructor(error;: Error | string, path?: string[];) -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| error | Error | string | | -| path | string[] | | - + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationError](./kibana-plugin-server.routevalidationerror.md) > [(constructor)](./kibana-plugin-server.routevalidationerror._constructor_.md) + +## RouteValidationError.(constructor) + +Constructs a new instance of the `RouteValidationError` class + +Signature: + +```typescript +constructor(error: Error | string, path?: string[]); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| error | Error | string | | +| path | string[] | | + diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md index 2462ae17943be..36ea6103fb352 100644 --- a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md +++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.badrequest.md @@ -1,13 +1,13 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md) - -## RouteValidationResultFactory.badRequest property - -Signature: - -```typescript -(error: Error | string, path?: string[]) => { - RouteValidationError; - }; -``` + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [badRequest](./kibana-plugin-server.routevalidationresultfactory.badrequest.md) + +## RouteValidationResultFactory.badRequest property + +Signature: + +```typescript +badRequest: (error: Error | string, path?: string[]) => { + error: RouteValidationError; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md index c86ef616de103..eca6a31bd547f 100644 --- a/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md +++ b/docs/development/core/server/kibana-plugin-server.routevalidationresultfactory.ok.md @@ -1,13 +1,13 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md) - -## RouteValidationResultFactory.ok property - -Signature: - -```typescript -(value: T) => { - T; - }; -``` + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidationResultFactory](./kibana-plugin-server.routevalidationresultfactory.md) > [ok](./kibana-plugin-server.routevalidationresultfactory.ok.md) + +## RouteValidationResultFactory.ok property + +Signature: + +```typescript +ok: (value: T) => { + value: T; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md index b1c75e6dbdf67..0406a372c4e9d 100644 --- a/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md +++ b/docs/development/core/server/kibana-plugin-server.routevalidatoroptions.unsafe.md @@ -1,18 +1,17 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) > [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md) - -## RouteValidatorOptions.unsafe property - -Set the `unsafe` config to avoid running some additional internal \*safe\* validations on top of your custom validation - -Signature: - -```typescript -unsafe?: { - params?: boolean; - query?: boolean; - body?: boolean; - } - -``` + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [RouteValidatorOptions](./kibana-plugin-server.routevalidatoroptions.md) > [unsafe](./kibana-plugin-server.routevalidatoroptions.unsafe.md) + +## RouteValidatorOptions.unsafe property + +Set the `unsafe` config to avoid running some additional internal \*safe\* validations on top of your custom validation + +Signature: + +```typescript +unsafe?: { + params?: boolean; + query?: boolean; + body?: boolean; + }; +``` diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index d97f7485fb4d2..3fa4bdcbc5fa5 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -26,7 +26,7 @@ import { first, mapTo, filter, map, take } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; import { FSWatcher } from 'chokidar'; -import { LegacyConfig } from '../../core/server/legacy/config'; +import { LegacyConfig } from '../../core/server/legacy'; import { BasePathProxyServer } from '../../core/server/http'; // @ts-ignore diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 6a44000bf617e..0bde1b68e1876 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -80,9 +80,6 @@ export interface InjectedMetadataParams { user?: Record; }; }; - apm: { - [key: string]: unknown; - }; }; } diff --git a/src/legacy/ui/ui_nav_links/ui_nav_links_mixin.js b/src/core/server/config/config.mock.ts similarity index 61% rename from src/legacy/ui/ui_nav_links/ui_nav_links_mixin.js rename to src/core/server/config/config.mock.ts index e445f5e9126d4..e098fa142b9d1 100644 --- a/src/legacy/ui/ui_nav_links/ui_nav_links_mixin.js +++ b/src/core/server/config/config.mock.ts @@ -17,18 +17,18 @@ * under the License. */ -import { UiNavLink } from './ui_nav_link'; +import { Config } from './config'; -export function uiNavLinksMixin(kbnServer, server) { - const uiApps = server.getAllUiApps(); +type ConfigMock = jest.Mocked; - const { navLinkSpecs = [] } = kbnServer.uiExports; +const createConfigMock = (): ConfigMock => ({ + has: jest.fn(), + get: jest.fn(), + set: jest.fn(), + getFlattenedPaths: jest.fn(), + toRaw: jest.fn(), +}); - const fromSpecs = navLinkSpecs.map(navLinkSpec => new UiNavLink(navLinkSpec)); - - const fromApps = uiApps.map(app => app.getNavLink()).filter(Boolean); - - const uiNavLinks = fromSpecs.concat(fromApps).sort((a, b) => a.getOrder() - b.getOrder()); - - server.decorate('server', 'getUiNavLinks', () => uiNavLinks.slice(0)); -} +export const configMock = { + create: createConfigMock, +}; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 1668b409050b7..700ae04f00d47 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -20,6 +20,7 @@ import { Server } from 'hapi'; import { CspConfig } from '../csp'; import { mockRouter } from './router/router.mock'; +import { configMock } from '../config/config.mock'; import { InternalHttpServiceSetup } from './types'; import { HttpService } from './http_service'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; @@ -28,13 +29,14 @@ import { sessionStorageMock } from './cookie_session_storage.mocks'; import { OnPostAuthToolkit } from './lifecycle/on_post_auth'; import { OnPreResponseToolkit } from './lifecycle/on_pre_response'; +type BasePathMocked = jest.Mocked; export type HttpServiceSetupMock = jest.Mocked & { - basePath: jest.Mocked; + basePath: BasePathMocked; }; -const createBasePathMock = (): jest.Mocked => ({ - serverBasePath: '/mock-server-basepath', - get: jest.fn(), +const createBasePathMock = (serverBasePath = '/mock-server-basepath'): BasePathMocked => ({ + serverBasePath, + get: jest.fn().mockReturnValue(serverBasePath), set: jest.fn(), prepend: jest.fn(), remove: jest.fn(), @@ -44,9 +46,12 @@ const createSetupContractMock = () => { const setupContract: HttpServiceSetupMock = { // we can mock other hapi server methods when we need it server: ({ + name: 'http-server-test', + version: 'kibana', route: jest.fn(), start: jest.fn(), stop: jest.fn(), + config: jest.fn().mockReturnValue(configMock.create()), } as unknown) as jest.MockedClass, createCookieSessionStorageFactory: jest.fn(), registerOnPreAuth: jest.fn(), diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 878f854f2a517..953fa0738597c 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -41,6 +41,7 @@ import { ElasticsearchServiceSetup, IScopedClusterClient } from './elasticsearch'; import { HttpServiceSetup } from './http'; +import { IScopedRenderingClient } from './rendering'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings'; @@ -149,6 +150,7 @@ export { SessionCookieValidationResult, SessionStorageFactory, } from './http'; +export { RenderingServiceSetup, IRenderOptions, LegacyRenderOptions } from './rendering'; export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging'; export { @@ -229,12 +231,21 @@ export { SavedObjectsMigrationVersion, } from './types'; -export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy'; +export { + LegacyServiceSetupDeps, + LegacyServiceStartDeps, + LegacyServiceDiscoverPlugins, + LegacyConfig, + LegacyUiExports, + LegacyInternals, +} from './legacy'; /** * Plugin specific context passed to a route handler. * * Provides the following clients: + * - {@link IScopedRenderingClient | rendering} - Rendering client + * which uses the data of the incoming request * - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client * which uses the credentials of the incoming request * - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch @@ -248,6 +259,7 @@ export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy'; */ export interface RequestHandlerContext { core: { + rendering: IScopedRenderingClient; savedObjects: { client: SavedObjectsClientContract; }; @@ -301,6 +313,7 @@ export { CapabilitiesSetup, CapabilitiesStart, ContextSetup, + IScopedRenderingClient, PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId, diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 52adaaccab4b7..be4d830c55eab 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -17,15 +17,15 @@ * under the License. */ +import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; +import { ContextSetup } from './context'; import { InternalElasticsearchServiceSetup } from './elasticsearch'; import { InternalHttpServiceSetup } from './http'; -import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; -import { ContextSetup } from './context'; import { - InternalSavedObjectsServiceStart, InternalSavedObjectsServiceSetup, + InternalSavedObjectsServiceStart, } from './saved_objects'; -import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; +import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { UuidServiceSetup } from './uuid'; /** @internal */ diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts index 026683a7b7cb0..a68d3df577a89 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.ts @@ -19,7 +19,7 @@ import { getUnusedConfigKeys } from './get_unused_config_keys'; import { ConfigService } from '../../config'; -import { LegacyServiceDiscoverPlugins } from '../legacy_service'; +import { LegacyServiceDiscoverPlugins } from '../types'; import { CriticalError } from '../../errors'; export async function ensureValidConfiguration( diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index bf011fa01a342..c4452fc6a1209 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -17,8 +17,7 @@ * under the License. */ -import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs'; -import { LegacyConfig } from './types'; +import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; import { getUnusedConfigKeys } from './get_unused_config_keys'; describe('getUnusedConfigKeys', () => { @@ -26,7 +25,7 @@ describe('getUnusedConfigKeys', () => { jest.resetAllMocks(); }); - const getConfig = (values: Record = {}): LegacyConfig => + const getConfig = (values: LegacyVars = {}): LegacyConfig => ({ get: () => values as any, } as LegacyConfig); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 73cc7d8c50474..e425082ba126d 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -22,8 +22,7 @@ import { difference, get, set } from 'lodash'; import { getTransform } from '../../../../legacy/deprecation/index'; import { unset, getFlattenedObject } from '../../../../legacy/utils'; import { hasConfigPathIntersection } from '../../config'; -import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs'; -import { LegacyConfig } from './types'; +import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object)); @@ -37,7 +36,7 @@ export async function getUnusedConfigKeys({ coreHandledConfigPaths: string[]; pluginSpecs: LegacyPluginSpec[]; disabledPluginSpecs: LegacyPluginSpec[]; - settings: Record; + settings: LegacyVars; legacyConfig: LegacyConfig; }) { // transform deprecated plugin settings diff --git a/src/core/server/legacy/config/index.ts b/src/core/server/legacy/config/index.ts index c3f308fd6d903..f10e3f22d53c5 100644 --- a/src/core/server/legacy/config/index.ts +++ b/src/core/server/legacy/config/index.ts @@ -20,9 +20,3 @@ export { ensureValidConfiguration } from './ensure_valid_configuration'; export { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter'; export { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; -export { - LegacyConfig, - LegacyConfigDeprecation, - LegacyConfigDeprecationFactory, - LegacyConfigDeprecationProvider, -} from './types'; diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts index 144e057c118f7..8651d05064492 100644 --- a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts @@ -17,11 +17,11 @@ * under the License. */ -import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; -import { LegacyConfigDeprecationProvider } from './types'; import { ConfigDeprecation } from '../../config'; import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory'; import { applyDeprecations } from '../../config/deprecation/apply_deprecations'; +import { LegacyConfigDeprecationProvider } from '../types'; +import { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; jest.spyOn(configDeprecationFactory, 'unusedFromRoot'); jest.spyOn(configDeprecationFactory, 'renameFromRoot'); diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.ts index b0e3bc37e1510..1e0733969e662 100644 --- a/src/core/server/legacy/config/legacy_deprecation_adapters.ts +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.ts @@ -18,8 +18,8 @@ */ import { ConfigDeprecation, ConfigDeprecationProvider } from '../../config/deprecation'; -import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from './index'; import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory'; +import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from '../types'; const convertLegacyDeprecation = ( legacyDeprecation: LegacyConfigDeprecation diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index ffcbfda4e024d..bdcde8262ef98 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -19,6 +19,7 @@ import { ConfigPath } from '../../config'; import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter'; +import { LegacyVars } from '../types'; /** * Represents logging config supported by the legacy platform. @@ -77,7 +78,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { }; } - private static transformPlugins(configValue: Record) { + private static transformPlugins(configValue: LegacyVars) { // These properties are the only ones we use from the existing `plugins` config node // since `scanDirs` isn't respected by new platform plugin discovery. return { @@ -94,7 +95,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { case 'server': return LegacyObjectToConfigAdapter.transformServer(configValue); case 'plugins': - return LegacyObjectToConfigAdapter.transformPlugins(configValue as Record); + return LegacyObjectToConfigAdapter.transformPlugins(configValue as LegacyVars); default: return configValue; } diff --git a/src/core/server/legacy/config/types.ts b/src/core/server/legacy/config/types.ts deleted file mode 100644 index cac1002d6c244..0000000000000 --- a/src/core/server/legacy/config/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * New platform representation of the legacy configuration (KibanaConfig) - * - * @internal - */ -export interface LegacyConfig { - get(key?: string): T; - has(key: string): boolean; - set(key: string, value: any): void; - set(config: Record): void; -} - -/** - * Representation of a legacy configuration deprecation factory used for - * legacy plugin deprecations. - * - * @internal - */ -export interface LegacyConfigDeprecationFactory { - rename(oldKey: string, newKey: string): LegacyConfigDeprecation; - unused(unusedKey: string): LegacyConfigDeprecation; -} - -/** - * Representation of a legacy configuration deprecation. - * - * @internal - */ -export type LegacyConfigDeprecation = ( - settings: Record, - log: (msg: string) => void -) => void; - -/** - * Representation of a legacy configuration deprecation provider. - * - * @internal - */ -export type LegacyConfigDeprecationProvider = ( - factory: LegacyConfigDeprecationFactory -) => LegacyConfigDeprecation[] | Promise; diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts index 10686fc521d35..208e9b1167253 100644 --- a/src/core/server/legacy/index.ts +++ b/src/core/server/legacy/index.ts @@ -18,6 +18,10 @@ */ /** @internal */ -export { LegacyObjectToConfigAdapter, ensureValidConfiguration, LegacyConfig } from './config'; +export { LegacyObjectToConfigAdapter, ensureValidConfiguration } from './config'; /** @internal */ -export { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy_service'; +export { LegacyInternals } from './legacy_internals'; +/** @internal */ +export { LegacyService, ILegacyService } from './legacy_service'; +/** @internal */ +export * from './types'; diff --git a/src/core/server/legacy/legacy_internals.test.ts b/src/core/server/legacy/legacy_internals.test.ts new file mode 100644 index 0000000000000..dcab62627442b --- /dev/null +++ b/src/core/server/legacy/legacy_internals.test.ts @@ -0,0 +1,211 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Server } from 'hapi'; + +import { configMock } from '../config/config.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { httpServerMock } from '../http/http_server.mocks'; +import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; +import { LegacyInternals } from './legacy_internals'; +import { ILegacyInternals, LegacyConfig, LegacyVars, LegacyUiExports } from './types'; + +function varsProvider(vars: LegacyVars, configValue?: any) { + return { + fn: jest.fn().mockReturnValue(vars), + pluginSpec: { + readConfigValue: jest.fn().mockReturnValue(configValue), + }, + }; +} + +describe('LegacyInternals', () => { + describe('getInjectedUiAppVars()', () => { + let uiExports: LegacyUiExports; + let config: LegacyConfig; + let server: Server; + let legacyInternals: ILegacyInternals; + + beforeEach(async () => { + uiExports = findLegacyPluginSpecsMock().uiExports; + config = configMock.create() as any; + server = httpServiceMock.createSetupContract().server; + legacyInternals = new LegacyInternals(uiExports, config, server); + }); + + it('gets with no injectors', async () => { + await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot( + `Object {}` + ); + }); + + it('gets with no matching injectors', async () => { + const injector = jest.fn().mockResolvedValue({ not: 'core' }); + legacyInternals.injectUiAppVars('not-core', injector); + + await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot( + `Object {}` + ); + expect(injector).not.toHaveBeenCalled(); + }); + + it('gets with single matching injector', async () => { + const injector = jest.fn().mockResolvedValue({ is: 'core' }); + legacyInternals.injectUiAppVars('core', injector); + + await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(` + Object { + "is": "core", + } + `); + expect(injector).toHaveBeenCalled(); + }); + + it('gets with multiple matching injectors', async () => { + const injectors = [ + jest.fn().mockResolvedValue({ is: 'core' }), + jest.fn().mockReturnValue({ sync: 'injector' }), + jest.fn().mockResolvedValue({ is: 'merged-core' }), + ]; + + injectors.forEach(injector => legacyInternals.injectUiAppVars('core', injector)); + + await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(` + Object { + "is": "merged-core", + "sync": "injector", + } + `); + expect(injectors[0]).toHaveBeenCalled(); + expect(injectors[1]).toHaveBeenCalled(); + expect(injectors[2]).toHaveBeenCalled(); + }); + }); + + describe('getVars()', () => { + let uiExports: LegacyUiExports; + let config: LegacyConfig; + let server: Server; + let legacyInternals: LegacyInternals; + + beforeEach(async () => { + uiExports = findLegacyPluginSpecsMock().uiExports; + config = configMock.create() as any; + server = httpServiceMock.createSetupContract().server; + legacyInternals = new LegacyInternals(uiExports, config, server); + }); + + it('gets: no default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => { + const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); + + expect(vars).toMatchInlineSnapshot(`Object {}`); + }); + + it('gets: with default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => { + uiExports.defaultInjectedVarProviders = [ + varsProvider({ alpha: 'alpha' }), + varsProvider({ gamma: 'gamma' }), + varsProvider({ alpha: 'beta' }), + ]; + + const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); + + expect(vars).toMatchInlineSnapshot(` + Object { + "alpha": "beta", + "gamma": "gamma", + } + `); + }); + + it('gets: no default injectors, with injected vars replacers, with ui app injectors, no inject arg', async () => { + uiExports.injectedVarsReplacers = [ + jest.fn(async vars => ({ ...vars, added: 'key' })), + jest.fn(vars => vars), + jest.fn(vars => ({ replaced: 'all' })), + jest.fn(async vars => ({ ...vars, added: 'last-key' })), + ]; + + const request = httpServerMock.createRawRequest(); + const vars = await legacyInternals.getVars('core', request); + + expect(vars).toMatchInlineSnapshot(` + Object { + "added": "last-key", + "replaced": "all", + } + `); + }); + + it('gets: no default injectors, no injected vars replacers, with ui app injectors, no inject arg', async () => { + legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' })); + legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' })); + legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' })); + + const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); + + expect(vars).toMatchInlineSnapshot(` + Object { + "is": "merged-core", + "sync": "injector", + } + `); + }); + + it('gets: no default injectors, no injected vars replacers, no ui app injectors, with inject arg', async () => { + const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), { + injected: 'arg', + }); + + expect(vars).toMatchInlineSnapshot(` + Object { + "injected": "arg", + } + `); + }); + + it('gets: with default injectors, with injected vars replacers, with ui app injectors, with inject arg', async () => { + uiExports.defaultInjectedVarProviders = [ + varsProvider({ alpha: 'alpha' }), + varsProvider({ gamma: 'gamma' }), + varsProvider({ alpha: 'beta' }), + ]; + uiExports.injectedVarsReplacers = [jest.fn(async vars => ({ ...vars, gamma: 'delta' }))]; + + legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' })); + legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' })); + legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' })); + + const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), { + injected: 'arg', + sync: 'arg', + }); + + expect(vars).toMatchInlineSnapshot(` + Object { + "alpha": "beta", + "gamma": "delta", + "injected": "arg", + "is": "merged-core", + "sync": "arg", + } + `); + }); + }); +}); diff --git a/src/core/server/legacy/legacy_internals.ts b/src/core/server/legacy/legacy_internals.ts new file mode 100644 index 0000000000000..3bf54e5f75dce --- /dev/null +++ b/src/core/server/legacy/legacy_internals.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Server } from 'hapi'; + +import { LegacyRequest } from '../http'; +import { mergeVars } from './merge_vars'; +import { ILegacyInternals, LegacyVars, VarsInjector, LegacyConfig, LegacyUiExports } from './types'; + +/** + * @internal + * @deprecated + */ +export class LegacyInternals implements ILegacyInternals { + private readonly injectors = new Map>(); + private cachedDefaultVars?: LegacyVars; + + constructor( + private readonly uiExports: LegacyUiExports, + private readonly config: LegacyConfig, + private readonly server: Server + ) {} + + private get defaultVars(): LegacyVars { + if (this.cachedDefaultVars) { + return this.cachedDefaultVars; + } + + const { defaultInjectedVarProviders = [] } = this.uiExports; + + return (this.cachedDefaultVars = defaultInjectedVarProviders.reduce( + (vars, { fn, pluginSpec }) => + mergeVars(vars, fn(this.server, pluginSpec.readConfigValue(this.config, []))), + {} + )); + } + + private replaceVars(vars: LegacyVars, request: LegacyRequest) { + const { injectedVarsReplacers = [] } = this.uiExports; + + return injectedVarsReplacers.reduce( + async (injected, replacer) => replacer(await injected, request, this.server), + Promise.resolve(vars) + ); + } + + public injectUiAppVars(id: string, injector: VarsInjector) { + if (!this.injectors.has(id)) { + this.injectors.set(id, new Set()); + } + + this.injectors.get(id)!.add(injector); + } + + public getInjectedUiAppVars(id: string) { + return [...(this.injectors.get(id) || [])].reduce( + async (promise, injector) => ({ + ...(await promise), + ...(await injector()), + }), + Promise.resolve({}) + ); + } + + public async getVars(id: string, request: LegacyRequest, injected: LegacyVars = {}) { + return this.replaceVars( + mergeVars(this.defaultVars, await this.getInjectedUiAppVars(id), injected), + request + ); + } +} diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index ac0319cdf4eb5..495141cdcb58d 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -17,23 +17,33 @@ * under the License. */ -import { LegacyServiceDiscoverPlugins } from './legacy_service'; +import { LegacyService } from './legacy_service'; +import { LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types'; -const createDiscoverMock = () => { - const setupContract: DeeplyMockedKeys = { - pluginSpecs: [], - disabledPluginSpecs: [], - uiExports: {} as any, - settings: {}, - pluginExtendedConfig: { - get: jest.fn(), - has: jest.fn(), - set: jest.fn(), - } as any, - }; - return setupContract; -}; +type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; + +const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({ + pluginSpecs: [], + uiExports: {} as any, + navLinks: [], + pluginExtendedConfig: { + get: jest.fn(), + has: jest.fn(), + set: jest.fn(), + }, + disabledPluginSpecs: [], + settings: {}, +}); +const createLegacyServiceMock = (): LegacyServiceMock => ({ + legacyId: Symbol(), + discoverPlugins: jest.fn().mockResolvedValue(createDiscoverPluginsMock()), + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), +}); export const legacyServiceMock = { - createDiscover: createDiscoverMock, + create: createLegacyServiceMock, + createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), + createDiscoverPlugins: createDiscoverPluginsMock, }; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts index e8d4a0ed0bd4d..451a75ced7ae2 100644 --- a/src/core/server/legacy/legacy_service.test.mocks.ts +++ b/src/core/server/legacy/legacy_service.test.mocks.ts @@ -17,18 +17,19 @@ * under the License. */ -export const findLegacyPluginSpecsMock = jest - .fn() - .mockImplementation((settings: Record) => ({ - pluginSpecs: [], - pluginExtendedConfig: { - has: jest.fn(), - get: jest.fn(() => settings), - set: jest.fn(), - }, - disabledPluginSpecs: [], - uiExports: [], - })); +import { LegacyVars } from './types'; + +export const findLegacyPluginSpecsMock = jest.fn().mockImplementation((settings: LegacyVars) => ({ + pluginSpecs: [], + pluginExtendedConfig: { + has: jest.fn(), + get: jest.fn().mockReturnValue(settings), + set: jest.fn(), + }, + disabledPluginSpecs: [], + uiExports: {}, + navLinks: [], +})); jest.doMock('./plugins/find_legacy_plugin_specs.ts', () => ({ findLegacyPluginSpecs: findLegacyPluginSpecsMock, })); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index c652bb1c94887..608392e4943f9 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -25,7 +25,7 @@ jest.mock('./config/legacy_deprecation_adapters', () => ({ import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; -import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; + // @ts-ignore: implicit any for JS file import { ClusterManager as MockClusterManager } from '../../../cli/cluster/cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; @@ -33,7 +33,6 @@ import { Config, Env, ObjectToConfigAdapter } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; import { BasePathProxyServer } from '../http'; import { DiscoveredPlugin } from '../plugins'; -import { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs'; import { configServiceMock } from '../config/config_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; @@ -42,7 +41,11 @@ import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; +import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { uuidServiceMock } from '../uuid/uuid_service.mock'; +import { findLegacyPluginSpecs } from './plugins'; +import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; +import { LegacyService } from './legacy_service'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -89,6 +92,7 @@ beforeEach(() => { browserConfigs: new Map(), }, }, + rendering: renderingServiceMock, uuid: uuidSetup, }, plugins: { 'plugin-id': 'plugin-value' }, @@ -138,7 +142,7 @@ describe('once LegacyService is set up with connection info', () => { { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value expect.objectContaining({ get: expect.any(Function) }), expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } ); expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ path: { autoListen: true }, @@ -168,7 +172,7 @@ describe('once LegacyService is set up with connection info', () => { { path: { autoListen: false }, server: { autoListen: true } }, expect.objectContaining({ get: expect.any(Function) }), expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } ); expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ path: { autoListen: false }, @@ -309,7 +313,7 @@ describe('once LegacyService is set up without connection info', () => { { path: {}, server: { autoListen: true } }, expect.objectContaining({ get: expect.any(Function) }), expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } + { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } ); expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ path: {}, @@ -395,16 +399,18 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { }); }); -test('Cannot start without setup phase', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, +describe('start', () => { + test('Cannot start without setup phase', async () => { + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Legacy service is not setup yet."` + ); }); - await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service is not setup yet."` - ); }); describe('#discoverPlugins()', () => { @@ -438,7 +444,8 @@ describe('#discoverPlugins()', () => { ], pluginExtendedConfig: settings, disabledPluginSpecs: [], - uiExports: [], + uiExports: {}, + navLinks: [], }) as any ); @@ -469,15 +476,16 @@ test('Sets the server.uuid property on the legacy configuration', async () => { const configSetMock = jest.fn(); - findLegacyPluginSpecsMock.mockImplementation((settings: Record) => ({ + findLegacyPluginSpecsMock.mockImplementation((settings: LegacyVars) => ({ pluginSpecs: [], pluginExtendedConfig: { has: jest.fn(), - get: jest.fn(() => settings), + get: jest.fn().mockReturnValue(settings), set: configSetMock, }, disabledPluginSpecs: [], - uiExports: [], + uiExports: {}, + navLinks: [], })); await legacyService.discoverPlugins(); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 2e8a467eff995..2ed87f4c6d488 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -19,24 +19,30 @@ import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, tap } from 'rxjs/operators'; + import { CoreService } from '../../types'; -import { CoreSetup, CoreStart } from '../'; -import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; -import { SavedObjectsLegacyUiExports } from '../types'; import { Config, ConfigDeprecationProvider } from '../config'; import { CoreContext } from '../core_context'; import { CspConfigType, config as cspConfig } from '../csp'; import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; -import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; -import { findLegacyPluginSpecs } from './plugins'; -import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs'; import { PathConfigType } from '../path'; -import { LegacyConfig, convertLegacyDeprecationProvider } from './config'; +import { findLegacyPluginSpecs } from './plugins'; +import { convertLegacyDeprecationProvider } from './config'; +import { + LegacyServiceSetupDeps, + LegacyServiceStartDeps, + LegacyPlugins, + LegacyServiceDiscoverPlugins, + LegacyConfig, + LegacyVars, +} from './types'; +import { LegacyInternals } from './legacy_internals'; +import { CoreSetup, CoreStart } from '..'; interface LegacyKbnServer { - applyLoggingConfiguration: (settings: Readonly>) => void; + applyLoggingConfiguration: (settings: Readonly) => void; listen: () => Promise; ready: () => Promise; close: () => Promise; @@ -53,43 +59,14 @@ function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) { return { ...rawConfig, - path: pathConfig, // We rely heavily in the default value of 'path.data' in the legacy world and, since it has been moved to NP, it won't show up in RawConfig - }; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceSetupDeps { - core: InternalCoreSetup & { - plugins: PluginsServiceSetup; - }; - plugins: Record; -} - -/** - * @public - * @deprecated - */ -export interface LegacyServiceStartDeps { - core: InternalCoreStart & { - plugins: PluginsServiceStart; + // We rely heavily in the default value of 'path.data' in the legacy world and, + // since it has been moved to NP, it won't show up in RawConfig. + path: pathConfig, }; - plugins: Record; } /** @internal */ -export interface LegacyServiceDiscoverPlugins { - pluginSpecs: LegacyPluginSpec[]; - disabledPluginSpecs: LegacyPluginSpec[]; - uiExports: SavedObjectsLegacyUiExports; - pluginExtendedConfig: LegacyConfig; - settings: Record; -} - -/** @internal */ -export type ILegacyService = Pick; +export type ILegacyService = PublicMethodsOf; /** @internal */ export class LegacyService implements CoreService { @@ -101,16 +78,10 @@ export class LegacyService implements CoreService { private kbnServer?: LegacyKbnServer; private configSubscription?: Subscription; private setupDeps?: LegacyServiceSetupDeps; - private update$: ConnectableObservable<[Config, PathConfigType]> | undefined; - private legacyRawConfig: LegacyConfig | undefined; - private legacyPlugins: - | { - pluginSpecs: LegacyPluginSpec[]; - disabledPluginSpecs: LegacyPluginSpec[]; - uiExports: SavedObjectsLegacyUiExports; - } - | undefined; - private settings: Record | undefined; + private update$?: ConnectableObservable<[Config, PathConfigType]>; + private legacyRawConfig?: LegacyConfig; + private legacyPlugins?: LegacyPlugins; + private settings?: LegacyVars; constructor(private readonly coreContext: CoreContext) { const { logger, configService, env } = coreContext; @@ -153,12 +124,14 @@ export class LegacyService implements CoreService { pluginExtendedConfig, disabledPluginSpecs, uiExports, + navLinks, } = await findLegacyPluginSpecs(this.settings, this.coreContext.logger); this.legacyPlugins = { pluginSpecs, disabledPluginSpecs, uiExports, + navLinks, }; const deprecationProviders = await pluginSpecs @@ -188,6 +161,7 @@ export class LegacyService implements CoreService { pluginSpecs, disabledPluginSpecs, uiExports, + navLinks, pluginExtendedConfig, settings: this.settings, }; @@ -195,35 +169,37 @@ export class LegacyService implements CoreService { public async setup(setupDeps: LegacyServiceSetupDeps) { this.log.debug('setting up legacy service'); - if (!this.legacyRawConfig || !this.legacyPlugins || !this.settings) { + + if (!this.legacyPlugins) { throw new Error( 'Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()' ); } - // propagate the instance uuid to the legacy config, as it was the legacy way to access it. - this.legacyRawConfig.set('server.uuid', setupDeps.core.uuid.getInstanceUuid()); + // propagate the instance uuid to the legacy config, as it was the legacy way to access it. + this.legacyRawConfig!.set('server.uuid', setupDeps.core.uuid.getInstanceUuid()); this.setupDeps = setupDeps; } public async start(startDeps: LegacyServiceStartDeps) { const { setupDeps } = this; - if (!setupDeps || !this.legacyRawConfig || !this.legacyPlugins || !this.settings) { + + if (!setupDeps || !this.legacyPlugins) { throw new Error('Legacy service is not setup yet.'); } + this.log.debug('starting legacy service'); // Receive initial config and create kbnServer/ClusterManager. - if (this.coreContext.env.isDevClusterMaster) { - await this.createClusterManager(this.legacyRawConfig); + await this.createClusterManager(this.legacyRawConfig!); } else { this.kbnServer = await this.createKbnServer( - this.settings, - this.legacyRawConfig, + this.settings!, + this.legacyRawConfig!, setupDeps, startDeps, - this.legacyPlugins + this.legacyPlugins! ); } } @@ -263,15 +239,11 @@ export class LegacyService implements CoreService { } private async createKbnServer( - settings: Record, + settings: LegacyVars, config: LegacyConfig, setupDeps: LegacyServiceSetupDeps, startDeps: LegacyServiceStartDeps, - legacyPlugins: { - pluginSpecs: LegacyPluginSpec[]; - disabledPluginSpecs: LegacyPluginSpec[]; - uiExports: SavedObjectsLegacyUiExports; - } + legacyPlugins: LegacyPlugins ) { const coreSetup: CoreSetup = { capabilities: setupDeps.core.capabilities, @@ -338,8 +310,10 @@ export class LegacyService implements CoreService { kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, + rendering: setupDeps.core.rendering, uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, + legacy: new LegacyInternals(legacyPlugins.uiExports, config, setupDeps.core.http.server), }, logger: this.coreContext.logger, }, diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index 011dfae8a5cef..6d82d929e7daa 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -21,6 +21,7 @@ import { schema } from '@kbn/config-schema'; import { DisposableAppender } from '../../../logging/appenders/appenders'; import { LogRecord } from '../../../logging/log_record'; import { LegacyLoggingServer } from '../legacy_logging_server'; +import { LegacyVars } from '../../types'; /** * Simple appender that just forwards `LogRecord` to the legacy KbnServer log. @@ -34,7 +35,7 @@ export class LegacyAppender implements DisposableAppender { private readonly loggingServer: LegacyLoggingServer; - constructor(legacyLoggingConfig: Readonly>) { + constructor(legacyLoggingConfig: Readonly) { this.loggingServer = new LegacyLoggingServer(legacyLoggingConfig); } diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/src/core/server/legacy/logging/legacy_logging_server.ts index 57706bcac2232..85a8686b4eded 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.ts +++ b/src/core/server/legacy/logging/legacy_logging_server.ts @@ -25,9 +25,10 @@ import { Config } from '../../../../legacy/server/config'; import { setupLogging } from '../../../../legacy/server/logging'; import { LogLevel } from '../../logging/log_level'; import { LogRecord } from '../../logging/log_record'; +import { LegacyVars } from '../../types'; export const metadataSymbol = Symbol('log message with metadata'); -export function attachMetaData(message: string, metadata: Record = {}) { +export function attachMetaData(message: string, metadata: LegacyVars = {}) { return { [metadataSymbol]: { message, @@ -50,7 +51,7 @@ interface PluginRegisterParams { options: PluginRegisterParams['options'] ) => Promise; }; - options: Record; + options: LegacyVars; } /** @@ -84,7 +85,7 @@ export class LegacyLoggingServer { private onPostStopCallback?: () => void; - constructor(legacyLoggingConfig: Readonly>) { + constructor(legacyLoggingConfig: Readonly) { // We set `ops.interval` to max allowed number and `ops` filter to value // that doesn't exist to avoid logging of ops at all, if turned on it will be // logged by the "legacy" Kibana. diff --git a/src/legacy/ui/ui_render/lib/merge_variables.test.ts b/src/core/server/legacy/merge_vars.test.ts similarity index 58% rename from src/legacy/ui/ui_render/lib/merge_variables.test.ts rename to src/core/server/legacy/merge_vars.test.ts index 4d69216bc0bfd..d977ee292d039 100644 --- a/src/legacy/ui/ui_render/lib/merge_variables.test.ts +++ b/src/core/server/legacy/merge_vars.test.ts @@ -17,29 +17,26 @@ * under the License. */ -import { mergeVariables } from './merge_variables'; +import { mergeVars } from './merge_vars'; -describe('mergeVariables', () => { +describe('mergeVars', () => { it('merges two objects together', () => { - const someVariables = { - name: 'value', - canFoo: true, - nested: { - anotherVariable: 'ok', - }, - }; - - const otherVariables = { + const first = { otherName: 'value', otherCanFoo: true, otherNested: { otherAnotherVariable: 'ok', }, }; + const second = { + name: 'value', + canFoo: true, + nested: { + anotherVariable: 'ok', + }, + }; - const result = mergeVariables(someVariables, otherVariables); - - expect(result).toEqual({ + expect(mergeVars(first, second)).toEqual({ name: 'value', canFoo: true, nested: { @@ -54,86 +51,76 @@ describe('mergeVariables', () => { }); it('does not mutate the source objects', () => { - const original = { - var1: 'original', + const first = { + var1: 'first', }; - - const set1 = { - var1: 'value1', - var2: 'value1', + const second = { + var1: 'second', + var2: 'second', }; - - const set2 = { - var1: 'value2', - var2: 'value2', - var3: 'value2', + const third = { + var1: 'third', + var2: 'third', + var3: 'third', }; - - const set3 = { - var1: 'value3', - var2: 'value3', - var3: 'value3', - var4: 'value3', + const fourth = { + var1: 'fourth', + var2: 'fourth', + var3: 'fourth', + var4: 'fourth', }; - mergeVariables(original, set1, set2, set3); + mergeVars(first, second, third, fourth); - expect(original).toEqual({ var1: 'original' }); - expect(set1).toEqual({ var1: 'value1', var2: 'value1' }); - expect(set2).toEqual({ var1: 'value2', var2: 'value2', var3: 'value2' }); - expect(set3).toEqual({ var1: 'value3', var2: 'value3', var3: 'value3', var4: 'value3' }); + expect(first).toEqual({ var1: 'first' }); + expect(second).toEqual({ var1: 'second', var2: 'second' }); + expect(third).toEqual({ var1: 'third', var2: 'third', var3: 'third' }); + expect(fourth).toEqual({ var1: 'fourth', var2: 'fourth', var3: 'fourth', var4: 'fourth' }); }); - it('merges multiple objects together, preferring the leftmost values', () => { - const original = { - var1: 'original', + it('merges multiple objects together with precedence increasing from left-to-right', () => { + const first = { + var1: 'first', + var2: 'first', + var3: 'first', + var4: 'first', }; - - const set1 = { - var1: 'value1', - var2: 'value1', + const second = { + var1: 'second', + var2: 'second', + var3: 'second', }; - - const set2 = { - var1: 'value2', - var2: 'value2', - var3: 'value2', + const third = { + var1: 'third', + var2: 'third', }; - - const set3 = { - var1: 'value3', - var2: 'value3', - var3: 'value3', - var4: 'value3', + const fourth = { + var1: 'fourth', }; - const result = mergeVariables(original, set1, set2, set3); - - expect(result).toEqual({ - var1: 'original', - var2: 'value1', - var3: 'value2', - var4: 'value3', + expect(mergeVars(first, second, third, fourth)).toEqual({ + var1: 'fourth', + var2: 'third', + var3: 'second', + var4: 'first', }); }); - it('retains the original variable value if a duplicate entry is found', () => { - const someVariables = { - name: 'value', - canFoo: true, + it('overwrites the original variable value if a duplicate entry is found', () => { + const first = { nested: { - anotherVariable: 'ok', + otherAnotherVariable: 'ok', }, }; - - const otherVariables = { + const second = { + name: 'value', + canFoo: true, nested: { - otherAnotherVariable: 'ok', + anotherVariable: 'ok', }, }; - const result = mergeVariables(someVariables, otherVariables); - expect(result).toEqual({ + expect(mergeVars(first, second)).toEqual({ name: 'value', canFoo: true, nested: { @@ -143,55 +130,61 @@ describe('mergeVariables', () => { }); it('combines entries within "uiCapabilities"', () => { - const someVariables = { - name: 'value', - canFoo: true, + const first = { uiCapabilities: { firstCapability: 'ok', + sharedCapability: 'shared', }, }; - - const otherVariables = { + const second = { + name: 'value', + canFoo: true, uiCapabilities: { secondCapability: 'ok', }, }; + const third = { + name: 'value', + canFoo: true, + uiCapabilities: { + thirdCapability: 'ok', + sharedCapability: 'blocked', + }, + }; - const result = mergeVariables(someVariables, otherVariables); - - expect(result).toEqual({ + expect(mergeVars(first, second, third)).toEqual({ name: 'value', canFoo: true, uiCapabilities: { firstCapability: 'ok', secondCapability: 'ok', + thirdCapability: 'ok', + sharedCapability: 'blocked', }, }); }); it('does not deeply combine entries within "uiCapabilities"', () => { - const someVariables = { - name: 'value', - canFoo: true, + const first = { uiCapabilities: { firstCapability: 'ok', nestedCapability: { - nestedProp: 'nestedValue', + otherNestedProp: 'otherNestedValue', }, }, }; - - const otherVariables = { + const second = { + name: 'value', + canFoo: true, uiCapabilities: { secondCapability: 'ok', nestedCapability: { - otherNestedProp: 'otherNestedValue', + nestedProp: 'nestedValue', }, }, }; - const result = mergeVariables(someVariables, otherVariables); - expect(result).toEqual({ + expect(mergeVars(first, second)).toEqual({ name: 'value', canFoo: true, uiCapabilities: { diff --git a/src/legacy/ui/ui_render/lib/merge_variables.ts b/src/core/server/legacy/merge_vars.ts similarity index 65% rename from src/legacy/ui/ui_render/lib/merge_variables.ts rename to src/core/server/legacy/merge_vars.ts index 0f65c7825bdba..a1d43af2f861d 100644 --- a/src/legacy/ui/ui_render/lib/merge_variables.ts +++ b/src/core/server/legacy/merge_vars.ts @@ -17,23 +17,18 @@ * under the License. */ -const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities']; - -export function mergeVariables(...sources: Array>) { - const result: Record = {}; +import { LegacyVars } from './types'; - for (const source of sources) { - Object.entries(source).forEach(([key, value]) => { - if (ELIGIBLE_FLAT_MERGE_KEYS.includes(key)) { - result[key] = { - ...value, - ...result[key], - }; - } else if (!result.hasOwnProperty(key)) { - result[key] = value; - } - }); - } +const ELIGIBLE_FLAT_MERGE_KEYS = ['uiCapabilities']; - return result; +export function mergeVars(...sources: LegacyVars[]): LegacyVars { + return Object.assign( + {}, + ...sources, + ...ELIGIBLE_FLAT_MERGE_KEYS.flatMap(key => + sources.some(source => key in source) + ? [{ [key]: Object.assign({}, ...sources.map(source => source[key] || {})) }] + : [] + ) + ); } diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 0a49154801e56..d2e7a39236d0a 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -19,25 +19,77 @@ import { Observable, merge, forkJoin } from 'rxjs'; import { toArray, tap, distinct, map } from 'rxjs/operators'; + import { findPluginSpecs, defaultConfig, // @ts-ignore } from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; -import { LoggerFactory } from '../../logging'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; -import { LegacyConfig, LegacyConfigDeprecationProvider } from '../config'; -export interface LegacyPluginPack { - getPath(): string; +import { LoggerFactory } from '../../logging'; +import { + LegacyUiExports, + LegacyNavLink, + LegacyPluginSpec, + LegacyPluginPack, + LegacyConfig, +} from '../types'; + +const REMOVE_FROM_ARRAY: LegacyNavLink[] = []; + +function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { + return uiAppSpecs.flatMap(spec => { + if (!spec) { + return REMOVE_FROM_ARRAY; + } + + const id = spec.pluginId || spec.id; + + if (!id) { + throw new Error('Every app must specify an id'); + } + + if (spec.pluginId && !pluginSpecs.some(plugin => plugin.getId() === spec.pluginId)) { + throw new Error(`Unknown plugin id "${spec.pluginId}"`); + } + + const listed = typeof spec.listed === 'boolean' ? spec.listed : true; + + if (spec.hidden || !listed) { + return REMOVE_FROM_ARRAY; + } + + return { + id, + title: spec.title, + order: typeof spec.order === 'number' ? spec.order : 0, + icon: spec.icon, + euiIconType: spec.euiIconType, + url: spec.url || `/app/${id}`, + linkToLastSubUrl: spec.linkToLastSubUrl, + }; + }); } -export interface LegacyPluginSpec { - getId: () => unknown; - getExpectedKibanaVersion: () => string; - getConfigPrefix: () => string; - getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined; +function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { + return (uiExports.navLinkSpecs || []) + .map(spec => ({ + id: spec.id, + title: spec.title, + order: typeof spec.order === 'number' ? spec.order : 0, + url: spec.url, + subUrlBase: spec.subUrlBase || spec.url, + icon: spec.icon, + euiIconType: spec.euiIconType, + linkToLastSub: 'linkToLastSubUrl' in spec ? spec.linkToLastSubUrl : false, + hidden: 'hidden' in spec ? spec.hidden : false, + disabled: 'disabled' in spec ? spec.disabled : false, + tooltip: spec.tooltip || '', + })) + .concat(getUiAppsNavLinks(uiExports, pluginSpecs)) + .sort((a, b) => a.order - b.order); } export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) { @@ -128,11 +180,14 @@ export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: Lo spec$.pipe(toArray()), log$.pipe(toArray()) ).toPromise(); + const uiExports = collectLegacyUiExports(pluginSpecs); + const navLinks = getNavLinks(uiExports, pluginSpecs); return { disabledPluginSpecs, pluginSpecs, pluginExtendedConfig: configToMutate, - uiExports: collectLegacyUiExports(pluginSpecs), + uiExports, + navLinks, }; } diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts index 7c69546f0c4de..a6d55e1da7839 100644 --- a/src/core/server/legacy/plugins/index.ts +++ b/src/core/server/legacy/plugins/index.ts @@ -16,4 +16,5 @@ * specific language governing permissions and limitations * under the License. */ + export { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts new file mode 100644 index 0000000000000..6ec893be9b310 --- /dev/null +++ b/src/core/server/legacy/types.ts @@ -0,0 +1,222 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Server } from 'hapi'; + +import { ChromeNavLink } from '../../public'; +import { LegacyRequest } from '../http'; +import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; +import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; +import { RenderingServiceSetup } from '../rendering'; +import { SavedObjectsLegacyUiExports } from '../types'; + +/** + * @internal + * @deprecated + */ +export type LegacyVars = Record; + +type LegacyCoreSetup = InternalCoreSetup & { + plugins: PluginsServiceSetup; + rendering: RenderingServiceSetup; +}; +type LegacyCoreStart = InternalCoreStart & { plugins: PluginsServiceStart }; + +/** + * New platform representation of the legacy configuration (KibanaConfig) + * + * @internal + * @deprecated + */ +export interface LegacyConfig { + get(key?: string): T; + has(key: string): boolean; + set(key: string, value: any): void; + set(config: LegacyVars): void; +} + +/** + * Representation of a legacy configuration deprecation factory used for + * legacy plugin deprecations. + * + * @internal + * @deprecated + */ +export interface LegacyConfigDeprecationFactory { + rename(oldKey: string, newKey: string): LegacyConfigDeprecation; + unused(unusedKey: string): LegacyConfigDeprecation; +} + +/** + * Representation of a legacy configuration deprecation. + * + * @internal + * @deprecated + */ +export type LegacyConfigDeprecation = (settings: LegacyVars, log: (msg: string) => void) => void; + +/** + * Representation of a legacy configuration deprecation provider. + * + * @internal + * @deprecated + */ +export type LegacyConfigDeprecationProvider = ( + factory: LegacyConfigDeprecationFactory +) => LegacyConfigDeprecation[] | Promise; + +/** + * @internal + * @deprecated + */ +export interface LegacyPluginPack { + getPath(): string; +} + +/** + * @internal + * @deprecated + */ +export interface LegacyPluginSpec { + getId: () => unknown; + getExpectedKibanaVersion: () => string; + getConfigPrefix: () => string; + getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined; +} + +/** + * @internal + * @deprecated + */ +export interface VarsProvider { + fn: (server: Server, configValue: any) => LegacyVars; + pluginSpec: { + readConfigValue(config: any, key: string | string[]): any; + }; +} + +/** + * @internal + * @deprecated + */ +export type VarsInjector = () => LegacyVars; + +/** + * @internal + * @deprecated + */ +export type VarsReplacer = ( + vars: LegacyVars, + request: LegacyRequest, + server: Server +) => LegacyVars | Promise; + +/** + * @internal + * @deprecated + */ +export type LegacyNavLinkSpec = Record & ChromeNavLink; + +/** + * @internal + * @deprecated + */ +export type LegacyAppSpec = Pick< + ChromeNavLink, + 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden' +> & { pluginId?: string; id?: string; listed?: boolean }; + +/** + * @internal + * @deprecated + */ +export type LegacyNavLink = Omit & { + order: number; +}; + +/** + * @internal + * @deprecated + */ +export type LegacyUiExports = SavedObjectsLegacyUiExports & { + defaultInjectedVarProviders?: VarsProvider[]; + injectedVarsReplacers?: VarsReplacer[]; + navLinkSpecs?: LegacyNavLinkSpec[] | null; + uiAppSpecs?: Array; + unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }]; +}; + +/** + * @public + * @deprecated + */ +export interface LegacyServiceSetupDeps { + core: LegacyCoreSetup; + plugins: Record; +} + +/** + * @public + * @deprecated + */ +export interface LegacyServiceStartDeps { + core: LegacyCoreStart; + plugins: Record; +} + +/** + * @internal + * @deprecated + */ +export interface ILegacyInternals { + /** + * Inject UI app vars for a particular plugin + */ + injectUiAppVars(id: string, injector: VarsInjector): void; + + /** + * Get all the merged injected UI app vars for a particular plugin + */ + getInjectedUiAppVars(id: string): Promise; + + /** + * Get the metadata vars for a particular plugin + */ + getVars(id: string, request: LegacyRequest, injected?: LegacyVars): Promise; +} + +/** + * @internal + * @deprecated + */ +export interface LegacyPlugins { + disabledPluginSpecs: LegacyPluginSpec[]; + pluginSpecs: LegacyPluginSpec[]; + uiExports: LegacyUiExports; + navLinks: LegacyNavLink[]; +} + +/** + * @internal + * @deprecated + */ +export interface LegacyServiceDiscoverPlugins extends LegacyPlugins { + pluginExtendedConfig: LegacyConfig; + settings: LegacyVars; +} diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 8d3c6a8c909a2..5a52ebccbd472 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -17,28 +17,28 @@ * under the License. */ -import { PluginsService } from './plugins_service'; +import { PluginsService, PluginsServiceSetup } from './plugins_service'; -type ServiceContract = PublicMethodsOf; -const createServiceMock = () => { - const mocked: jest.Mocked = { - discover: jest.fn(), - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - }; - mocked.setup.mockResolvedValue({ - contracts: new Map(), - uiPlugins: { - browserConfigs: new Map(), - internal: new Map(), - public: new Map(), - }, - }); - mocked.start.mockResolvedValue({ contracts: new Map() }); - return mocked; -}; +type PluginsServiceMock = jest.Mocked>; + +const createSetupContractMock = (): PluginsServiceSetup => ({ + contracts: new Map(), + uiPlugins: { + browserConfigs: new Map(), + internal: new Map(), + public: new Map(), + }, +}); +const createStartContractMock = () => ({ contracts: new Map() }); +const createServiceMock = (): PluginsServiceMock => ({ + discover: jest.fn(), + setup: jest.fn().mockResolvedValue(createSetupContractMock()), + start: jest.fn().mockResolvedValue(createStartContractMock()), + stop: jest.fn(), +}); export const pluginServiceMock = { create: createServiceMock, + createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, }; diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts new file mode 100644 index 0000000000000..392b2f0c5e2a4 --- /dev/null +++ b/src/core/server/rendering/__mocks__/params.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mockCoreContext } from '../../core_context.mock'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { pluginServiceMock } from '../../plugins/plugins_service.mock'; +import { legacyServiceMock } from '../../legacy/legacy_service.mock'; + +const context = mockCoreContext.create(); +const http = httpServiceMock.createSetupContract(); +const plugins = pluginServiceMock.createSetupContract(); +const legacyPlugins = legacyServiceMock.createDiscoverPlugins(); + +export const mockRenderingServiceParams = context; +export const mockRenderingSetupDeps = { + http, + legacyPlugins, + plugins, +}; diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts new file mode 100644 index 0000000000000..33dca7cc0d30e --- /dev/null +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RenderingService as Service } from '../rendering_service'; +import { RenderingServiceSetup } from '../types'; +import { mockRenderingServiceParams } from './params'; + +type IRenderingService = PublicMethodsOf; + +export const setupMock: jest.Mocked = { + render: jest.fn(), +}; +export const mockSetup = jest.fn().mockResolvedValue(setupMock); +export const mockStart = jest.fn(); +export const mockStop = jest.fn(); +export const mockRenderingService: jest.Mocked = { + setup: mockSetup, + start: mockStart, + stop: mockStop, +}; +export const RenderingService = jest.fn( + () => mockRenderingService +); diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap new file mode 100644 index 0000000000000..edde1dee85f4f --- /dev/null +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -0,0 +1,719 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RenderingService setup() render() renders "core" from legacy request 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:core", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": false, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "core" page 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:core", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": false, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "core" page driven by settings 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:core", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object { + "theme:darkMode": Object { + "userValue": true, + }, + }, + }, + "version": Any, + }, + "legacyMode": false, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "core" page for blank basepath 1`] = ` +Object { + "basePath": "", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:core", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": false, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:core", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": false, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "legacy" page 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:legacy", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": true, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "legacy" page for blank basepath 1`] = ` +Object { + "basePath": "", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:legacy", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": true, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "legacy" with custom vars 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:legacy", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": true, + "uiPlugins": Array [], + "vars": Object { + "fake": "__TEST_TOKEN__", + }, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "legacy" with excluded user settings 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:legacy", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": true, + "uiPlugins": Array [], + "vars": Object {}, + "version": Any, +} +`; + +exports[`RenderingService setup() render() renders "legacy" with excluded user settings and custom vars 1`] = ` +Object { + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNumber": Any, + "csp": Object { + "warnLegacyBrowsers": true, + }, + "env": Object { + "binDir": Any, + "cliArgs": Object { + "basePath": false, + "dev": true, + "open": false, + "optimize": false, + "oss": false, + "quiet": false, + "repl": false, + "silent": false, + "watch": false, + }, + "configDir": Any, + "configs": Array [], + "homeDir": Any, + "isDevClusterMaster": false, + "logDir": Any, + "mode": Object { + "dev": true, + "name": "development", + "prod": false, + }, + "packageInfo": Object { + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "dist": false, + "version": Any, + }, + "pluginSearchPaths": Any, + "staticFilesDir": Any, + }, + "i18n": Object { + "translationsUrl": "/mock-server-basepath/translations/en.json", + }, + "legacyMetadata": Object { + "app": Object {}, + "basePath": "/mock-server-basepath", + "branch": Any, + "buildNum": Any, + "buildSha": Any, + "bundleId": "app:legacy", + "devMode": true, + "nav": Array [], + "serverName": "http-server-test", + "uiSettings": Object { + "defaults": Object { + "registered": Object { + "name": "title", + }, + }, + "user": Object {}, + }, + "version": Any, + }, + "legacyMode": true, + "uiPlugins": Array [], + "vars": Object { + "fake": "__TEST_TOKEN__", + }, + "version": Any, +} +`; diff --git a/src/core/server/rendering/index.ts b/src/core/server/rendering/index.ts new file mode 100644 index 0000000000000..233f4b26a70db --- /dev/null +++ b/src/core/server/rendering/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { RenderingService } from './rendering_service'; +export * from './types'; diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts new file mode 100644 index 0000000000000..63145f2b30573 --- /dev/null +++ b/src/core/server/rendering/rendering_service.test.ts @@ -0,0 +1,185 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { load } from 'cheerio'; + +import { httpServerMock } from '../http/http_server.mocks'; +import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; +import { mockRenderingServiceParams, mockRenderingSetupDeps } from './__mocks__/params'; +import { RenderingServiceSetup } from './types'; +import { RenderingService } from './rendering_service'; + +const INJECTED_METADATA = { + version: expect.any(String), + branch: expect.any(String), + buildNumber: expect.any(Number), + env: { + binDir: expect.any(String), + configDir: expect.any(String), + homeDir: expect.any(String), + logDir: expect.any(String), + packageInfo: { + branch: expect.any(String), + buildNum: expect.any(Number), + buildSha: expect.any(String), + version: expect.any(String), + }, + pluginSearchPaths: expect.any(Array), + staticFilesDir: expect.any(String), + }, + legacyMetadata: { + branch: expect.any(String), + buildNum: expect.any(Number), + buildSha: expect.any(String), + version: expect.any(String), + }, +}; +const { createKibanaRequest, createRawRequest } = httpServerMock; +const legacyApp = { getId: () => 'legacy' }; + +describe('RenderingService', () => { + let service: RenderingService; + + beforeEach(() => { + jest.clearAllMocks(); + service = new RenderingService(mockRenderingServiceParams); + }); + + describe('setup()', () => { + it('creates instance of RenderingServiceSetup', async () => { + const rendering = await service.setup(mockRenderingSetupDeps); + + expect(rendering.render).toBeInstanceOf(Function); + }); + + describe('render()', () => { + let uiSettings: ReturnType; + let render: RenderingServiceSetup['render']; + + beforeEach(async () => { + uiSettings = uiSettingsServiceMock.createClient(); + uiSettings.getRegistered.mockReturnValue({ + registered: { name: 'title' }, + }); + render = (await service.setup(mockRenderingSetupDeps)).render; + }); + + it('renders "core" page', async () => { + const content = await render(createKibanaRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "core" page for blank basepath', async () => { + mockRenderingSetupDeps.http.basePath.get.mockReturnValueOnce(''); + + const content = await render(createKibanaRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "core" page driven by settings', async () => { + uiSettings.getUserProvided.mockResolvedValue({ 'theme:darkMode': { userValue: true } }); + const content = await render(createKibanaRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "core" with excluded user settings', async () => { + const content = await render(createKibanaRequest(), uiSettings, { + includeUserSettings: false, + }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "core" from legacy request', async () => { + const content = await render(createRawRequest(), uiSettings); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "legacy" page', async () => { + const content = await render(createRawRequest(), uiSettings, { app: legacyApp }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "legacy" page for blank basepath', async () => { + mockRenderingSetupDeps.http.basePath.get.mockReturnValueOnce(''); + + const content = await render(createRawRequest(), uiSettings, { app: legacyApp }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "legacy" with custom vars', async () => { + const content = await render(createRawRequest(), uiSettings, { + app: legacyApp, + vars: { + fake: '__TEST_TOKEN__', + }, + }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "legacy" with excluded user settings', async () => { + const content = await render(createRawRequest(), uiSettings, { + app: legacyApp, + includeUserSettings: false, + }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + + it('renders "legacy" with excluded user settings and custom vars', async () => { + const content = await render(createRawRequest(), uiSettings, { + app: legacyApp, + includeUserSettings: false, + vars: { + fake: '__TEST_TOKEN__', + }, + }); + const dom = load(content); + const data = JSON.parse(dom('kbn-injected-metadata').attr('data')); + + expect(data).toMatchSnapshot(INJECTED_METADATA); + }); + }); + }); +}); diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx new file mode 100644 index 0000000000000..41810c6a10655 --- /dev/null +++ b/src/core/server/rendering/rendering_service.tsx @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { take } from 'rxjs/operators'; + +import { i18n } from '@kbn/i18n'; + +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Template } from './views'; +import { + RenderingSetupDeps, + RenderingServiceSetup, + RenderingMetadata, + LegacyRenderOptions, +} from './types'; + +/** @internal */ +export class RenderingService implements CoreService { + constructor(private readonly coreContext: CoreContext) {} + + public async setup({ + http, + legacyPlugins, + plugins, + }: RenderingSetupDeps): Promise { + async function getUiConfig(pluginId: string) { + const browserConfig = plugins.uiPlugins.browserConfigs.get(pluginId); + + return ((await browserConfig?.pipe(take(1)).toPromise()) ?? {}) as Record; + } + + return { + render: async ( + request, + uiSettings, + { + app = { getId: () => 'core' }, + includeUserSettings = true, + vars = {}, + }: LegacyRenderOptions = {} + ) => { + const { env } = this.coreContext; + const basePath = http.basePath.get(request); + const settings = { + defaults: uiSettings.getRegistered(), + user: includeUserSettings ? await uiSettings.getUserProvided() : {}, + }; + const appId = app.getId(); + const metadata: RenderingMetadata = { + strictCsp: http.csp.strict, + uiPublicUrl: `${basePath}/ui`, + bootstrapScriptUrl: `${basePath}/bundles/app/${appId}/bootstrap.js`, + i18n: i18n.translate, + locale: i18n.getLocale(), + darkMode: settings.user?.['theme:darkMode']?.userValue + ? Boolean(settings.user['theme:darkMode'].userValue) + : false, + injectedMetadata: { + version: env.packageInfo.version, + buildNumber: env.packageInfo.buildNum, + branch: env.packageInfo.branch, + basePath, + env, + legacyMode: appId !== 'core', + i18n: { + translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, + }, + csp: { warnLegacyBrowsers: http.csp.warnLegacyBrowsers }, + vars, + uiPlugins: await Promise.all( + [...plugins.uiPlugins.public].map(async ([id, plugin]) => ({ + id, + plugin, + config: await getUiConfig(id), + })) + ), + legacyMetadata: { + app, + bundleId: `app:${appId}`, + nav: legacyPlugins.navLinks, + version: env.packageInfo.version, + branch: env.packageInfo.branch, + buildNum: env.packageInfo.buildNum, + buildSha: env.packageInfo.buildSha, + serverName: http.server.name, + devMode: env.mode.dev, + basePath, + uiSettings: settings, + }, + }, + }; + + return `${renderToStaticMarkup(