diff --git a/.gitignore b/.gitignore index 02b20da297fc..e7391a5c292d 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ disabledPlugins webpackstats.json /config/* !/config/kibana.yml +!/config/apm.js coverage selenium .babel_register_cache.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2a8459c2b01..17d2ca85a54b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,7 @@ A high level overview of our contributing guidelines. - [Internationalization](#internationalization) - [Testing and Building](#testing-and-building) - [Debugging server code](#debugging-server-code) + - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) - [Debugging Unit Tests](#debugging-unit-tests) - [Unit Testing Plugins](#unit-testing-plugins) - [Cross-browser compatibility](#cross-browser-compatibility) @@ -374,6 +375,21 @@ macOS users on a machine with a discrete graphics card may see significant speed ### Debugging Server Code `yarn debug` will start the server with Node's inspect flag. Kibana's development mode will start three processes on ports `9229`, `9230`, and `9231`. Chrome's developer tools need to be configured to connect to all three connections. Add `localhost:` for each Kibana process in Chrome's developer tools connection tab. +### Instrumenting with Elastic APM +Kibana ships with the [Elastic APM Node.js Agent](https://github.com/elastic/apm-agent-nodejs) built-in for debugging purposes. + +Its default configuration is meant to be used by core Kibana developers only, but it can easily be re-configured to your needs. +In its default configuration it's disabled and will, once enabled, send APM data to a centrally managed Elasticsearch cluster accessible only to Elastic employees. + +To change the location where data is sent, use the [`serverUrl`](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#server-url) APM config option. +To activate the APM agent, use the [`active`](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#active) APM config option. + +All config options can be set either via environment variables, or by creating an appropriate config file under `config/apm.dev.js`. +For more information about configuring the APM agent, please refer to [the documentation](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuring-the-agent.html). + +Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics. +The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana. + ### Unit testing frameworks Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still exist in Mocha but all new unit tests should be written in Jest. Mocha tests @@ -389,7 +405,7 @@ The following table outlines possible test file locations and how to invoke them | Jest | `src/**/*.test.js`
`src/**/*.test.ts` | `node scripts/jest -t regexp [test path]` | | Jest (integration) | `**/integration_tests/**/*.test.js` | `node scripts/jest_integration -t regexp [test path]` | | Mocha | `src/**/__tests__/**/*.js`
`!src/**/public/__tests__/*.js`
`packages/kbn-datemath/test/**/*.js`
`packages/kbn-dev-utils/src/**/__tests__/**/*.js`
`tasks/**/__tests__/**/*.js` | `node scripts/mocha --grep=regexp [test path]` | -| Functional | `test/*integration/**/config.js`
`test/*functional/**/config.js` | `node scripts/functional_tests_server --config test/[directory]/config.js`
`node scripts/functional_test_runner --config test/[directory]/config.js --grep=regexp` | +| Functional | `test/*integration/**/config.js`
`test/*functional/**/config.js`
`test/accessibility/config.js` | `node scripts/functional_tests_server --config test/[directory]/config.js`
`node scripts/functional_test_runner --config test/[directory]/config.js --grep=regexp` | | Karma | `src/**/public/__tests__/*.js` | `npm run test:dev` | For X-Pack tests located in `x-pack/` see [X-Pack Testing](x-pack/README.md#testing) diff --git a/config/apm.js b/config/apm.js new file mode 100644 index 000000000000..8efbbf87487e --- /dev/null +++ b/config/apm.js @@ -0,0 +1,81 @@ +/* + * 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. + */ + +/** + * DO NOT EDIT THIS FILE! + * + * This file contains the configuration for the Elastic APM instrumentaion of + * Kibana itself and is only intented to be used during development of Kibana. + * + * Instrumentation is turned off by default. Once activated it will send APM + * data to an Elasticsearch cluster accessible by Elastic employees. + * + * To modify the configuration, either use environment variables, or create a + * file named `config/apm.dev.js`, which exports a config object as described + * in the docs. + * + * For an overview over the available configuration files, see: + * https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html + * + * For general information about Elastic APM, see: + * https://www.elastic.co/guide/en/apm/get-started/current/index.html + */ + +const { readFileSync } = require('fs'); +const { join } = require('path'); +const { execSync } = require('child_process'); +const merge = require('lodash.merge'); + +module.exports = merge({ + active: false, + serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', + // The secretToken below is intended to be hardcoded in this file even though + // it makes it public. This is not a security/privacy issue. Normally we'd + // instead disable the need for a secretToken in the APM Server config where + // the data is transmitted to, but due to how it's being hosted, it's easier, + // for now, to simply leave it in. + secretToken: 'R0Gjg46pE9K9wGestd', + globalLabels: {}, + centralConfig: false, + logUncaughtExceptions: true +}, devConfig()); + +const rev = gitRev(); +if (rev !== null) module.exports.globalLabels.git_rev = rev; + +try { + const filename = join(__dirname, '..', 'data', 'uuid'); + module.exports.globalLabels.kibana_uuid = readFileSync(filename, 'utf-8'); +} catch (e) {} // eslint-disable-line no-empty + +function gitRev() { + try { + return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + } catch (e) { + return null; + } +} + +function devConfig() { + try { + return require('./apm.dev'); // eslint-disable-line import/no-unresolved + } catch (e) { + return {}; + } +} diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index 6d2c5a72f053..350a3c2a997c 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -98,6 +98,7 @@ When run without any arguments the `FunctionalTestRunner` automatically loads th * `--config test/functional/config.js` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run in Chrome. * `--config test/functional/config.firefox.js` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run in Firefox. * `--config test/api_integration/config.js` starts Elasticsearch and Kibana servers with the api integration tests configuration. +* `--config test/accessibility/config.ts` starts Elasticsearch and Kibana servers with the WebDriver tests configured to run an accessibility audit using https://www.deque.com/axe/[axe]. There are also command line flags for `--bail` and `--grep`, which behave just like their mocha counterparts. For instance, use `--grep=foo` to run only tests that match a regular expression. @@ -362,7 +363,7 @@ Full list of services that are used in functional tests can be found here: {blob ** Source: {blob}test/functional/services/remote/remote.ts[test/functional/services/remote/remote.ts] ** Instance of https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html[WebDriver] class ** Responsible for all communication with the browser -** To perform browser actions, use `remote` service +** To perform browser actions, use `remote` service ** For searching and manipulating with DOM elements, use `testSubjects` and `find` services ** See the https://seleniumhq.github.io/selenium/docs/api/javascript/[selenium-webdriver docs] for the full API. 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 cecceb04240e..1ce18834f531 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 c4ceb47f66e1..6033c667c186 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" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage">) => 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.plugininitializercontext.config.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md index 33a26eef8a7c..56d064dcb290 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md @@ -8,6 +8,9 @@ ```typescript config: { + legacy: { + globalConfig$: Observable; + }; create: () => Observable; createIfExists: () => Observable; }; diff --git a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md index e7aa32edaa29..c2fadfb779fc 100644 --- a/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md +++ b/docs/development/core/server/kibana-plugin-server.plugininitializercontext.md @@ -16,7 +16,7 @@ export interface PluginInitializerContext | Property | Type | Description | | --- | --- | --- | -| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | +| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
legacy: {
globalConfig$: Observable<SharedGlobalConfig>;
};
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
} | | | [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
} | | | [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | LoggerFactory | | | [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId | | diff --git a/package.json b/package.json index ea6276496e84..1ff42384f95d 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "typespec": "typings-tester --config x-pack/legacy/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/legacy/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", - "start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ", + "start": "node --trace-warnings --throw-deprecation scripts/kibana --dev", "debug": "node --nolazy --inspect scripts/kibana --dev", "debug-break": "node --nolazy --inspect-brk scripts/kibana --dev", "karma": "karma start", @@ -159,6 +159,7 @@ "d3-cloud": "1.2.5", "deepmerge": "^4.2.2", "del": "^5.1.0", + "elastic-apm-node": "^3.2.0", "elasticsearch": "^16.5.0", "elasticsearch-browser": "^16.5.0", "encode-uri-query": "1.0.1", @@ -203,6 +204,7 @@ "minimatch": "^3.0.4", "moment": "^2.24.0", "moment-timezone": "^0.5.27", + "monaco-editor": "~0.17.0", "mustache": "2.3.2", "ngreact": "0.5.1", "node-fetch": "1.7.3", @@ -221,7 +223,9 @@ "react-grid-layout": "^0.16.2", "react-input-range": "^1.3.0", "react-markdown": "^3.4.1", + "react-monaco-editor": "~0.27.0", "react-redux": "^5.1.2", + "react-resize-detector": "^4.2.0", "react-router-dom": "^4.3.1", "react-sizeme": "^2.3.6", "reactcss": "1.2.3", @@ -334,6 +338,7 @@ "@types/react": "^16.9.11", "@types/react-dom": "^16.9.4", "@types/react-redux": "^6.0.6", + "@types/react-resize-detector": "^4.0.1", "@types/react-router-dom": "^4.3.1", "@types/react-virtualized": "^9.18.7", "@types/redux": "^3.6.31", @@ -405,7 +410,6 @@ "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", "iedriver": "^3.14.1", - "image-diff": "1.6.3", "intl-messageformat-parser": "^1.4.0", "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", @@ -433,7 +437,7 @@ "node-sass": "^4.9.4", "normalize-path": "^3.0.0", "nyc": "^14.1.1", - "pixelmatch": "4.0.2", + "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", "postcss": "^7.0.5", diff --git a/packages/kbn-babel-code-parser/src/strategies.js b/packages/kbn-babel-code-parser/src/strategies.js index 89621bc53bd5..f116abde9e0e 100644 --- a/packages/kbn-babel-code-parser/src/strategies.js +++ b/packages/kbn-babel-code-parser/src/strategies.js @@ -20,6 +20,7 @@ import { canRequire } from './can_require'; import { dependenciesVisitorsGenerator } from './visitors'; import { dirname, isAbsolute, resolve } from 'path'; +import { builtinModules } from 'module'; export function _calculateTopLevelDependency(inputDep, outputDep = '') { // The path separator will be always the forward slash @@ -48,14 +49,18 @@ export function _calculateTopLevelDependency(inputDep, outputDep = '') { return _calculateTopLevelDependency(depSplitPaths.join(pathSeparator), outputDep); } -export async function dependenciesParseStrategy(cwd, parseSingleFile, mainEntry, wasParsed, results) { - // Retrieve native nodeJS modules - const natives = process.binding('natives'); - +export async function dependenciesParseStrategy( + cwd, + parseSingleFile, + mainEntry, + wasParsed, + results +) { // Get dependencies from a single file and filter // out node native modules from the result - const dependencies = (await parseSingleFile(mainEntry, dependenciesVisitorsGenerator)) - .filter(dep => !natives[dep]); + const dependencies = (await parseSingleFile(mainEntry, dependenciesVisitorsGenerator)).filter( + dep => !builtinModules.includes(dep) + ); // Return the list of all the new entries found into // the current mainEntry that we could use to look for diff --git a/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js b/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js index db33b209951e..64656baee6fd 100644 --- a/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js +++ b/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js @@ -41,19 +41,9 @@ module.exports = function rewritePackage(buildSource, buildVersion, kibanaVersio delete pkg.scripts; delete pkg.devDependencies; - file.contents = toBuffer(JSON.stringify(pkg, null, 2)); + file.contents = Buffer.from(JSON.stringify(pkg, null, 2)); } return file; }); }; - -function toBuffer(string) { - if (typeof Buffer.from === 'function') { - return Buffer.from(string, 'utf8'); - } else { - // this was deprecated in node v5 in favor - // of Buffer.from(string, encoding) - return new Buffer(string, 'utf8'); - } -} diff --git a/scripts/kibana.js b/scripts/kibana.js index b1b470a37535..f5a63e6c07dd 100644 --- a/scripts/kibana.js +++ b/scripts/kibana.js @@ -17,5 +17,6 @@ * under the License. */ +require('../src/apm')(process.env.ELASTIC_APM_PROXY_SERVICE_NAME || 'kibana-proxy'); require('../src/setup_node_env'); require('../src/cli/cli'); diff --git a/src/apm.js b/src/apm.js new file mode 100644 index 000000000000..04a70ee71c53 --- /dev/null +++ b/src/apm.js @@ -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. + */ + +const { existsSync } = require('fs'); +const { join } = require('path'); +const { name, version } = require('../package.json'); + +module.exports = function (serviceName = name) { + if (process.env.kbnWorkerType === 'optmzr') return; + + const conf = { + serviceName: `${serviceName}-${version.replace(/\./g, '_')}` + }; + + if (configFileExists()) conf.configFile = 'config/apm.js'; + else conf.active = false; + + require('elastic-apm-node').start(conf); +}; + +function configFileExists() { + return existsSync(join(__dirname, '..', 'config', 'apm.js')); +} diff --git a/src/cli/cli.js b/src/cli/cli.js index 6b6d45ed2eb3..36cf82ee50bc 100644 --- a/src/cli/cli.js +++ b/src/cli/cli.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { pkg } from '../legacy/utils'; +import { pkg } from '../core/server/utils'; import Command from './command'; import serveCommand from './serve/serve'; diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 13135196e380..8ddeda93e6a7 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -167,7 +167,7 @@ export default class ClusterManager { setupWatching(extraPaths, pluginInternalDirsIgnore) { const chokidar = require('chokidar'); - const { fromRoot } = require('../../legacy/utils'); + const { fromRoot } = require('../../core/server/utils'); const watchPaths = [ fromRoot('src/core'), diff --git a/src/cli/cluster/worker.js b/src/cli/cluster/worker.js index 8eca55a08af2..4d9aba93d61d 100644 --- a/src/cli/cluster/worker.js +++ b/src/cli/cluster/worker.js @@ -21,7 +21,8 @@ import _ from 'lodash'; import cluster from 'cluster'; import { EventEmitter } from 'events'; -import { BinderFor, fromRoot } from '../../legacy/utils'; +import { BinderFor } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; const cliPath = fromRoot('src/cli'); const baseArgs = _.difference(process.argv.slice(2), ['--no-watch']); diff --git a/src/cli/index.js b/src/cli/index.js index 4af5e3c68423..45f88eaf82a5 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -17,5 +17,6 @@ * under the License. */ +require('../apm')(); require('../setup_node_env'); require('./cli'); diff --git a/src/cli/serve/read_keystore.js b/src/cli/serve/read_keystore.js index b5b580cbc2d7..c17091a11f5c 100644 --- a/src/cli/serve/read_keystore.js +++ b/src/cli/serve/read_keystore.js @@ -21,9 +21,9 @@ import path from 'path'; import { set } from 'lodash'; import { Keystore } from '../../legacy/server/keystore'; -import { getData } from '../../legacy/server/path'; +import { getDataPath } from '../../core/server/path'; -export function readKeystore(dataPath = getData()) { +export function readKeystore(dataPath = getDataPath()) { const keystore = new Keystore(path.join(dataPath, 'kibana.keystore')); keystore.load(); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 1f7593d78830..48b5db318d1c 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -22,8 +22,9 @@ import { statSync } from 'fs'; import { resolve } from 'path'; import url from 'url'; -import { fromRoot, IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; -import { getConfig } from '../../legacy/server/path'; +import { IS_KIBANA_DISTRIBUTABLE } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; +import { getConfigPath } from '../../core/server/path'; import { bootstrap } from '../../core/server'; import { readKeystore } from './read_keystore'; @@ -166,7 +167,7 @@ export default function (program) { '-c, --config ', 'Path to the config file, use multiple --config args to include multiple config files', configPathCollector, - [ getConfig() ] + [ getConfigPath() ] ) .option('-p, --port ', 'The port to bind to', parseInt) .option('-q, --quiet', 'Prevent all logging except errors') diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index 823d7bd4f44e..47b49d936028 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -19,12 +19,12 @@ import { join } from 'path'; -import { pkg } from '../legacy/utils'; +import { pkg } from '../core/server/utils'; import Command from '../cli/command'; -import { getData } from '../legacy/server/path'; +import { getDataPath } from '../core/server/path'; import { Keystore } from '../legacy/server/keystore'; -const path = join(getData(), 'kibana.keystore'); +const path = join(getDataPath(), 'kibana.keystore'); const keystore = new Keystore(path); import { createCli } from './create'; diff --git a/src/cli_plugin/cli.js b/src/cli_plugin/cli.js index e4e49a0e2996..cbe82d973442 100644 --- a/src/cli_plugin/cli.js +++ b/src/cli_plugin/cli.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { pkg } from '../legacy/utils'; +import { pkg } from '../core/server/utils'; import Command from '../cli/command'; import listCommand from './list'; import installCommand from './install'; diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index 16927357082e..ad00fdc9eb96 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -17,10 +17,10 @@ * under the License. */ -import { fromRoot, pkg } from '../../legacy/utils'; +import { fromRoot, pkg } from '../../core/server/utils'; import install from './install'; import Logger from '../lib/logger'; -import { getConfig } from '../../legacy/server/path'; +import { getConfigPath } from '../../core/server/path'; import { parse, parseMilliseconds } from './settings'; import logWarnings from '../lib/log_warnings'; import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; @@ -50,7 +50,7 @@ export default function pluginInstall(program) { .option( '-c, --config ', 'path to the config file', - getConfig() + getConfigPath() ) .option( '-t, --timeout ', diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index 9e3da9ee3a49..6e674e63d3a7 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { fromRoot } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; import { resolve } from 'path'; import { parseMilliseconds, parse } from './settings'; diff --git a/src/cli_plugin/list/index.js b/src/cli_plugin/list/index.js index 60965b95ab2e..c0f708b8ccf8 100644 --- a/src/cli_plugin/list/index.js +++ b/src/cli_plugin/list/index.js @@ -17,7 +17,7 @@ * under the License. */ -import { fromRoot } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; import list from './list'; import Logger from '../lib/logger'; import { parse } from './settings'; diff --git a/src/cli_plugin/list/settings.test.js b/src/cli_plugin/list/settings.test.js index 38ec96dff177..812d6ee294eb 100644 --- a/src/cli_plugin/list/settings.test.js +++ b/src/cli_plugin/list/settings.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { fromRoot } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; import { parse } from './settings'; describe('kibana cli', function () { diff --git a/src/cli_plugin/remove/index.js b/src/cli_plugin/remove/index.js index 8937ade8d162..d9eebd03bf0e 100644 --- a/src/cli_plugin/remove/index.js +++ b/src/cli_plugin/remove/index.js @@ -17,11 +17,11 @@ * under the License. */ -import { fromRoot } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; import remove from './remove'; import Logger from '../lib/logger'; import { parse } from './settings'; -import { getConfig } from '../../legacy/server/path'; +import { getConfigPath } from '../../core/server/path'; import logWarnings from '../lib/log_warnings'; import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; @@ -50,7 +50,7 @@ export default function pluginRemove(program) { .option( '-c, --config ', 'path to the config file', - getConfig() + getConfigPath() ) .option( '-d, --plugin-dir ', diff --git a/src/cli_plugin/remove/settings.test.js b/src/cli_plugin/remove/settings.test.js index 8cf8ff1d72c5..027178ae0804 100644 --- a/src/cli_plugin/remove/settings.test.js +++ b/src/cli_plugin/remove/settings.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { fromRoot } from '../../legacy/utils'; +import { fromRoot } from '../../core/server/utils'; import { parse } from './settings'; describe('kibana cli', function () { diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index fbe2740b9610..786409614e6d 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -23,6 +23,8 @@ my_plugin/ └── server ├── routes │ └── index.ts + ├── collectors + │ └── register.ts    ├── services    │   ├── my_service    │   │ └── index.ts @@ -30,7 +32,6 @@ my_plugin/    ├── index.ts    └── plugin.ts ``` - - Both `server` and `public` should have an `index.ts` and a `plugin.ts` file: - `index.ts` should only contain: - The `plugin` export @@ -44,8 +45,9 @@ my_plugin/ - If there is only a single application, this directory can be called `application` that exports the `renderApp` function. - Services provided to other plugins as APIs should live inside the `services` subdirectory. - Services should model the plugin lifecycle (more details below). -- HTTP routes should be contained inside the `routes` directory. +- HTTP routes should be contained inside the `server/routes` directory. - More should be fleshed out here... +- Usage collectors for Telemetry should be defined in a separate `server/collectors/` directory. ### The PluginInitializer @@ -213,7 +215,7 @@ export class Plugin { ### Usage Collection -For creating and registering a Usage Collector. Collectors would be defined in a separate directory `server/collectors/register.ts`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`. +For creating and registering a Usage Collector. Collectors should be defined in a separate directory `server/collectors/`. You can read more about usage collectors on `src/plugins/usage_collection/README.md`. ```ts // server/collectors/register.ts @@ -247,3 +249,8 @@ export function registerMyPluginUsageCollector(usageCollection?: UsageCollection usageCollection.registerCollector(myCollector); } ``` + +### Naming conventions + +Export start and setup contracts as `MyPluginStart` and `MyPluginSetup`. +This avoids naming clashes, if everyone exported them simply as `Start` and `Setup`. diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 7c1489a345e5..eb9a66e99f0a 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -16,15 +16,15 @@ - [Switch to new platform services](#switch-to-new-platform-services) - [Migrate to the new plugin system](#migrate-to-the-new-plugin-system) - [Browser-side plan of action](#browser-side-plan-of-action) - - [1. Create a plugin definition file](#1-create-a-plugin-definition-file) - - [2. Export all static code and types from `public/index.ts`](#2-export-all-static-code-and-types-from-publicindexts) - - [3. Export your runtime contract](#3-export-your-runtime-contract) - - [4. Move "owned" UI modules into your plugin and expose them from your public contract](#4-move-owned-ui-modules-into-your-plugin-and-expose-them-from-your-public-contract) - - [5. Provide plugin extension points decoupled from angular.js](#5-provide-plugin-extension-points-decoupled-from-angularjs) - - [6. Move all webpack alias imports into uiExport entry files](#6-move-all-webpack-alias-imports-into-uiexport-entry-files) - - [7. Switch to new platform services](#7-switch-to-new-platform-services) - - [8. Migrate to the new plugin system](#8-migrate-to-the-new-plugin-system) - - [Bonus: Tips for complex migration scenarios](#bonus-tips-for-complex-migration-scenarios) + - [1. Create a plugin definition file](#1-create-a-plugin-definition-file) + - [2. Export all static code and types from `public/index.ts`](#2-export-all-static-code-and-types-from-publicindexts) + - [3. Export your runtime contract](#3-export-your-runtime-contract) + - [4. Move "owned" UI modules into your plugin and expose them from your public contract](#4-move-owned-ui-modules-into-your-plugin-and-expose-them-from-your-public-contract) + - [5. Provide plugin extension points decoupled from angular.js](#5-provide-plugin-extension-points-decoupled-from-angularjs) + - [6. Move all webpack alias imports into uiExport entry files](#6-move-all-webpack-alias-imports-into-uiexport-entry-files) + - [7. Switch to new platform services](#7-switch-to-new-platform-services) + - [8. Migrate to the new plugin system](#8-migrate-to-the-new-plugin-system) + - [Bonus: Tips for complex migration scenarios](#bonus-tips-for-complex-migration-scenarios) - [Frequently asked questions](#frequently-asked-questions) - [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) - [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) @@ -71,7 +71,7 @@ Plugins are defined as classes and exposed to the platform itself through a simp The basic file structure of a new platform plugin named "demo" that had both client-side and server-side code would be: -``` +```tree src/plugins demo kibana.json [1] @@ -83,7 +83,7 @@ src/plugins plugin.ts [5] ``` -**[1] `kibana.json`** is a static manifest file that is used to identify the plugin and to determine what kind of code the platform should execute from the plugin: +**[1] `kibana.json`** is a [static manifest](../../docs/development/core/server/kibana-plugin-server.pluginmanifest.md) file that is used to identify the plugin and to determine what kind of code the platform should execute from the plugin: ```json { @@ -93,13 +93,14 @@ src/plugins "ui": true } ``` +More details about[manifest file format](/docs/development/core/server/kibana-plugin-server.pluginmanifest.md) Note that `package.json` files are irrelevant to and ignored by the new platform. **[2] `public/index.ts`** is the entry point into the client-side code of this plugin. It must export a function named `plugin`, which will receive a standard set of core capabilities as an argument (e.g. logger). It should return an instance of its plugin definition for the platform to register at load time. ```ts -import { PluginInitializerContext } from '../../../core/public'; +import { PluginInitializerContext } from 'kibana/server'; import { Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { @@ -110,7 +111,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[3] `public/plugin.ts`** is the client-side plugin definition itself. Technically speaking it does not need to be a class or even a separate file from the entry point, but _all plugins at Elastic_ should be consistent in this way. ```ts -import { PluginInitializerContext, CoreSetup, CoreStart } from '../../../core/public'; +import { PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -133,7 +134,7 @@ export class Plugin { **[4] `server/index.ts`** is the entry-point into the server-side code of this plugin. It is identical in almost every way to the client-side entry-point: ```ts -import { PluginInitializerContext } from '../../../core/server'; +import { PluginInitializerContext } from 'kibana/server'; import { Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { @@ -144,7 +145,7 @@ export function plugin(initializerContext: PluginInitializerContext) { **[5] `server/plugin.ts`** is the server-side plugin definition. The _shape_ of this plugin is the same as it's client-side counter-part: ```ts -import { PluginInitializerContext, CoreSetup, CoreStart } from '../../../core/server'; +import { PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server'; export class Plugin { constructor(initializerContext: PluginInitializerContext) { @@ -185,7 +186,7 @@ There is no equivalent behavior to `start` or `stop` in legacy plugins, so this The lifecycle-specific contracts exposed by core services are always passed as the first argument to the equivalent lifecycle function in a plugin. For example, the core `UiSettings` service exposes a function `get` to all plugin `setup` functions. To use this function to retrieve a specific UI setting, a plugin just accesses it off of the first argument: ```ts -import { CoreSetup } from '../../../core/public'; +import { CoreSetup } from 'kibana/server'; export class Plugin { public setup(core: CoreSetup) { @@ -200,6 +201,15 @@ For example, the `stop` function in the browser gets invoked as part of the `win Core services that expose functionality to plugins always have their `setup` function ran before any plugins. +These are the contracts exposed by the core services for each lifecycle event: + +| lifecycle event | contract | +| --------------- | --------------------------------------------------------------------------------------------------------------- | +| *contructor* | [PluginInitializerContext](../../docs/development/core/server/kibana-plugin-server.plugininitializercontext.md) | +| *setup* | [CoreSetup](../../docs/development/core/server/kibana-plugin-server.coresetup.md) | +| *start* | [CoreStart](../../docs/development/core/server/kibana-plugin-server.corestart.md) | +| *stop* | | + ### Integrating with other plugins Plugins can expose public interfaces for other plugins to consume. Like `core`, those interfaces are bound to the lifecycle functions `setup` and/or `start`. @@ -357,6 +367,7 @@ npStart.plugins.data.fieldFormats.getType(myFieldFormatId); Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. This greatly simplifies the plan of action for migrating server-side plugins. Here is the high-level for migrating a server-side plugin: + - De-couple from hapi.js server and request objects - Introduce a new plugin definition shim - Replace legacy services in shim with new platform services @@ -515,7 +526,7 @@ interface FooSetup { } // We inject the miminal legacy dependencies into our plugin including dependencies on other legacy -// plugins. Take care to only expose the legacy functionality you need e.g. don't inject the whole +// plugins. Take care to only expose the legacy functionality you need e.g. don't inject the whole // `Legacy.Server` if you only depend on `Legacy.Server['route']`. interface LegacySetup { route: Legacy.Server['route'] @@ -539,7 +550,7 @@ export class DemoPlugin implements Plugin { }); } ``` + > Note: An equally valid approach is to extend `CoreSetup` with a `__legacy` > property instead of introducing a third parameter to your plugins lifecycle > function. The important thing is that you reduce the legacy API surface that @@ -657,7 +669,7 @@ the legacy core. A similar approach can be taken for your plugin dependencies. To start consuming an API from a New Platform plugin access these from `server.newPlatform.setup.plugins` and inject it into your plugin's setup -function. +function. ```ts init(server) { @@ -689,7 +701,7 @@ entirely powered by the New Platform and New Platform plugins. > Note: All New Platform plugins are exposed to legacy plugins via > `server.newPlatform.setup.plugins`. Once you move your plugin over to the > New Platform you will have to explicitly declare your dependencies on other -> plugins in your `kibana.json` manifest file. +> plugins in your `kibana.json` manifest file. At this point, your legacy server-side plugin logic is no longer coupled to legacy plugins. @@ -702,6 +714,7 @@ Many plugins will copy and paste all of their plugin code into a new plugin dire With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. Other plugins may want to move subsystems over individually. For instance, you can move routes over to the New Platform in groups rather than all at once. Other examples that could be broken up: + - Configuration schema ([see example](./MIGRATION_EXAMPLES.md#declaring-config-schema)) - HTTP route registration ([see example](./MIGRATION_EXAMPLES.md#http-routes)) - Polling mechanisms (eg. job worker) @@ -726,7 +739,7 @@ This definition isn't going to do much for us just yet, but as we get further in ```ts // public/plugin.ts -import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; +import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; import { FooSetup, FooStart } from '../../../../legacy/core_plugins/foo/public'; /** @@ -794,7 +807,7 @@ While you're at it, you can also add your plugin initializer to this file: ```ts // public/index.ts -import { PluginInitializer, PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializer, PluginInitializerContext } from 'kibana/server'; import { DemoSetup, DemoStart, DemoSetupDeps, DemoStartDeps, DemoPlugin } from './plugin'; // Core will be looking for this when loading our plugin in the new platform @@ -828,7 +841,7 @@ So we will take a similar approach to what was described above in the server sec ```ts // public/legacy.ts -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from 'kibana/server'; import { npSetup, npStart } from 'ui/new_platform'; import { plugin } from '.'; @@ -858,10 +871,10 @@ The point is that, over time, this becomes the one file in our plugin containing Everything inside of the `ui/public` directory is going to be dealt with in one of the following ways: -* Deleted because it doesn't need to be used anymore -* Moved to or replaced by something in core that isn't coupled to angular -* Moved to or replaced by an extension point in a specific plugin that "owns" that functionality -* Copied into each plugin that depends on it and becomes an implementation detail there +- Deleted because it doesn't need to be used anymore +- Moved to or replaced by something in core that isn't coupled to angular +- Moved to or replaced by an extension point in a specific plugin that "owns" that functionality +- Copied into each plugin that depends on it and becomes an implementation detail there To rapidly define ownership and determine interdependencies, UI modules should move to the most appropriate plugins to own them. Modules that are considered "core" can remain in the ui directory as the platform team works to move them out. @@ -873,12 +886,12 @@ If it is determined that your plugin is going to own any UI modules that other p Depending on the module's level of complexity and the number of other places in Kibana that rely on it, there are a number of strategies you could use for this: -* **Do it all at once.** Move the code, expose it from your plugin, and update all imports across Kibana. +- **Do it all at once.** Move the code, expose it from your plugin, and update all imports across Kibana. - This works best for small pieces of code that aren't widely used. -* **Shim first, move later.** Expose the code from your plugin by importing it in your shim and then re-exporting it from your plugin first, then gradually update imports to pull from the new location, leaving the actual moving of the code as a final step. +- **Shim first, move later.** Expose the code from your plugin by importing it in your shim and then re-exporting it from your plugin first, then gradually update imports to pull from the new location, leaving the actual moving of the code as a final step. - This works best for the largest, most widely used modules that would otherwise result in huge, hard-to-review PRs. - It makes things easier by splitting the process into small, incremental PRs, but is probably overkill for things with a small surface area. -* **Hybrid approach.** As a middle ground, you can also move the code to your plugin immediately, and then re-export your plugin code from the original `ui/public` directory. +- **Hybrid approach.** As a middle ground, you can also move the code to your plugin immediately, and then re-export your plugin code from the original `ui/public` directory. - This eliminates any concerns about backwards compatibility by allowing you to update the imports across Kibana later. - Works best when the size of the PR is such that moving the code can be done without much refactoring. @@ -940,6 +953,7 @@ Many plugins at this point will copy over their plugin definition class & the co With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. Other plugins may want to move subsystems over individually. Examples of pieces that could be broken up: + - Registration logic (eg. viz types, embeddables, chrome nav controls) - Application mounting - Polling mechanisms (eg. job worker) @@ -977,6 +991,7 @@ At the very least, any plugin exposing an extension point should do so with firs Legacy Kibana has never run as a single page application. Each plugin has it's own entry point and gets "ownership" of every module it imports when it is loaded into the browser. This has allowed stateful modules to work without breaking other plugins because each time the user navigates to a new plugin, the browser reloads with a different entry bundle, clearing the state of the previous plugin. Because of this "feature" many undesirable things developed in the legacy platform: + - We had to invent an unconventional and fragile way of allowing plugins to integrate and communicate with one another, `uiExports`. - It has never mattered if shared modules in `ui/public` were stateful or cleaned up after themselves, so many of them behave like global singletons. These modules could never work in single-page application because of this state. - We've had to ship Webpack with Kibana in production so plugins could be disabled or installed and still have access to all the "platform" features of `ui/public` modules and all the `uiExports` would be present for any enabled plugins. @@ -994,7 +1009,6 @@ One goal of a stable Kibana core API is to allow Kibana instances to run plugins This method of building and installing plugins comes with side effects which are important to be aware of when developing a plugin. - - **Any code you export to other plugins will get copied into their bundles.** If a plugin is built for 8.1 and is running on Kibana 8.2, any modules it imported that changed will not be updated in that plugin. - **When a plugin is disabled, other plugins can still import its static exports.** This can make code difficult to reason about and result in poor user experience. For example, users generally expect that all of a plugin’s features will be disabled when the plugin is disabled. If another plugin imports a disabled plugin’s feature and exposes it to the user, then users will be confused about whether that plugin really is disabled or not. - **Plugins cannot share state by importing each others modules.** Sharing state via imports does not work because exported modules will be copied into plugins that import them. Let’s say your plugin exports a module that’s imported by other plugins. If your plugin populates state into this module, a natural expectation would be that the other plugins now have access to this state. However, because those plugins have copies of the exported module, this assumption will be incorrect. @@ -1004,16 +1018,19 @@ This method of building and installing plugins comes with side effects which are The general rule of thumb here is: any module that is not purely functional should not be shared statically, and instead should be exposed at runtime via the plugin's `setup` and/or `start` contracts. Ask yourself these questions when deciding to share code through static exports or plugin contracts: + - Is its behavior dependent on any state populated from my plugin? - If a plugin uses an old copy (from an older version of Kibana) of this module, will it still break? If you answered yes to any of the above questions, you probably have an impure module that cannot be shared across plugins. Another way to think about this: if someone literally copied and pasted your exported module into their plugin, would it break if: + - Your original module changed in a future version and the copy was the old version; or - If your plugin doesn’t have access to the copied version in the other plugin (because it doesn't know about it). If your module were to break for either of these reasons, it should not be exported statically. This can be more easily illustrated by examples of what can and cannot be exported statically. Examples of code that could be shared statically: + - Constants. Strings and numbers that do not ever change (even between Kibana versions) - If constants do change between Kibana versions, then they should only be exported statically if the old value would not _break_ if it is still used. For instance, exporting a constant like `VALID_INDEX_NAME_CHARACTERS` would be fine, but exporting a constant like `API_BASE_PATH` would not because if this changed, old bundles using the previous value would break. - React components that do not depend on module state. @@ -1022,25 +1039,33 @@ Examples of code that could be shared statically: - Pure computation functions, for example lodash-like functions like `mapValues`. Examples of code that could **not** be shared statically and how to fix it: + - A function that calls a Core service, but does not take that service as a parameter. - If the function does not take a client as an argument, it must have an instance of the client in its internal state, populated by your plugin. This would not work across plugin boundaries because your plugin would not be able to call `setClient` in the copy of this module in other plugins: + ```js let esClient; export const setClient = (client) => esClient = client; export const query = (params) => esClient.search(params); ``` + - This could be fixed by requiring the calling code to provide the client: + ```js export const query = (esClient, params) => esClient.search(params); ``` + - A function that allows other plugins to register values that get pushed into an array defined internally to the module. - The values registered would only be visible to the plugin that imported it. Each plugin would essentially have their own registry of visTypes that is not visible to any other plugins. + ```js const visTypes = []; export const registerVisType = (visType) => visTypes.push(visType); export const getVisTypes = () => visTypes; ``` + - For state that does need to be shared across plugins, you will need to expose methods in your plugin's `setup` and `start` contracts. + ```js class MyPlugin { constructor() { this.visTypes = [] } @@ -1084,6 +1109,7 @@ If you have code that should be available to other plugins on both the client an ### How can I avoid passing Core services deeply within my UI component tree? There are some Core services that are purely presentational, for example `core.overlays.openModal()` or `core.application.createLink()` where UI code does need access to these deeply within your application. However, passing these services down as props throughout your application leads to lots of boilerplate. To avoid this, you have three options: + 1. Use an abstraction layer, like Redux, to decouple your UI code from core (**this is the highly preferred option**); or - [redux-thunk](https://github.com/reduxjs/redux-thunk#injecting-a-custom-argument) and [redux-saga](https://redux-saga.js.org/docs/api/#createsagamiddlewareoptions) already have ways to do this. 2. Use React Context to provide these services to large parts of your React tree; or @@ -1121,12 +1147,12 @@ The packages directory should have the least amount of code in Kibana. Just beca Many of the utilities you're using to build your plugins are available in the New Platform or in New Platform plugins. To help you build the shim for these new services, use the tables below to find where the New Platform equivalent lives. - #### Client-side TODO: add links to API docs on items in "New Platform" column. ##### Core services + In client code, `core` can be imported in legacy plugins via the `ui/new_platform` module. ```ts @@ -1140,7 +1166,6 @@ import { npStart: { core } } from 'ui/new_platform'; | `chrome.getUiSettingsClient` | [`core.uiSettings`](/docs/development/core/public/kibana-plugin-public.uisettingsclient.md) | | | `chrome.helpExtension.set` | [`core.chrome.setHelpExtension`](/docs/development/core/public/kibana-plugin-public.chromestart.sethelpextension.md) | | | `chrome.setVisible` | [`core.chrome.setIsVisible`](/docs/development/core/public/kibana-plugin-public.chromestart.setisvisible.md) | | -| `chrome.getInjected` | -- | Not implemented yet, see [#41990](https://github.com/elastic/kibana/issues/41990) | | `chrome.setRootTemplate` / `chrome.setRootController` | -- | Use application mounting via `core.application.register` (not available to legacy plugins at this time). | | `import { recentlyAccessed } from 'ui/persisted_log'` | [`core.chrome.recentlyAccessed`](/docs/development/core/public/kibana-plugin-public.chromerecentlyaccessed.md) | | | `ui/capabilities` | [`core.application.capabilities`](/docs/development/core/public/kibana-plugin-public.capabilities.md) | | @@ -1150,11 +1175,12 @@ import { npStart: { core } } from 'ui/new_platform'; | `ui/routes` | -- | There is no global routing mechanism. Each app [configures its own routing](/rfcs/text/0004_application_service_mounting.md#complete-example). | | `ui/saved_objects` | [`core.savedObjects`](/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md) | Client API is the same | | `ui/doc_title` | [`core.chrome.docTitle`](/docs/development/core/public/kibana-plugin-public.chromedoctitle.md) | | -| `uiExports/injectedVars` | [Configure plugin](#configure-plugin) and [`PluginConfigDescriptor.exposeToBrowser`](/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Can only be used to expose configuration properties | +| `uiExports/injectedVars` / `chrome.getInjected` | [Configure plugin](#configure-plugin) and [`PluginConfigDescriptor.exposeToBrowser`](/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | Can only be used to expose configuration properties | _See also: [Public's CoreStart API Docs](/docs/development/core/public/kibana-plugin-public.corestart.md)_ ##### Plugins for shared application services + In client code, we have a series of plugins which house shared application services that are being built in the shape of the new platform, but for the time being, are only available in legacy. So if your plugin depends on any of the APIs below, you'll need build your plugin as a legacy plugin that shims the new platform. Once these API's have been moved to the new platform you can migrate your plugin and declare a dependency on the plugin that owns the API's you require. The contracts for these plugins are exposed for you to consume in your own plugin; we have created dedicated exports for the `setup` and `start` contracts in a file called `legacy`. By passing these contracts to your plugin's `setup` and `start` methods, you can mimic the functionality that will eventually be provided in the new platform. @@ -1189,6 +1215,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; #### Server-side ##### Core services + In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side: | Legacy Platform | New Platform | Notes | @@ -1251,6 +1278,7 @@ Examples: - **uiSettingDefaults** Before: + ```js uiExports: { uiSettingDefaults: { @@ -1263,7 +1291,9 @@ uiExports: { } } ``` + After: + ```ts // src/plugins/my-plugin/server/plugin.ts setup(core: CoreSetup){ @@ -1281,6 +1311,7 @@ setup(core: CoreSetup){ ## How to ### Configure plugin + Kibana provides ConfigService if a plugin developer may want to support adjustable runtime behavior for their plugins. Access to Kibana config in New platform has been subject to significant refactoring. Config service does not provide access to the whole config anymore. New platform plugin cannot read configuration parameters of the core services nor other plugins directly. Use plugin contract to provide data. @@ -1294,8 +1325,10 @@ const basePath = core.http.basePath.get(request); ``` In order to have access to your plugin config, you *should*: + - Declare plugin specific "configPath" (will fallback to plugin "id" if not specified) in `kibana.json` file. - Export schema validation for config from plugin's main file. Schema is mandatory. If a plugin reads from the config without schema declaration, ConfigService will throw an error. + ```typescript // my_plugin/server/index.ts import { schema, TypeOf } from '@kbn/config-schema'; @@ -1305,7 +1338,9 @@ export const config = { }; export type MyPluginConfigType = TypeOf; ``` + - Read config value exposed via initializerContext. No config path is required. + ```typescript class MyPlugin { constructor(initializerContext: PluginInitializerContext) { @@ -1316,6 +1351,7 @@ class MyPlugin { ``` If your plugin also have a client-side part, you can also expose configuration properties to it using a whitelisting mechanism with the configuration `exposeToBrowser` property. + ```typescript // my_plugin/server/index.ts import { schema, TypeOf } from '@kbn/config-schema'; @@ -1337,6 +1373,7 @@ export const config: PluginConfigDescriptor = { ``` Configuration containing only the exposed properties will be then available on the client-side using the plugin's `initializerContext`: + ```typescript // my_plugin/public/index.ts interface ClientConfigType { @@ -1352,10 +1389,19 @@ export class Plugin implements Plugin { } ``` +All plugins are considered enabled by default. If you want to disable your plugin by default, you could declare the `enabled` flag in plugin config. This is a special Kibana platform key. The platform reads its value and won't create a plugin instance if `enabled: false`. +```js +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; +``` + ### Mock new platform services in tests #### Writing mocks for your plugin + Core services already provide mocks to simplify testing and make sure plugins always rely on valid public contracts: + ```typescript // my_plugin/server/plugin.test.ts import { configServiceMock } from 'src/core/server/mocks'; @@ -1367,6 +1413,7 @@ const plugin = new MyPlugin({ configService }, …); ``` Or if you need to get the whole core `setup` or `start` contracts: + ```typescript // my_plugin/public/plugin.test.ts import { coreMock } from 'src/core/public/mocks'; @@ -1379,8 +1426,8 @@ coreSetup.uiSettings.get.mockImplementation((key: string) => { const plugin = new MyPlugin(coreSetup, ...); ``` - Although it isn't mandatory, we strongly recommended you export your plugin mocks as well, in order for dependent plugins to use them in tests. Your plugin mocks should be exported from the root `/server` and `/public` directories in your plugin: + ```typescript // my_plugin/server/mocks.ts or my_plugin/public/mocks.ts const createSetupContractMock = () => { @@ -1397,26 +1444,31 @@ export const myPluginMocks = { createStart: … } ``` + Plugin mocks should consist of mocks for *public APIs only*: setup/start/stop contracts. Mocks aren't necessary for pure functions as other plugins can call the original implementation in tests. #### Using mocks in your tests + During the migration process, it is likely you are preparing your plugin by shimming in new platform-ready dependencies via the legacy `ui/new_platform` module: + ```typescript import { npSetup, npStart } from 'ui/new_platform'; ``` If you are using this approach, the easiest way to mock core and new platform-ready plugins in your legacy tests is to mock the `ui/new_platform` module: + ```typescript jest.mock('ui/new_platform'); ``` -This will automatically mock the services in `ui/new_platform` thanks to the [helpers that have been added](https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/new_platform/__mocks__/helpers.ts) to that module. +This will automatically mock the services in `ui/new_platform` thanks to the [helpers that have been added](../../src/legacy/ui/public/new_platform/__mocks__/helpers.ts) to that module. If others are consuming your plugin's new platform contracts via the `ui/new_platform` module, you'll want to update the helpers as well to ensure your contracts are properly mocked. > Note: The `ui/new_platform` mock is only designed for use by old Jest tests. If you are writing new tests, you should structure your code and tests such that you don't need this mock. Instead, you should import the `core` mock directly and instantiate it. #### What about karma tests? + While our plan is to only provide first-class mocks for Jest tests, there are many legacy karma tests that cannot be quickly or easily converted to Jest -- particularly those which are still relying on mocking Angular services via `ngMock`. For these tests, we are maintaining a separate set of mocks. Files with a `.karma_mock.{js|ts|tsx}` extension will be loaded _globally_ before karma tests are run. @@ -1424,11 +1476,15 @@ For these tests, we are maintaining a separate set of mocks. Files with a `.karm It is important to note that this behavior is different from `jest.mock('ui/new_platform')`, which only mocks tests on an individual basis. If you encounter any failures in karma tests as a result of new platform migration efforts, you may need to add a `.karma_mock.js` file for the affected services, or add to the existing karma mock we are maintaining in `ui/new_platform`. ### Provide Legacy Platform API to the New platform plugin + #### On the server side + During migration, you can face a problem that not all API is available in the New platform yet. You can work around this by extending your new platform plugin with Legacy API: + - create New platform plugin - New platform plugin should expose a method `registerLegacyAPI` that allows passing API from the Legacy platform and store it in the NP plugin instance + ```js class MyPlugin { public async setup(core){ @@ -1438,7 +1494,9 @@ class MyPlugin { } } ``` + - The legacy plugin provides API calling `registerLegacyAPI` + ```js new kibana.Plugin({ init(server){ @@ -1450,7 +1508,9 @@ new kibana.Plugin({ } }) ``` + - The new platform plugin access stored Legacy platform API via `getLegacyAPI` getter. Getter function must have name indicating that’s API provided from the Legacy platform. + ```js class MyPlugin { private getLegacyAPI(){ @@ -1469,6 +1529,7 @@ class MyPlugin { ``` #### On the client side -It's not currently possible to use a similar pattern on the client-side. -Because Legacy platform plugins heavily rely on global angular modules, which aren't available on the new platform. + +It's not currently possible to use a similar pattern on the client-side. +Because Legacy platform plugins heavily rely on global angular modules, which aren't available on the new platform. So you can utilize the same approach for only *stateless Angular components*, as long as they are not consumed by a New Platform application. When New Platform applications are on the page, no legacy code is executed, so the `registerLegacyAPI` function would not be called. diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index ccf14879baa3..04535039acbe 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -56,6 +56,62 @@ export const config = { export type MyPluginConfig = TypeOf; ``` +### Using New Platform config in a new plugin + +After setting the config schema for your plugin, you might want to reach the configuration in the plugin. +It is provided as part of the [PluginInitializerContext](../../docs/development/core/server/kibana-plugin-server.plugininitializercontext.md) +in the *constructor* of the plugin: + +```ts +// myPlugin/(public|server)/index.ts + +import { PluginInitializerContext } from 'kibana/server'; +import { MyPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new MyPlugin(initializerContext); +} +``` + +```ts +// myPlugin/(public|server)/plugin.ts + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { CoreSetup, Logger, Plugin, PluginInitializerContext, PluginName } from 'kibana/server'; +import { MyPlugin } from './plugin'; + +export class MyPlugin implements Plugin { + private readonly config$: Observable; + private readonly log: Logger; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.log = initializerContext.logger.get(); + this.config$ = initializerContext.config.create(); + } + + public async setup(core: CoreSetup, deps: Record) { + const isEnabled = await this.config$.pipe(first()).toPromise(); + ... + } + ... +} +} +``` + +Additionally, some plugins need to read other plugins' config to act accordingly (like timing out a request, matching ElasticSearch's timeout). For those use cases, the plugin can rely on the *globalConfig* and *env* properties in the context: + +```ts +export class MyPlugin implements Plugin { +... + public async setup(core: CoreSetup, deps: Record) { + const { mode: { dev }, packageInfo: { version } } = this.initializerContext.env; + const { elasticsearch: { shardTimeout }, path: { data } } = await this.initializerContext.config.legacy.globalConfig$ + .pipe(first()).toPromise(); + ... + } +``` + ### Using New Platform config from a Legacy plugin During the migration process, you'll want to migrate your schema to the new @@ -64,6 +120,7 @@ config service due to the way that config is tied to the `kibana.json` file (which does not exist for legacy plugins). There is a workaround though: + - Create a New Platform plugin that contains your plugin's config schema in the new format - Expose the config from the New Platform plugin in its setup contract - Read the config from the setup contract in your legacy plugin @@ -153,6 +210,7 @@ interface, which is exposed via the object injected into the `setup` method of server-side plugins. This interface has a different API with slightly different behaviors. + - All input (body, query parameters, and URL parameters) must be validated using the `@kbn/config-schema` package. If no validation schema is provided, these values will be empty objects. @@ -166,6 +224,7 @@ Because of the incompatibility between the legacy and New Platform HTTP Route API's it might be helpful to break up your migration work into several stages. ### 1. Legacy route registration + ```ts // legacy/plugins/myplugin/index.ts import Joi from 'joi'; @@ -191,6 +250,7 @@ new kibana.Plugin({ ``` ### 2. New Platform shim using legacy router + Create a New Platform shim and inject the legacy `server.route` into your plugin's setup function. @@ -214,6 +274,7 @@ export default (kibana) => { } } ``` + ```ts // legacy/plugins/demoplugin/server/plugin.ts import { CoreSetup } from 'src/core/server'; @@ -246,11 +307,13 @@ export class Plugin { ``` ### 3. New Platform shim using New Platform router + We now switch the shim to use the real New Platform HTTP API's in `coreSetup` instead of relying on the legacy `server.route`. Since our plugin is now using -the New Platform API's we are guaranteed that our HTTP route handling is 100% +the New Platform API's we are guaranteed that our HTTP route handling is 100% compatible with the New Platform. As a result, we will also have to adapt our -route registration accordingly. +route registration accordingly. + ```ts // legacy/plugins/demoplugin/index.ts import { Plugin } from './server/plugin'; @@ -268,6 +331,7 @@ export default (kibana) => { } } ``` + ```ts // legacy/plugins/demoplugin/server/plugin.ts import { schema } from '@kbn/config-schema'; @@ -298,8 +362,10 @@ class Plugin { } } ``` -If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` + +If your plugin still relies on throwing Boom errors from routes, you can use the `router.handleLegacyErrors` as a temporary solution until error migration is complete: + ```ts // legacy/plugins/demoplugin/server/plugin.ts import { schema } from '@kbn/config-schema'; @@ -327,11 +393,12 @@ class Plugin { } ``` - #### 4. New Platform plugin + As the final step we delete the shim and move all our code into a New Platform plugin. Since we were already consuming the New Platform API's no code changes are necessary inside `plugin.ts`. + ```ts // Move legacy/plugins/demoplugin/server/plugin.ts -> plugins/demoplugin/server/plugin.ts ``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index bde148f1e1e4..15f8b544fd63 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -871,7 +871,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index cde35f3cbe99..276e3955a467 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -17,6 +17,8 @@ * under the License. */ +import apm from 'elastic-apm-node'; + import { ByteSizeValue } from '@kbn/config-schema'; import { Server, Request } from 'hapi'; import Url from 'url'; @@ -139,6 +141,7 @@ export class BasePathProxyServer { // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { + apm.setTransactionName(`${request.method.toUpperCase()} /{basePath}/{kbnPath*}`); await blockUntil(); return responseToolkit.continue; }, diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 7d95110b98a1..6117190c57ba 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -329,7 +329,7 @@ describe('Response factory', () => { const router = createRouter('/'); router.get({ path: '/', validate: false }, (context, req, res) => { - const buffer = new Buffer('abc'); + const buffer = Buffer.from('abc'); return res.ok({ body: buffer, diff --git a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap index 9a23b3b3b23b..69b7f9fc7831 100644 --- a/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/server/legacy/__snapshots__/legacy_service.test.ts.snap @@ -7,6 +7,7 @@ Array [ "logging": Object { "verbose": true, }, + "path": Object {}, }, ], ] @@ -19,6 +20,7 @@ Array [ "logging": Object { "verbose": true, }, + "path": Object {}, }, ], ] diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 0bd4eafe78ce..73b7ced60ee4 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -133,8 +133,8 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( - { server: { autoListen: true } }, - { server: { autoListen: true } }, + { path: { autoListen: true }, server: { autoListen: true } }, + { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value expect.any(Object), { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); @@ -159,8 +159,8 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( - { server: { autoListen: true } }, - { server: { autoListen: true } }, + { path: { autoListen: false }, server: { autoListen: true } }, + { path: { autoListen: false }, server: { autoListen: true } }, expect.any(Object), { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); @@ -296,8 +296,8 @@ describe('once LegacyService is set up without connection info', () => { test('creates legacy kbnServer with `autoListen: false`.', () => { expect(MockKbnServer).toHaveBeenCalledTimes(1); expect(MockKbnServer).toHaveBeenCalledWith( - { server: { autoListen: true } }, - { server: { autoListen: true } }, + { path: {}, server: { autoListen: true } }, + { path: {}, server: { autoListen: true } }, expect.any(Object), { disabledPluginSpecs: [], pluginSpecs: [], uiExports: [] } ); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index d2ff4e09352d..5d111884144c 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -31,6 +31,7 @@ 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 } from './config'; interface LegacyKbnServer { @@ -40,7 +41,7 @@ interface LegacyKbnServer { close: () => Promise; } -function getLegacyRawConfig(config: Config) { +function getLegacyRawConfig(config: Config, pathConfig: PathConfigType) { const rawConfig = config.toRaw(); // Elasticsearch config is solely handled by the core and legacy platform @@ -49,7 +50,10 @@ function getLegacyRawConfig(config: Config) { delete rawConfig.elasticsearch; } - return rawConfig; + 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 + }; } /** @@ -96,7 +100,7 @@ export class LegacyService implements CoreService { private kbnServer?: LegacyKbnServer; private configSubscription?: Subscription; private setupDeps?: LegacyServiceSetupDeps; - private update$: ConnectableObservable | undefined; + private update$: ConnectableObservable<[Config, PathConfigType]> | undefined; private legacyRawConfig: LegacyConfig | undefined; private legacyPlugins: | { @@ -118,22 +122,25 @@ export class LegacyService implements CoreService { } public async discoverPlugins(): Promise { - this.update$ = this.coreContext.configService.getConfig$().pipe( - tap(config => { + this.update$ = combineLatest( + this.coreContext.configService.getConfig$(), + this.coreContext.configService.atPath('path') + ).pipe( + tap(([config, pathConfig]) => { if (this.kbnServer !== undefined) { - this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config)); + this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig)); } }), tap({ error: err => this.log.error(err) }), publishReplay(1) - ) as ConnectableObservable; + ) as ConnectableObservable<[Config, PathConfigType]>; this.configSubscription = this.update$.connect(); this.settings = await this.update$ .pipe( first(), - map(config => getLegacyRawConfig(config)) + map(([config, pathConfig]) => getLegacyRawConfig(config, pathConfig)) ) .toPromise(); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 025ebce22e2c..8f864dda6b9f 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -17,6 +17,7 @@ * under the License. */ import { of } from 'rxjs'; +import { duration } from 'moment'; import { PluginInitializerContext, CoreSetup, CoreStart } from '.'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; @@ -24,6 +25,7 @@ import { httpServiceMock } from './http/http_service.mock'; import { contextServiceMock } from './context/context_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +import { SharedGlobalConfig } from './plugins'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; @@ -37,7 +39,19 @@ export { savedObjectsClientMock } from './saved_objects/service/saved_objects_cl export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export function pluginInitializerContextConfigMock(config: T) { + const globalConfig: SharedGlobalConfig = { + kibana: { defaultAppId: 'home-mocks', index: '.kibana-tests' }, + elasticsearch: { + shardTimeout: duration('30s'), + requestTimeout: duration('30s'), + pingTimeout: duration('30s'), + startupTimeout: duration('30s'), + }, + path: { data: '/tmp' }, + }; + const mock: jest.Mocked['config']> = { + legacy: { globalConfig$: of(globalConfig) }, create: jest.fn().mockReturnValue(of(config)), createIfExists: jest.fn().mockReturnValue(of(config)), }; diff --git a/src/legacy/server/path/index.test.js b/src/core/server/path/index.test.ts similarity index 72% rename from src/legacy/server/path/index.test.js rename to src/core/server/path/index.test.ts index 08d3568cfbba..048622e1f7ea 100644 --- a/src/legacy/server/path/index.test.js +++ b/src/core/server/path/index.test.ts @@ -17,17 +17,17 @@ * under the License. */ -import { getConfig, getData } from './'; -import { accessSync, R_OK } from 'fs'; +import { accessSync, constants } from 'fs'; +import { getConfigPath, getDataPath } from './'; -describe('Default path finder', function () { +describe('Default path finder', () => { it('should find a kibana.yml', () => { - const configPath = getConfig(); - expect(() => accessSync(configPath, R_OK)).not.toThrow(); + const configPath = getConfigPath(); + expect(() => accessSync(configPath, constants.R_OK)).not.toThrow(); }); it('should find a data directory', () => { - const dataPath = getData(); - expect(() => accessSync(dataPath, R_OK)).not.toThrow(); + const dataPath = getDataPath(); + expect(() => accessSync(dataPath, constants.R_OK)).not.toThrow(); }); }); diff --git a/src/legacy/server/path/index.js b/src/core/server/path/index.ts similarity index 53% rename from src/legacy/server/path/index.js rename to src/core/server/path/index.ts index 130ce59694d0..ef8a3caeefa2 100644 --- a/src/legacy/server/path/index.js +++ b/src/core/server/path/index.ts @@ -18,34 +18,53 @@ */ import { join } from 'path'; -import { accessSync, R_OK } from 'fs'; -import { find } from 'lodash'; -import { fromRoot } from '../../utils'; +import { accessSync, constants } from 'fs'; +import { TypeOf, schema } from '@kbn/config-schema'; +import { fromRoot } from '../utils'; + +const isString = (v: any): v is string => typeof v === 'string'; const CONFIG_PATHS = [ process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), - process.env.CONFIG_PATH, //deprecated + process.env.CONFIG_PATH, // deprecated fromRoot('config/kibana.yml'), - '/etc/kibana/kibana.yml' -].filter(Boolean); + '/etc/kibana/kibana.yml', +].filter(isString); const DATA_PATHS = [ - process.env.DATA_PATH, //deprecated + process.env.DATA_PATH, // deprecated fromRoot('data'), - '/var/lib/kibana' -].filter(Boolean); + '/var/lib/kibana', +].filter(isString); -function findFile(paths) { - const availablePath = find(paths, configPath => { +function findFile(paths: string[]) { + const availablePath = paths.find(configPath => { try { - accessSync(configPath, R_OK); + accessSync(configPath, constants.R_OK); return true; } catch (e) { - //Check the next path + // Check the next path } }); return availablePath || paths[0]; } -export const getConfig = () => findFile(CONFIG_PATHS); -export const getData = () => findFile(DATA_PATHS); +/** + * Get the path where the config files are stored + * @internal + */ +export const getConfigPath = () => findFile(CONFIG_PATHS); +/** + * Get the path where the data can be stored + * @internal + */ +export const getDataPath = () => findFile(DATA_PATHS); + +export type PathConfigType = TypeOf; + +export const config = { + path: 'path', + schema: schema.object({ + data: schema.string({ defaultValue: () => getDataPath() }), + }), +}; diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts new file mode 100644 index 000000000000..547ac08cca76 --- /dev/null +++ b/src/core/server/plugins/plugin_context.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { duration } from 'moment'; +import { BehaviorSubject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { createPluginInitializerContext } from './plugin_context'; +import { CoreContext } from '../core_context'; +import { Env, ObjectToConfigAdapter } from '../config'; +import { loggingServiceMock } from '../logging/logging_service.mock'; +import { getEnvOptions } from '../config/__mocks__/env'; +import { PluginManifest } from './types'; +import { Server } from '../server'; +import { fromRoot } from '../utils'; + +const logger = loggingServiceMock.create(); + +let coreId: symbol; +let env: Env; +let coreContext: CoreContext; +let server: Server; + +function createPluginManifest(manifestProps: Partial = {}): PluginManifest { + return { + id: 'some-plugin-id', + version: 'some-version', + configPath: 'path', + kibanaVersion: '7.0.0', + requiredPlugins: ['some-required-dep'], + optionalPlugins: ['some-optional-dep'], + server: true, + ui: true, + ...manifestProps, + }; +} + +describe('Plugin Context', () => { + beforeEach(async () => { + coreId = Symbol('core'); + env = Env.createDefault(getEnvOptions()); + const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); + server = new Server(config$, env, logger); + await server.setupConfigSchemas(); + coreContext = { coreId, env, logger, configService: server.configService }; + }); + + it('should return a globalConfig handler in the context', async () => { + const manifest = createPluginManifest(); + const opaqueId = Symbol(); + const pluginInitializerContext = createPluginInitializerContext( + coreContext, + opaqueId, + manifest + ); + + expect(pluginInitializerContext.config.legacy.globalConfig$).toBeDefined(); + + const configObject = await pluginInitializerContext.config.legacy.globalConfig$ + .pipe(first()) + .toPromise(); + expect(configObject).toStrictEqual({ + kibana: { defaultAppId: 'home', index: '.kibana' }, + elasticsearch: { + shardTimeout: duration(30, 's'), + requestTimeout: duration(30, 's'), + pingTimeout: duration(30, 's'), + startupTimeout: duration(5, 's'), + }, + path: { data: fromRoot('data') }, + }); + }); +}); diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 773b6e121ce5..dfd1052bbec7 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -17,10 +17,24 @@ * under the License. */ +import { map } from 'rxjs/operators'; +import { combineLatest } from 'rxjs'; import { CoreContext } from '../core_context'; import { PluginWrapper } from './plugin'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; -import { PluginInitializerContext, PluginManifest, PluginOpaqueId } from './types'; +import { + PluginInitializerContext, + PluginManifest, + PluginOpaqueId, + SharedGlobalConfigKeys, +} from './types'; +import { PathConfigType, config as pathConfig } from '../path'; +import { KibanaConfigType, config as kibanaConfig } from '../kibana_config'; +import { + ElasticsearchConfigType, + config as elasticsearchConfig, +} from '../elasticsearch/elasticsearch_config'; +import { pick, deepFreeze } from '../../utils'; import { CoreSetup, CoreStart } from '..'; /** @@ -65,6 +79,27 @@ export function createPluginInitializerContext( * Core configuration functionality, enables fetching a subset of the config. */ config: { + legacy: { + /** + * Global configuration + * Note: naming not final here, it will be renamed in a near future (https://github.com/elastic/kibana/issues/46240) + * @deprecated + */ + globalConfig$: combineLatest( + coreContext.configService.atPath(kibanaConfig.path), + coreContext.configService.atPath(elasticsearchConfig.path), + coreContext.configService.atPath(pathConfig.path) + ).pipe( + map(([kibana, elasticsearch, path]) => + deepFreeze({ + kibana: pick(kibana, SharedGlobalConfigKeys.kibana), + elasticsearch: pick(elasticsearch, SharedGlobalConfigKeys.elasticsearch), + path: pick(path, SharedGlobalConfigKeys.path), + }) + ) + ), + }, + /** * Reads the subset of the config at the `configPath` defined in the plugin * manifest and validates it against the schema in the static `schema` on diff --git a/src/core/server/plugins/plugins_service.test.mocks.ts b/src/core/server/plugins/plugins_service.test.mocks.ts index 13b492e382d6..8d4ba12c8375 100644 --- a/src/core/server/plugins/plugins_service.test.mocks.ts +++ b/src/core/server/plugins/plugins_service.test.mocks.ts @@ -17,8 +17,11 @@ * under the License. */ -export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); -jest.mock('../../../legacy/utils/package_json', () => ({ pkg: mockPackage })); +export const mockPackage = new Proxy( + { raw: { __dirname: '/tmp' } as any }, + { get: (obj, prop) => obj.raw[prop] } +); +jest.mock('../../../core/server/utils/package_json', () => ({ pkg: mockPackage })); export const mockDiscover = jest.fn(); jest.mock('./discovery/plugins_discovery', () => ({ discover: mockDiscover })); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index fd487d9fe00a..b4c8c9886426 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -20,8 +20,12 @@ import { Observable } from 'rxjs'; import { Type } from '@kbn/config-schema'; +import { RecursiveReadonly } from 'kibana/public'; import { ConfigPath, EnvironmentMode, PackageInfo } from '../config'; import { LoggerFactory } from '../logging'; +import { KibanaConfigType } from '../kibana_config'; +import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; +import { PathConfigType } from '../path'; import { CoreSetup, CoreStart } from '..'; /** @@ -195,6 +199,19 @@ export interface Plugin< stop?(): void; } +export const SharedGlobalConfigKeys = { + // We can add more if really needed + kibana: ['defaultAppId', 'index'] as const, + elasticsearch: ['shardTimeout', 'requestTimeout', 'pingTimeout', 'startupTimeout'] as const, + path: ['data'] as const, +}; + +export type SharedGlobalConfig = RecursiveReadonly<{ + kibana: Pick; + elasticsearch: Pick; + path: Pick; +}>; + /** * Context that's available to plugins during initialization stage. * @@ -208,6 +225,7 @@ export interface PluginInitializerContext { }; logger: LoggerFactory; config: { + legacy: { globalConfig$: Observable }; create: () => Observable; createIfExists: () => Observable; }; diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 9ae4b3220282..4d9bcdda3c8a 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -49,6 +49,28 @@ const mockMappings = { }, }, }, + alert: { + properties: { + actions: { + type: 'nested', + properties: { + group: { + type: 'keyword', + }, + actionRef: { + type: 'keyword', + }, + actionTypeId: { + type: 'keyword', + }, + params: { + enabled: false, + type: 'object', + }, + }, + }, + }, + }, hiddenType: { properties: { description: { @@ -108,6 +130,16 @@ describe('Filter Utils', () => { ); }); + test('Assemble filter with a nested filter', () => { + expect( + validateConvertFilterToKueryNode( + ['alert'], + 'alert.attributes.actions:{ actionTypeId: ".server-log" }', + mockMappings + ) + ).toEqual(esKuery.fromKueryExpression('alert.actions:{ actionTypeId: ".server-log" }')); + }); + test('Lets make sure that we are throwing an exception if we get an error', () => { expect(() => { validateConvertFilterToKueryNode( @@ -129,13 +161,13 @@ describe('Filter Utils', () => { describe('#validateFilterKueryNode', () => { test('Validate filter query through KueryNode - happy path', () => { - const validationObject = validateFilterKueryNode( - esKuery.fromKueryExpression( + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), - ['foo'], - mockMappings - ); + types: ['foo'], + indexMapping: mockMappings, + }); expect(validationObject).toEqual([ { @@ -183,14 +215,34 @@ describe('Filter Utils', () => { ]); }); + test('Validate nested filter query through KueryNode - happy path', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.actions:{ actionTypeId: ".server-log" }' + ), + types: ['alert'], + indexMapping: mockMappings, + hasNestedKey: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionTypeId', + type: 'alert', + }, + ]); + }); + test('Return Error if key is not wrapper by a saved object type', () => { - const validationObject = validateFilterKueryNode( - esKuery.fromKueryExpression( + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), - ['foo'], - mockMappings - ); + types: ['foo'], + indexMapping: mockMappings, + }); expect(validationObject).toEqual([ { @@ -239,13 +291,13 @@ describe('Filter Utils', () => { }); test('Return Error if key of a saved object type is not wrapped with attributes', () => { - const validationObject = validateFilterKueryNode( - esKuery.fromKueryExpression( + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), - ['foo'], - mockMappings - ); + types: ['foo'], + indexMapping: mockMappings, + }); expect(validationObject).toEqual([ { @@ -296,13 +348,13 @@ describe('Filter Utils', () => { }); test('Return Error if filter is not using an allowed type', () => { - const validationObject = validateFilterKueryNode( - esKuery.fromKueryExpression( + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), - ['foo'], - mockMappings - ); + types: ['foo'], + indexMapping: mockMappings, + }); expect(validationObject).toEqual([ { @@ -351,13 +403,13 @@ describe('Filter Utils', () => { }); test('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { - const validationObject = validateFilterKueryNode( - esKuery.fromKueryExpression( + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), - ['foo'], - mockMappings - ); + types: ['foo'], + indexMapping: mockMappings, + }); expect(validationObject).toEqual([ { @@ -407,11 +459,12 @@ describe('Filter Utils', () => { }); test('Return Error if filter is using an non-existing key null key', () => { - const validationObject = validateFilterKueryNode( - esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), - ['foo'], - mockMappings - ); + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), + types: ['foo'], + indexMapping: mockMappings, + }); + expect(validationObject).toEqual([ { astPath: 'arguments.0', diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index e7509933a38a..9d796c279a77 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -23,6 +23,8 @@ import { IndexMapping } from '../../mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { esKuery } from '../../../../../plugins/data/server'; +const astFunctionType = ['is', 'range', 'nested']; + export const validateConvertFilterToKueryNode = ( allowedTypes: string[], filter: string, @@ -31,12 +33,14 @@ export const validateConvertFilterToKueryNode = ( if (filter && filter.length > 0 && indexMapping) { const filterKueryNode = esKuery.fromKueryExpression(filter); - const validationFilterKuery = validateFilterKueryNode( - filterKueryNode, - allowedTypes, + const validationFilterKuery = validateFilterKueryNode({ + astFilter: filterKueryNode, + types: allowedTypes, indexMapping, - filterKueryNode.type === 'function' && ['is', 'range'].includes(filterKueryNode.function) - ); + storeValue: + filterKueryNode.type === 'function' && astFunctionType.includes(filterKueryNode.function), + hasNestedKey: filterKueryNode.type === 'function' && filterKueryNode.function === 'nested', + }); if (validationFilterKuery.length === 0) { throw SavedObjectsErrorHelpers.createBadRequestError( @@ -90,26 +94,44 @@ interface ValidateFilterKueryNode { type: string | null; } -export const validateFilterKueryNode = ( - astFilter: esKuery.KueryNode, - types: string[], - indexMapping: IndexMapping, - storeValue: boolean = false, - path: string = 'arguments' -): ValidateFilterKueryNode[] => { +interface ValidateFilterKueryNodeParams { + astFilter: esKuery.KueryNode; + types: string[]; + indexMapping: IndexMapping; + hasNestedKey?: boolean; + nestedKeys?: string; + storeValue?: boolean; + path?: string; +} + +export const validateFilterKueryNode = ({ + astFilter, + types, + indexMapping, + hasNestedKey = false, + nestedKeys, + storeValue = false, + path = 'arguments', +}: ValidateFilterKueryNodeParams): ValidateFilterKueryNode[] => { + let localNestedKeys: string | undefined; return astFilter.arguments.reduce( (kueryNode: string[], ast: esKuery.KueryNode, index: number) => { + if (hasNestedKey && ast.type === 'literal' && ast.value != null) { + localNestedKeys = ast.value; + } if (ast.arguments) { const myPath = `${path}.${index}`; return [ ...kueryNode, - ...validateFilterKueryNode( - ast, + ...validateFilterKueryNode({ + astFilter: ast, types, indexMapping, - ast.type === 'function' && ['is', 'range'].includes(ast.function), - `${myPath}.arguments` - ), + storeValue: ast.type === 'function' && astFunctionType.includes(ast.function), + path: `${myPath}.arguments`, + hasNestedKey: ast.type === 'function' && ast.function === 'nested', + nestedKeys: localNestedKeys, + }), ]; } if (storeValue && index === 0) { @@ -118,10 +140,17 @@ export const validateFilterKueryNode = ( ...kueryNode, { astPath: splitPath.slice(0, splitPath.length - 1).join('.'), - error: hasFilterKeyError(ast.value, types, indexMapping), - isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), - key: ast.value, - type: getType(ast.value), + error: hasFilterKeyError( + nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value, + types, + indexMapping + ), + isSavedObjectAttr: isSavedObjectAttr( + nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value, + indexMapping + ), + key: nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value, + type: getType(nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value), }, ]; } @@ -164,7 +193,6 @@ export const hasFilterKeyError = ( return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`; } else if (key.includes('.')) { const keySplit = key.split('.'); - if (keySplit.length <= 1 || !types.includes(keySplit[0])) { return `This type ${keySplit[0]} is not allowed`; } @@ -177,7 +205,10 @@ export const hasFilterKeyError = ( if ( (keySplit.length === 2 && !fieldDefined(indexMapping, keySplit[1])) || (keySplit.length > 2 && - !fieldDefined(indexMapping, keySplit[0] + '.' + keySplit.slice(2, keySplit.length))) + !fieldDefined( + indexMapping, + `${keySplit[0]}.${keySplit.slice(2, keySplit.length).join('.')}` + )) ) { return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a3f283ee4a8b..7e1226aa7238 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -108,6 +108,7 @@ import { PingParams } from 'elasticsearch'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import { Readable } from 'stream'; +import { RecursiveReadonly as RecursiveReadonly_2 } from 'kibana/public'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; @@ -1017,6 +1018,9 @@ export type PluginInitializer { // (undocumented) config: { + legacy: { + globalConfig$: Observable; + }; create: () => Observable; createIfExists: () => Observable; }; @@ -1759,5 +1763,6 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:228:15 - (ae-forgotten-export) The symbol "SharedGlobalConfig" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index ceadec03cf5c..e7166f30caa3 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -33,6 +33,7 @@ import { config as elasticsearchConfig } from './elasticsearch'; import { config as httpConfig } from './http'; import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; +import { config as pathConfig } from './path'; import { config as kibanaConfig } from './kibana_config'; import { config as savedObjectsConfig } from './saved_objects'; import { config as uiSettingsConfig } from './ui_settings'; @@ -208,6 +209,7 @@ export class Server { public async setupConfigSchemas() { const schemas: Array<[ConfigPath, Type]> = [ + [pathConfig.path, pathConfig.schema], [elasticsearchConfig.path, elasticsearchConfig.schema], [loggingConfig.path, loggingConfig.schema], [httpConfig.path, httpConfig.schema], diff --git a/src/legacy/utils/from_root.ts b/src/core/server/utils/from_root.ts similarity index 100% rename from src/legacy/utils/from_root.ts rename to src/core/server/utils/from_root.ts diff --git a/src/core/server/utils/index.ts b/src/core/server/utils/index.ts new file mode 100644 index 000000000000..86924c559e5f --- /dev/null +++ b/src/core/server/utils/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 * from './from_root'; +export * from './package_json'; diff --git a/src/legacy/utils/package_json.ts b/src/core/server/utils/package_json.ts similarity index 84% rename from src/legacy/utils/package_json.ts rename to src/core/server/utils/package_json.ts index 7cc67b413922..ab1700e681a9 100644 --- a/src/legacy/utils/package_json.ts +++ b/src/core/server/utils/package_json.ts @@ -20,8 +20,8 @@ import { dirname } from 'path'; export const pkg = { - __filename: require.resolve('../../../package.json'), - __dirname: dirname(require.resolve('../../../package.json')), + __filename: require.resolve('../../../../package.json'), + __dirname: dirname(require.resolve('../../../../package.json')), // eslint-disable no-var-requires - ...require('../../../package.json'), + ...require('../../../../package.json'), }; diff --git a/src/core/utils/pick.ts b/src/core/utils/pick.ts index 08288343d907..24801774727f 100644 --- a/src/core/utils/pick.ts +++ b/src/core/utils/pick.ts @@ -17,7 +17,7 @@ * under the License. */ -export function pick(obj: T, keys: K[]): Pick { +export function pick(obj: T, keys: readonly K[]): Pick { return keys.reduce((acc, key) => { if (obj.hasOwnProperty(key)) { acc[key] = obj[key]; diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index e487ac0567f7..a693c6ce8a8a 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -46,6 +46,7 @@ export const CopySourceTask = { 'typings/**', 'webpackShims/**', 'config/kibana.yml', + 'config/apm.js', 'tsconfig*.json', '.i18nrc.json', 'kibana.d.ts' diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 6cfcaca5843b..e5edf5bd9c26 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -14,6 +14,8 @@ cacheDir="$HOME/.kibana" RED='\033[0;31m' C_RESET='\033[0m' # Reset color +export NODE_OPTIONS="$NODE_OPTIONS --throw-deprecation" + ### ### Since the Jenkins logging output collector doesn't look like a TTY ### Node/Chalk and other color libs disable their color output. But Jenkins diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index f5c20da89dcf..23bba78e0527 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -98,8 +98,9 @@ export default { '^.+\\.html?$': 'jest-raw-loader', }, transformIgnorePatterns: [ - // ignore all node_modules except @elastic/eui which requires babel transforms to handle dynamic import() - '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)[/\\\\].+\\.js$', + // ignore all node_modules except @elastic/eui and monaco-editor which both require babel transforms to handle dynamic import() + // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) + '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)(?![\\/\\\\]monaco-editor)[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js' ], snapshotSerializers: [ diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx index 0fa0ec732c77..b99249b2b001 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -193,12 +193,20 @@ function EditorUI({ previousStateLocation = 'stored' }: EditorProps) { /> -
+ + {/* Axe complains about Ace's textarea element missing a label, which interferes with our + automated a11y tests per #52136. This wrapper does nothing to address a11y but it does + satisfy Axe. */} + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} +
); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx index c167155bd18a..3690ea61d568 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -87,7 +87,14 @@ function EditorOutputUI() { return (
-
+ {/* Axe complains about Ace's textarea element missing a label, which interferes with our + automated a11y tests per #52136. This wrapper does nothing to address a11y but it does + satisfy Axe. */} + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} +